Graphql - universal permission guards - graphql

I am trying to implement permission guards in a graphql backend using apollo server. The following code works:
Resolvers
const Notification = require('../../database/models/notifications');
const Task = require('../../database/models/tasks');
notification: combineResolvers(isNotificationOwner, async (_, { id }) => {
try {
const notification = await Notification.findById(id);
return notification;
} catch (error) {
throw error;
}
})
task: combineResolvers(isTaskOwner, async (_, { id }) => {
try {
const task = await Task.findById(id);
return task;
} catch (error) {
throw error;
}
})
Resolver Middleware (permission guards)
const Notification = require('../../database/models/notifications');
const Task = require('../../database/models/tasks');
// userId is the id of the logged in user retrieved from the context
module.exports.isNotificationOwner = async (_, { id }, { userId }) => {
try {
const notification = await Notification.findById(id);
if (notification.user.toString() !== userId) {
throw new ForbiddenError('You are not the owner');
}
return skip;
} catch (error) {
throw error;
}
}
module.exports.isTaskOwner = async (_, { id }, { userId }) => {
try {
const task = await Task.findById(id);
if (task.user.toString() !== userId) {
throw new ForbiddenError('You are not the owner');
}
return skip;
} catch (error) {
throw error;
}
}
Going on like this will create a lot of duplicate code and does not feel very DRY. Therefore, I am trying to create a more universal solution, to no evail so far.
What I tried:
Resolvers
const Notification = require('../../database/models/notifications');
const Task = require('../../database/models/tasks');
notification: combineResolvers(isOwner, async (_, { id }) => {
try {
const notification = await Notification.findById(id);
return notification;
} catch (error) {
throw error;
}
})
task: combineResolvers(isOwner, async (_, { id }) => {
try {
const task = await Task.findById(id);
return task;
} catch (error) {
throw error;
}
})
Resolver Middleware
const Notification = require('../../database/models/notifications');
const Task = require('../../database/models/tasks');
module.exports.isOwner = async (_, { id, collection }, { userId }) => {
try {
const document = await collection.findById(id);
if (document.user.toString() !== userId) {
throw new ForbiddenError('You are not the owner');
}
return skip;
} catch (error) {
throw error;
}
}
I am unable to pass a collection name as an argument to the middleware resolver.
I would be tremendously thankful for any kind of help!

Based off your code, it seems like you're looking for making isOwner a higher-order function, so that you can pass in the collection, and it returns the curried method.
module.exports.isOwner = (collection) => {
return async (_, { id }, { userId }) => {
try {
const document = await collection.findById(id);
if (document.user.toString() !== userId) {
throw new ForbiddenError('You are not the owner');
}
return skip;
} catch (error) {
throw error;
}
}
}
Usage:
const resolvers = {
Query: {
task: combineResolvers(isOwner(Task), async (_, { id }) => {
try {
const task = await Task.findById(id);
return task;
} catch (error) {
throw error;
}
})
},
};

Related

Providing two combined Reducers for my redux saga store prevents my websocket channel message from triggering, but only one does not?

Configured my store this way with redux toolkit for sure
const rootReducer = combineReducers({
someReducer,
systemsConfigs
});
const store = return configureStore({
devTools: true,
reducer: rootReducer ,
// middleware: [middleware, logger],
middleware: (getDefaultMiddleware) => getDefaultMiddleware({ thunk: false }).concat(middleware),
});
middleware.run(sagaRoot)
And thats my channel i am connecting to it
export function createSocketChannel(
productId: ProductId,
pair: string,
createSocket = () => new WebSocket('wss://somewebsocket')
) {
return eventChannel<SocketEvent>((emitter) => {
const socket_OrderBook = createSocket();
socket_OrderBook.addEventListener('open', () => {
emitter({
type: 'connection-established',
payload: true,
});
socket_OrderBook.send(
`subscribe-asdqwe`
);
});
socket_OrderBook.addEventListener('message', (event) => {
if (event.data?.includes('bids')) {
emitter({
type: 'message',
payload: JSON.parse(event.data),
});
//
}
});
socket_OrderBook.addEventListener('close', (event: any) => {
emitter(new SocketClosedByServer());
});
return () => {
if (socket_OrderBook.readyState === WebSocket.OPEN) {
socket_OrderBook.send(
`unsubscribe-order-book-${pair}`
);
}
if (socket_OrderBook.readyState === WebSocket.OPEN || socket_OrderBook.readyState === WebSocket.CONNECTING) {
socket_OrderBook.close();
}
};
}, buffers.expanding<SocketEvent>());
}
And here's how my saga connecting handlers looks like
export function* handleConnectingSocket(ctx: SagaContext) {
try {
const productId = yield select((state: State) => state.productId);
const requested_pair = yield select((state: State) => state.requested_pair);
if (ctx.socketChannel === null) {
ctx.socketChannel = yield call(createSocketChannel, productId, requested_pair);
}
//
const message: SocketEvent = yield take(ctx.socketChannel!);
if (message.type !== 'connection-established') {
throw new SocketUnexpectedResponseError();
}
yield put(connectedSocket());
} catch (error: any) {
reportError(error);
yield put(
disconnectedSocket({
reason: SocketStateReasons.BAD_CONNECTION,
})
);
}
}
export function* handleConnectedSocket(ctx: SagaContext) {
try {
while (true) {
if (ctx.socketChannel === null) {
break;
}
const events = yield flush(ctx.socketChannel);
const startedExecutingAt = performance.now();
if (Array.isArray(events)) {
const deltas = events.reduce(
(patch, event) => {
if (event.type === 'message') {
patch.bids.push(...event.payload.data?.bids);
patch.asks.push(...event.payload.data?.asks);
//
}
//
return patch;
},
{ bids: [], asks: [] } as SocketMessage
);
if (deltas.bids.length || deltas.asks.length) {
yield putResolve(receivedDeltas(deltas));
}
}
yield call(delayNextDispatch, startedExecutingAt);
}
} catch (error: any) {
reportError(error);
yield put(
disconnectedSocket({
reason: SocketStateReasons.UNKNOWN,
})
);
}
}
After Debugging I got the following:
The Thing is that when I Provide one Reducer to my store the channel works well and data is fetched where as when providing combinedReducers I am getting
an established connection from my handleConnectingSocket generator function
and an empty event array [] from
const events = yield flush(ctx.socketChannel) written in handleConnectedSocket
Tried to clarify as much as possible
ok so I start refactoring my typescript by changing the types, then saw all the places that break, there was a problem in my sagas.tsx.
Ping me if someone faced such an issue in the future

React Hook useEffect has some missing dependencies

I tried to run a program, but I got this warning message: Line 75:8: React Hook useEffect has missing dependencies: 'client' and 'loading'. Either include them or remove the dependency array react-hooks/exhaustive-deps
This is my Code
const UserList = ({ setSelectedUsers }) => {
const { client } = useChatContext();
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [listEmpty, setListEmpty] = useState(false);
const [error, setError] = useState(false);
useEffect(() => {
const getUsers = async () => {
if(loading) return;
setLoading(true);
try {
const response = await client.queryUsers(
{ id: { $ne: client.userID } },
{ id: 1 },
{ limit: 8 }
);
if(response.users.length) {
setUsers(response.users);
} else {
setListEmpty(true);
}
} catch (error) {
setError(true);
}
setLoading(false);
}
if(client) getUsers()
}, []);
You can get rid of the warning by // eslint-disable-next-line react-hooks/exhaustive-deps above the useEffect dependency. useEffect sometimes suggests useless dependencies that should not be actually added in dependency array
useEffect(() => {
const getUsers = async () => {
if(loading) return;
setLoading(true);
try {
const response = await client.queryUsers(
{ id: { $ne: client.userID } },
{ id: 1 },
{ limit: 8 }
);
if(response.users.length) {
setUsers(response.users);
} else {
setListEmpty(true);
}
} catch (error) {
setError(true);
}
setLoading(false);
}
if(client) getUsers()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);```

How do I add recursive logic in resolvers using GraphQL mutations?

Is it possible to add logic in resolvers using GraphQL mutations?
I am trying to create a four-digit string as an alias for a post if the user does not provide it. Then, I would like to check the database to see if the four-digit string exists. If the string exists, I would like to create another four-digit string recursively.
At the moment, I'm exploring adding logic to mutations within resolvers, but I'm not sure if this is doable. I'm using these documents for my foundation: graphql.org sequelize.org
This is my current code block:
Working as of 12/4/2020
const MakeSlug = require("./services/MakeSlug");
const resolvers = {
Query: {
async allLinks(root, args, { models }) {
return models.Link.findAll();
},
async link(root, { id }, { models }) {
return models.Link.findByPk(id);
}
},
Mutation: {
async createLink(root, { slug, description, link }, { models }) {
if (slug !== undefined) {
const foundSlug = await models.Link.findOne({
where: { slug: slug }
});
if (foundSlug === undefined) {
return await models.Link.create({
slug,
description,
link,
shortLink: `https://shink.com/${slug}`
});
} else {
throw new Error(slug + " exists. Try a new short description.");
}
}
if (slug === undefined) {
const MAX_ATTEMPTS = 10;
let attempts = 0;
while (attempts < MAX_ATTEMPTS) {
attempts++;
let madeSlug = MakeSlug(4);
const foundSlug = await models.Link.findOne({
where: { slug: madeSlug }
});
if (foundSlug !== undefined) {
return await models.Link.create({
slug: madeSlug,
description,
link,
shortLink: `https://shink.com/${madeSlug}`
});
}
}
throw new Error("Unable to generate unique alias.");
}
}
}
};
module.exports = resolvers;
This is my full codebase.
Thank you!
A while loop solved the challenge. Thanks xadm.
const MakeSlug = require("./services/MakeSlug");
const resolvers = {
Query: {
async allLinks(root, args, { models }) {
return models.Link.findAll();
},
async link(root, { id }, { models }) {
return models.Link.findByPk(id);
}
},
Mutation: {
async createLink(root, { slug, description, link }, { models }) {
if (slug !== undefined) {
const foundSlug = await models.Link.findOne({
where: { slug: slug }
});
if (foundSlug === undefined) {
return await models.Link.create({
slug,
description,
link,
shortLink: `https://shink.com/${slug}`
});
} else {
throw new Error(slug + " exists. Try a new short description.");
}
}
if (slug === undefined) {
const MAX_ATTEMPTS = 10;
let attempts = 0;
while (attempts < MAX_ATTEMPTS) {
attempts++;
let madeSlug = MakeSlug(4);
const foundSlug = await models.Link.findOne({
where: { slug: madeSlug }
});
if (foundSlug !== undefined) {
return await models.Link.create({
slug: madeSlug,
description,
link,
shortLink: `https://shink.com/${madeSlug}`
});
}
}
throw new Error("Unable to generate unique alias.");
}
}
}
};
module.exports = resolvers;

How can i return rejected from async-await?

I would like to return rejected, when fetch request is fail. How can i reject async await?
class FetchUrl {
static async getJson(api) {
try {
const response = await fetch(api);
if (response.ok) {
const questions = await response.json();
return questions;
}
} catch (error) {
throw new Error("Request Failed!");
}
}
}
FetchUrl.getJson(this.api).then((resolved) => {
console.log(resolved)
// this sampe of code is executing even fetch is rejected.. what can i do
to avoid it?
}, rejected => {
console.log(rejected)
})
}

Await and Async callbacks hell

I want to make the UserDataGenerator class works like a traditional SYNC class.
My expectation is that userData.outputStructure can give me the data prepared.
let userData = new UserDataGenerator(dslContent)
userData.outputStructure
getFieldDescribe(this.inputStructure.tableName, field) is a ASYNC call which invokes Axios.get
Below is my current progress but it's still not waiting for the data ready when I print out the userData.outputStructure
export default class UserDataGenerator {
inputStructure = null;
outputStructure = null;
fieldDescribeRecords = [];
constructor(dslContent) {
this.outputStructure = Object.assign({}, dslContent, initSections)
process()
}
async process() {
await this.processSectionList()
return this.outputStructure
}
async processSectionList() {
await this.inputStructure.sections.map(section => {
this.outputStructure.sections.push(this.processSection(section));
})
}
async processSection(section) {
let outputSection = {
name: null,
fields: []
}
let outputFields = await section.fields.map(async(inputField) => {
return await this._processField(inputField).catch(e => {
throw new SchemaError(e, this.inputStructure.tableName, inputField)
})
})
outputSection.fields.push(outputFields)
return outputSection
}
async _processField(field) {
let resp = await ai
switch (typeof field) {
case 'string':
let normalizedDescribe = getNormalizedFieldDescribe(resp.data)
return new FieldGenerator(normalizedDescribe, field).outputFieldStructure
}
}
You're trying to await arrays, which doesn't work as you expect. When dealing with arrays of promises, you still need to use Promise.all before you can await it - just like you cannot chain .then on the array.
So your methods should look like this:
async processSectionList() {
const sections = await Promise.all(this.inputStructure.sections.map(section =>
this.processSection(section)
));
this.outputStructure.sections.push(...sections);
}
async processSection(section) {
return {
name: null,
fields: [await Promise.all(section.fields.map(inputField =>
this._processField(inputField).catch(e => {
throw new SchemaError(e, this.inputStructure.tableName, inputField)
})
))]
};
}

Resources