Merge pull request #4040 from strapi/fix/graphql-plugin-resolverof

Fix graphql plugin resolverof
This commit is contained in:
Alexandre BODIN 2019-09-18 13:58:59 +02:00 committed by GitHub
commit cc691c0a78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 529 additions and 135 deletions

View File

@ -17,6 +17,10 @@
"restaurant": {
"model": "restaurant",
"via": "menu"
},
"menusections": {
"collection": "menusection",
"via": "menu"
}
}
}

View File

@ -25,7 +25,8 @@
"repeatable": true
},
"menu": {
"model": "menu"
"model": "menu",
"via": "menusections"
}
}
}

View File

@ -174,7 +174,8 @@ module.exports = {
)
);
return async (obj, options, { context }) => {
return async (obj, options, graphqlCtx) => {
const { context } = graphqlCtx;
// Hack to be able to handle permissions for each query.
const ctx = Object.assign(_.clone(context), {
request: Object.assign(_.clone(context.request), {
@ -234,7 +235,7 @@ module.exports = {
: body;
}
return resolver.call(null, obj, options, context);
return resolver.call(null, obj, options, graphqlCtx);
}
// Resolver can be a promise.

View File

@ -202,56 +202,63 @@ const schemaBuilder = {
// Transform object to only contain function.
Object.keys(resolvers).reduce((acc, type) => {
return Object.keys(acc[type]).reduce((acc, resolver) => {
return Object.keys(acc[type]).reduce((acc, resolverName) => {
const resolverObj = acc[type][resolverName];
// Disabled this query.
if (acc[type][resolver] === false) {
delete acc[type][resolver];
if (resolverObj === false) {
delete acc[type][resolverName];
return acc;
}
if (!_.isFunction(acc[type][resolver])) {
acc[type][resolver] = acc[type][resolver].resolver;
if (_.isFunction(resolverObj)) {
return acc;
}
if (
_.isString(acc[type][resolver]) ||
_.isPlainObject(acc[type][resolver])
) {
const { plugin = '' } = _.isPlainObject(acc[type][resolver])
? acc[type][resolver]
: {};
let plugin;
if (_.has(resolverObj, ['plugin'])) {
plugin = resolverObj.plugin;
} else if (_.has(resolverObj, ['resolver', 'plugin'])) {
plugin = resolverObj.resolver.plugin;
}
switch (type) {
case 'Mutation': {
let name, action;
if (_.isString(acc[type][resolver])) {
[name, action] = acc[type][resolver].split('.');
} else if (
_.isPlainObject(acc[type][resolver]) &&
_.isString(acc[type][resolver].handler)
) {
[name, action] = acc[type][resolver].handler.split('.');
}
acc[type][resolver] = Mutation.composeMutationResolver({
_schema: strapi.plugins.graphql.config._schema.graphql,
plugin,
name: _.toLower(name),
action,
});
break;
switch (type) {
case 'Mutation': {
let name, action;
if (
_.has(resolverObj, ['resolver']) &&
_.isString(resolverObj.resolver)
) {
[name, action] = resolverObj.resolver.split('.');
} else if (
_.has(resolverObj, ['resolver', 'handler']) &&
_.isString(resolverObj.handler)
) {
[name, action] = resolverObj.resolver.handler.split('.');
} else {
name = null;
action = resolverName;
}
case 'Query':
default:
acc[type][resolver] = Query.composeQueryResolver({
_schema: strapi.plugins.graphql.config._schema.graphql,
plugin,
name: resolver,
isSingular: 'force', // Avoid singular/pluralize and force query name.
});
break;
const mutationResolver = Mutation.composeMutationResolver({
_schema: strapi.plugins.graphql.config._schema.graphql,
plugin,
name: _.toLower(name),
action,
});
acc[type][resolverName] = mutationResolver;
break;
}
case 'Query':
default:
acc[type][resolverName] = Query.composeQueryResolver({
_schema: strapi.plugins.graphql.config._schema.graphql,
plugin,
name: resolverName,
isSingular: 'force', // Avoid singular/pluralize and force query name.
});
break;
}
return acc;

View File

@ -46,5 +46,6 @@
"npm": ">=6.0.0"
},
"license": "MIT",
"gitHead": "c85658a19b8fef0f3164c19693a45db305dc07a9"
"gitHead": "c85658a19b8fef0f3164c19693a45db305dc07a9",
"devDependencies": {}
}

View File

@ -0,0 +1,131 @@
'use strict';
const fs = require('fs');
const { registerAndLogin } = require('../../../test/helpers/auth');
const { createAuthRequest } = require('../../../test/helpers/request');
let rq;
const defaultProviderConfig = {
provider: 'local',
name: 'Local server',
enabled: true,
sizeLimit: 1000000,
};
const resetProviderConfigToDefault = () => {
return setConfigOptions(defaultProviderConfig);
};
const setConfigOptions = assign => {
return rq.put('/upload/settings/development', {
body: {
...defaultProviderConfig,
...assign,
},
});
};
describe('Upload plugin end to end tests', () => {
beforeAll(async () => {
const token = await registerAndLogin();
rq = createAuthRequest(token);
}, 60000);
afterEach(async () => {
await resetProviderConfigToDefault();
});
test('Upload a single file', async () => {
const req = rq.post('/graphql');
const form = req.form();
form.append(
'operations',
JSON.stringify({
query: /* GraphQL */ `
mutation uploadFiles($file: Upload!) {
upload(file: $file) {
id
name
mime
url
}
}
`,
variables: {
file: null,
},
})
);
form.append(
'map',
JSON.stringify({
0: ['variables.file'],
})
);
form.append('0', fs.createReadStream(__dirname + '/rec.jpg'));
const res = await req;
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
data: {
upload: {
id: expect.anything(),
name: 'rec.jpg',
},
},
});
});
test('Upload multiple files', async () => {
const req = rq.post('/graphql');
const form = req.form();
form.append(
'operations',
JSON.stringify({
query: /* GraphQL */ `
mutation uploadFiles($files: [Upload]!) {
multipleUpload(files: $files) {
id
name
mime
url
}
}
`,
variables: {
files: [null, null],
},
})
);
form.append(
'map',
JSON.stringify({
0: ['variables.files.0'],
1: ['variables.files.1'],
})
);
form.append('0', fs.createReadStream(__dirname + '/rec.jpg'));
form.append('1', fs.createReadStream(__dirname + '/rec.jpg'));
const res = await req;
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({
data: {
multipleUpload: expect.arrayContaining([
expect.objectContaining({
id: expect.anything(),
name: 'rec.jpg',
}),
]),
},
});
});
});

View File

@ -4,19 +4,8 @@ const fs = require('fs');
// Helpers.
const { registerAndLogin } = require('../../../test/helpers/auth');
// const createModelsUtils = require('../../../test/helpers/models');
// const form = require('../../../test/helpers/generators');
const { createAuthRequest } = require('../../../test/helpers/request');
// const cleanDate = entry => {
// delete entry.updatedAt;
// delete entry.createdAt;
// delete entry.created_at;
// delete entry.updated_at;
// };
// let data;
// let modelsUtils;
let rq;
const defaultProviderConfig = {
@ -43,28 +32,6 @@ describe('Upload plugin end to end tests', () => {
beforeAll(async () => {
const token = await registerAndLogin();
rq = createAuthRequest(token);
// modelsUtils = createModelsUtils({ rq });
// await modelsUtils.createModels([
// form.article,
// form.tag,
// form.category,
// form.reference,
// form.product,
// form.articlewithtag,
// ]);
}, 60000);
afterAll(() => {
// modelsUtils.deleteModels([
// 'article',
// 'tag',
// 'category',
// 'reference',
// 'product',
// 'articlewithtag',
// ]),
}, 60000);
afterEach(async () => {

View File

@ -2,7 +2,7 @@ const _ = require('lodash');
module.exports = {
type: {
UsersPermissionsPermission: false // Make this type NOT queriable.
UsersPermissionsPermission: false, // Make this type NOT queriable.
},
definition: `
type UsersPermissionsMe {
@ -30,105 +30,136 @@ module.exports = {
resolverOf: 'User.me',
resolver: {
plugin: 'users-permissions',
handler: 'User.me'
}
handler: 'User.me',
},
},
role: {
plugin: 'users-permissions',
resolverOf: 'UsersPermissions.getRole',
resolver: async (obj, options, { context }) => {
await strapi.plugins['users-permissions'].controllers.userspermissions.getRole(context);
resolver: async (obj, options, { context }) => {
await strapi.plugins[
'users-permissions'
].controllers.userspermissions.getRole(context);
return context.body.role;
}
},
},
roles: {
description: `Retrieve all the existing roles. You can't apply filters on this query.`,
plugin: 'users-permissions',
resolverOf: 'UsersPermissions.getRoles', // Apply the `getRoles` permissions on the resolver.
resolver: async (obj, options, { context }) => {
await strapi.plugins['users-permissions'].controllers.userspermissions.getRoles(context);
resolver: async (obj, options, { context }) => {
await strapi.plugins[
'users-permissions'
].controllers.userspermissions.getRoles(context);
return context.body.roles;
}
}
},
},
},
Mutation: {
createRole: {
description: 'Create a new role',
plugin: 'users-permissions',
resolverOf: 'UsersPermissions.createRole',
resolver: async (obj, options, { context }) => {
await strapi.plugins['users-permissions'].controllers.userspermissions.createRole(context);
resolver: async (obj, options, { context }) => {
await strapi.plugins[
'users-permissions'
].controllers.userspermissions.createRole(context);
return { ok: true };
}
},
},
updateRole: {
description: 'Update an existing role',
plugin: 'users-permissions',
resolverOf: 'UsersPermissions.updateRole',
resolver: async (obj, options, { context }) => {
await strapi.plugins['users-permissions'].controllers.userspermissions.updateRole(context.params, context.body);
resolver: async (obj, options, { context }) => {
await strapi.plugins[
'users-permissions'
].controllers.userspermissions.updateRole(
context.params,
context.body
);
return { ok: true };
}
},
},
deleteRole: {
description: 'Delete an existing role',
plugin: 'users-permissions',
resolverOf: 'UsersPermissions.deleteRole',
resolver: async (obj, options, { context }) => {
await strapi.plugins['users-permissions'].controllers.userspermissions.deleteRole(context);
resolver: async (obj, options, { context }) => {
await strapi.plugins[
'users-permissions'
].controllers.userspermissions.deleteRole(context);
return { ok: true };
}
},
},
createUser: {
description: 'Create a new user',
plugin: 'users-permissions',
resolverOf: 'User.create',
resolver: async (obj, options, { context }) => {
context.params = _.toPlainObject(options.input.where);
context.params = _.toPlainObject(options.input.where);
context.request.body = _.toPlainObject(options.input.data);
await strapi.plugins['users-permissions'].controllers.user.create(context);
await strapi.plugins['users-permissions'].controllers.user.create(
context
);
return {
user: context.body.toJSON ? context.body.toJSON() : context.body
user: context.body.toJSON ? context.body.toJSON() : context.body,
};
}
},
},
updateUser: {
description: 'Update an existing user',
plugin: 'users-permissions',
resolverOf: 'User.update',
resolver: async (obj, options, { context }) => {
context.params = _.toPlainObject(options.input.where);
context.params = _.toPlainObject(options.input.where);
context.request.body = _.toPlainObject(options.input.data);
await strapi.plugins['users-permissions'].controllers.user.update(context);
await strapi.plugins['users-permissions'].controllers.user.update(
context
);
return {
user: context.body.toJSON ? context.body.toJSON() : context.body
return {
user: context.body.toJSON ? context.body.toJSON() : context.body,
};
}
},
},
deleteUser: {
description: 'Delete an existing user',
plugin: 'users-permissions',
resolverOf: 'User.destroy',
resolver: async (obj, options, { context }) => {
// Set parameters to context.
context.params = _.toPlainObject(options.input.where);
context.params = _.toPlainObject(options.input.where);
context.request.body = _.toPlainObject(options.input.data);
// Retrieve user to be able to return it because
// Retrieve user to be able to return it because
// Bookshelf doesn't return the row once deleted.
await strapi.plugins['users-permissions'].controllers.user.findOne(context);
await strapi.plugins['users-permissions'].controllers.user.findOne(
context
);
// Assign result to user.
const user = context.body.toJSON ? context.body.toJSON() : context.body;
const user = context.body.toJSON
? context.body.toJSON()
: context.body;
// Run destroy query.
await strapi.plugins['users-permissions'].controllers.user.destroy(context);
await strapi.plugins['users-permissions'].controllers.user.destroy(
context
);
return {
user
user,
};
}
}
}
}
},
},
},
},
};

View File

@ -170,27 +170,37 @@ module.exports = {
const { id } = ctx.params;
const { email, username, password } = ctx.request.body;
if (!email) return ctx.badRequest('missing.email');
if (!username) return ctx.badRequest('missing.username');
if (!password) return ctx.badRequest('missing.password');
const userWithSameUsername = await strapi
.query('user', 'users-permissions')
.findOne({ username });
if (userWithSameUsername && userWithSameUsername.id != id) {
return ctx.badRequest(
null,
ctx.request.admin
? adminError({
message: 'Auth.form.error.username.taken',
field: ['username'],
})
: 'username.alreadyTaken.'
);
if (_.has(ctx.request.body, 'email') && !email) {
return ctx.badRequest('email.notNull');
}
if (advancedConfigs.unique_email) {
if (_.has(ctx.request.body, 'username') && !username) {
return ctx.badRequest('username.notNull');
}
if (_.has(ctx.request.body, 'password') && !password) {
return ctx.badRequest('password.notNull');
}
if (_.has(ctx.request.body, 'username')) {
const userWithSameUsername = await strapi
.query('user', 'users-permissions')
.findOne({ username });
if (userWithSameUsername && userWithSameUsername.id != id) {
return ctx.badRequest(
null,
ctx.request.admin
? adminError({
message: 'Auth.form.error.username.taken',
field: ['username'],
})
: 'username.alreadyTaken.'
);
}
}
if (_.has(ctx.request.body, 'email') && advancedConfigs.unique_email) {
const userWithSameEmail = await strapi
.query('user', 'users-permissions')
.findOne({ email });
@ -216,7 +226,7 @@ module.exports = {
...ctx.request.body,
};
if (password === user.password) {
if (_.has(ctx.request.body, 'password') && password === user.password) {
delete updateData.password;
}

View File

@ -0,0 +1,241 @@
// Helpers.
const { registerAndLogin } = require('../../../test/helpers/auth');
const {
createAuthRequest,
createRequest,
} = require('../../../test/helpers/request');
let authReq;
const data = {};
describe('Test Graphql user service', () => {
beforeAll(async () => {
const token = await registerAndLogin();
authReq = createAuthRequest(token);
}, 60000);
describe('Check createUser authorizations', () => {
test('createUser is forbidden to public', async () => {
const rq = createRequest();
const res = await rq({
url: '/graphql',
method: 'POST',
body: {
query: /* GraphQL */ `
mutation {
createUser(
input: {
data: { username: "test", email: "test", password: "test" }
}
) {
user {
id
username
}
}
}
`,
},
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
data: {
createUser: null,
},
errors: [
{
message: 'Forbidden',
},
],
});
});
test('createUser is authorized for admins', async () => {
const res = await authReq({
url: '/graphql',
method: 'POST',
body: {
query: /* GraphQL */ `
mutation {
createUser(
input: {
data: {
username: "test"
email: "test@strapi.io"
password: "test"
}
}
) {
user {
id
username
}
}
}
`,
},
});
expect(res.statusCode).toBe(201);
expect(res.body).toMatchObject({
data: {
createUser: {
user: {
id: expect.anything(),
username: 'test',
},
},
},
});
data.user = res.body.data.createUser.user;
});
});
describe('Check updateUser authorizations', () => {
test('updateUser is forbidden to public', async () => {
const rq = createRequest();
const res = await rq({
url: '/graphql',
method: 'POST',
body: {
query: /* GraphQL */ `
mutation {
updateUser(
input: {
where: { id: 1 }
data: { username: "test", email: "test", password: "test" }
}
) {
user {
id
username
}
}
}
`,
},
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
data: {
updateUser: null,
},
errors: [
{
message: 'Forbidden',
},
],
});
});
test('updateUser is authorized for admins', async () => {
const res = await authReq({
url: '/graphql',
method: 'POST',
body: {
query: /* GraphQL */ `
mutation updateUser($id: ID!) {
updateUser(
input: { where: { id: $id }, data: { username: "newUsername" } }
) {
user {
id
username
}
}
}
`,
variables: {
id: data.user.id,
},
},
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
data: {
updateUser: {
user: {
id: expect.anything(),
username: 'newUsername',
},
},
},
});
data.user = res.body.data.updateUser.user;
});
});
describe('Check deleteUser authorizations', () => {
test('deleteUser is forbidden to public', async () => {
const rq = createRequest();
const res = await rq({
url: '/graphql',
method: 'POST',
body: {
query: /* GraphQL */ `
mutation deleteUser($id: ID!) {
deleteUser(input: { where: { id: $id } }) {
user {
id
username
}
}
}
`,
variables: {
id: data.user.id,
},
},
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
data: {
deleteUser: null,
},
errors: [
{
message: 'Forbidden',
},
],
});
});
test('deleteUser is authorized for admins', async () => {
const res = await authReq({
url: '/graphql',
method: 'POST',
body: {
query: /* GraphQL */ `
mutation deleteUser($id: ID!) {
deleteUser(input: { where: { id: $id } }) {
user {
id
username
}
}
}
`,
variables: {
id: data.user.id,
},
},
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
data: {
deleteUser: {
user: data.user,
},
},
});
});
});
});