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;
Related
I have a custom controller to replace a default querie named "groupedOrders"
but when I try to test the query the response is
"message": "Cannot read property 'user' of undefined",
the code in api/grouped-order/controllers/grouped-order.js is:
module.exports = {
byUser: async ctx => {
const user = ctx.state.user
const resTypeUser = await strapi.query('tipo-usuario').find({ _id: user.tipo_usuario })
var resGroupedOrder = { error: true }
if (resTypeUser[0].super_admin) {
resGroupedOrder = await strapi.query('grouped-order').find()
} else if (resTypeUser[0].cliente) {
resGroupedOrder = await strapi.query('grouped-order').find({ users_permissions_user: user._id })
}
return resGroupedOrder
}
};
and the code in api/grouped-order/config/schema.graphql.js is:
module.exports = {
definition: ``,
query: ``,
type: {},
resolver: {
Query: {
groupedOrders: {
description: "Retornar todos los pedidos dependiendo el tipo de usuario",
resolverOf: "application::grouped-order.grouped-order.byUser",
policies: [
'plugins::users-permissions.permissions',
],
resolver: async (obj, options, ctx) => {
return await strapi.controllers["grouped-order"].byUser(ctx);
}
}
},
},
}
the test that I try to run in http://localhost:1337/graphql is:
query groupedOrders($where:JSON){
groupedOrders(where:$where){
createdAt
detail
status
}
}
and the HTTP HEADERS:
{
"Authorization": "Bearer TOKENJWT"
}
Solved, I just add this code on my controller:
if (!ctx.state && ctx.request && ctx.request.header && ctx.request.header.authorization) {
const { id } = await strapi.plugins["users-permissions"].services.jwt.getToken(ctx);
ctx.state.user = await strapi.plugins['users-permissions'].services.user.fetchAuthenticatedUser(id);
}
we can add the code globally, like this: https://github.com/strapi/strapi/issues/9159#issuecomment-789484109
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;
}
})
},
};
I've got a Nuxt app with a Checkout page, and on the backend I'm using Strapi GraphQL. I created several coupons in Stripe, and I want to be able to verify the coupons from the Checkout page, but I'm struggling to figure out how to do this. Here's what I have so far:
Frontend (Nuxt)
Cart.vue:
this.$apollo.query({
query: validateCouponQuery,
variables: {
coupon: this.coupon
}
})
validateCoupon.gql:
query($coupon: String!) {
validateCoupon(coupon: $coupon) {
id
name
valid
}
}
Backend (Strapi):
./order/config/routes.json:
{
"method": "GET",
"path": "/orders/validateCoupon",
"handler": "order.validateCoupon",
"config": {
"policies": []
}
}
./order/config/schema.graphql.js:
const { sanitizeEntity } = require('strapi-utils');
module.exports = {
query: `
validateCoupon(coupon: String): Order
`,
resolver: {
Query: {
validateCoupon: {
resolverOf: 'Order.validateCoupon',
async resolver(_, { coupon }) {
const entity = await strapi.services.order.validateCoupon({ coupon });
return sanitizeEntity(entity, { model: strapi.models.order });
}
}
}
}
}
./order/controllers/order.js:
'use strict';
require('dotenv').config();
const stripe = require('stripe')(`${process.env.STRIPE_SECRET_KEY}`);
module.exports = {
validateCoupon: async ctx => {
const { coupon } = ctx.request.body;
console.log('request coupon: ', coupon);
try {
const coupons = await stripe.coupons.list({ limit: 3 }, function (err, coupons) {
console.log('err: ', err);
console.log('coupons: ', coupons);
});
return coupons;
} catch (err) {
console.error('error validating coupon: ', err)
}
}
};
Right now, when I try to run the query in the GraphQL Playground, I get the error strapi.services.order.validateCoupon is not a function.
I'm fairly new to GraphQL... is there a better way to fetch external data than running a query?
****Update****
I've added my order service, which has gotten rid of that original error. The issue now is that even though the service appears to be returning the coupon correctly, the const entity in the schema.graphql.js returns undefined for some reason. I wonder if the resolver can't be async/await?
./order/services/order.js:
'use strict';
const stripe = require('stripe')(`${process.env.STRIPE_SECRET_KEY}`);
module.exports = {
validateCoupon: ({ coupon }) => {
stripe.coupons.list()
.then(coupons => {
return coupons.data.filter(c => {
return (c.name === coupon && c.valid) ? c : null;
});
console.log('found: ', found);
})
.catch(err => console.error(err));
}
};
Well, your code to look up coupons on Stripe looks just fine! Looks like Strapi expects your service to be at ./order/services/order.jsācould it be as simple as that? Your example shows it at ./order/controllers/order.js. https://strapi.io/documentation/3.0.0-beta.x/concepts/services.html#custom-services
So I ended up creating a Coupon model in the Strapi content builder. This enabled me to more easily return a Coupon object from my GraphQL query. It's not ideal because I'm having to make sure I create both a Stripe and Strapi coupon object to match, however I also don't anticipate on creating too many coupons in the first place.
My updated code looks like this:
schema.graphql.js:
const { sanitizeEntity } = require('strapi-utils/lib');
module.exports = {
query: `
validateCoupon(coupon: String): Coupon
`,
resolver: {
Query: {
validateCoupon: {
description: 'Validate Stripe coupon',
resolver: 'application::order.order.validateCoupon',
}
}
}
}
./order/controllers/order.js:
'use strict';
require('dotenv').config();
const { sanitizeEntity } = require('strapi-utils');
module.exports = {
validateCoupon: async ctx => {
const coupon = ctx.query._coupon;
const found = await strapi.services.order.validateCoupon({ coupon });
return sanitizeEntity(found, { model: strapi.models.order });
}
};
./order/services/order.js:
'use strict';
const stripe = require('stripe')(`${process.env.STRIPE_SECRET_KEY}`);
module.exports = {
async validateCoupon({ coupon }) {
let foundCoupon = null;
try {
const coupons = await stripe.coupons.list();
const found = coupons.data.filter(c => {
return (c.name === coupon && c.valid) ? c : null;
});
if (found) foundCoupon = found[0];
} catch (err) {
console.error(err);
}
return foundCoupon;
}
};
There isn't any documentation for how the array meta info (arrayLength and sliceStart) should be implemented using facebook's graphql-relay-js helper library.
https://github.com/graphql/graphql-relay-js/issues/199
I managed to get it to work using the following implemention however I am guessing there is an easier/more correct way to do this.
Retrieve rows and row count from database
function transformRole(role: Role) {
return { ...role, roleId: role.id };
}
async function getRolesSlice({ roleId, after, first, last, before }: any): Promise<[Role[], number]> {
const queryBuilder = repository.createQueryBuilder();
if (roleId !== undefined) {
queryBuilder.where('id = :roleId', { roleId });
}
if (before) {
const beforeId = cursorToOffset(before);
queryBuilder.where('id < :id', { id: beforeId });
}
if (after) {
const afterId = cursorToOffset(after);
queryBuilder.where({
id: MoreThan(Number(afterId))
});
}
if (first === undefined && last === undefined) {
queryBuilder.orderBy('id', 'ASC');
}
if (first) {
queryBuilder.orderBy('id', 'ASC').limit(first);
}
if (last) {
queryBuilder.orderBy('id', 'DESC').limit(last);
}
return Promise.all([
queryBuilder.getMany()
.then(roles => roles.map(transformRole)),
repository.count() // Total number of roles
]);
}
Roles resolver
resolve: (_, args) =>
getRolesSlice(args)
.then(([results, count]) => {
const firstId = results[0] && results[0].roleId;
let sliceStart = 0;
if (args.first) {
sliceStart = firstId;
}
if (args.last) {
sliceStart = Math.max(firstId - args.last, 0);
}
if (args.after && args.last) {
sliceStart += 1;
}
return connectionFromArraySlice(
results,
args,
{
arrayLength: count + 1,
sliceStart
}
);
})
},
Edit:
This is what I came up with which is a little cleaner and seems to be working correctly.
const initialize = () => {
repository = getConnection().getRepository(Role);
}
function transformRole(role: Role) {
return { ...role, roleId: role.id };
}
function getRolesSlice(args: any):
Promise<[
Role[],
any,
{ arrayLength: number; sliceStart: number; }
]> {
if (!repository) initialize();
const { roleId, after, first, last, before } = args;
const queryBuilder = repository.createQueryBuilder();
if (roleId !== undefined) {
queryBuilder.where('id = :roleId', { roleId });
}
if (before !== undefined) {
const beforeId = cursorToOffset(before);
queryBuilder.where({
id: LessThan(beforeId)
});
}
if (after !== undefined) {
const afterId = cursorToOffset(after);
queryBuilder.where({
id: MoreThan(Number(afterId))
});
}
if (first !== undefined) {
queryBuilder.orderBy('id', 'ASC').limit(first);
} else if (last !== undefined) {
queryBuilder.orderBy('id', 'DESC').limit(last);
} else {
queryBuilder.orderBy('id', 'ASC');
}
return Promise.all([
queryBuilder.getMany()
.then(roles => roles.map(transformRole))
.then(roles => last !== undefined ? roles.slice().reverse() : roles),
repository.count()
]).then(([roles, totalCount]) =>
[
roles,
args,
{
arrayLength: totalCount + 1,
sliceStart: roles[0] && roles[0].roleId
}
]
);
}
// Resolver
roles: {
type: rolesConnection,
args: {
...connectionArgs,
roleId: {
type: GraphQLString
}
},
resolve: (_, args) =>
getRolesSlice(args)
.then((slice) => connectionFromArraySlice(...slice))
},
I have written integration tests for graphql-js subscriptions, which are showing weird behavior.
My graphq-js subscription works perfectly in GraphiQL. But when the same subscriptions is called from unit test, it fails.
Ggraphql-Js object, with resolve function and subscribe function
return {
type: outputType,
args: {
input: {type: new GraphQLNonNull(inputType)},
},
resolve(payload, args, context, info) {
const clientSubscriptionId = (payload) ? payload.subscriptionId : null;
const object = (payload) ? payload.object : null;
var where = null;
var type = null;
var target = null;
if (object) {
where = (payload) ? payload.object.where : null;
type = (payload) ? payload.object.type : null;
target = (payload) ? payload.object.target : null;
}
return Promise.resolve(subscribeAndGetPayload(payload, args, context, info))
.then(payload => ({
clientSubscriptionId, where, type, target, object: payload.data,
}));
},
subscribe: withFilter(
() => pubSub.asyncIterator(modelName),
(payload, variables, context, info) => {
const subscriptionPayload = {
clientSubscriptionId: variables.input.clientSubscriptionId,
remove: variables.input.remove,
create: variables.input.create,
update: variables.input.update,
opts: variables.input.options,
};
subscriptionPayload.model = model;
try {
pubSub.subscribe(info.fieldName, null, subscriptionPayload);
} catch (ex) {
console.log(ex);
}
return true;
}
),
};
Subscription query
subscription {
Customer(input: {create: true, clientSubscriptionId: 112}) {
customer {
id
name
age
}
}
}
Mutation query
mutation {
Customer {
CustomerCreate (input:{data:{name:"Atif 50", age:50}}) {
obj {
id
name
}
}
}
}
Integration Test
'use strict';
const ws = require('ws');
const { SubscriptionClient } = require('subscriptions-transport-ws');
const { ApolloClient } = require('apollo-client');
const { HttpLink } = require('apollo-link-http');
const { InMemoryCache } = require('apollo-cache-inmemory');
const Promise = require('bluebird');
const expect = require('chai').expect;
const chai = require('chai').use(require('chai-http'));
const server = require('../server/server');
const gql = require('graphql-tag');
let apollo;
let networkInterface;
const GRAPHQL_ENDPOINT = 'ws://localhost:5000/subscriptions';
describe('Subscription', () => {
before(async () => {
networkInterface = new SubscriptionClient(
GRAPHQL_ENDPOINT, { reconnect: true }, ws);
apollo = new ApolloClient({
networkInterface ,
link: new HttpLink({ uri: 'http://localhost:3000/graphql' }),
cache: new InMemoryCache()
});
});
after(done => {
networkInterface.close() ;
});
it('subscription', async () => {
const client = () => apollo;
// SUBSCRIBE and make a promise
const subscriptionPromise = new Promise((resolve, reject) => {
client().subscribe({
query: gql`
subscription {
Customer(input: {create: true,
clientSubscriptionId: 112,
options: {where: {age: 50}}}) {
customer {
name
}
}
}
`
}).subscribe({
next: resolve,
error: reject
});
});
let execGraphQL;
// MUTATE
await execGraphQL(
`mutation {
Customer {
CustomerCreate (input:{data:{name:"Atif 21", age:50}}) {
obj {
id
name
}
}
}
}`
);
// ASSERT SUBSCRIPTION RECEIVED EVENT
expect(await subscriptionPromise).to.deep.equal({});
});
});
Issue Here
When test in run, payload in the resolve function contains global data, where as it should contain the subscription payload. So the code breaks.