Remove nested operations from GraphQL API (#5931)

* Remove nested operations

* Improve error log

* Fix bug schema to load

* Fix ParseGraphQLSchema tests

* Fix tests

* Fix failing tests

* Rename call to callCloudCode
This commit is contained in:
Antonio Davi Macedo Coelho de Castro
2019-08-17 11:02:19 -07:00
committed by Antoine Cormouls
parent 47d1a74ac0
commit ee5aeeaff5
14 changed files with 1157 additions and 1619 deletions

View File

@@ -25,18 +25,21 @@ const RESERVED_GRAPHQL_TYPE_NAMES = [
'Query',
'Mutation',
'Subscription',
'ObjectsQuery',
'UsersQuery',
'ObjectsMutation',
'FilesMutation',
'UsersMutation',
'FunctionsMutation',
'Viewer',
'SignUpFieldsInput',
'LogInFieldsInput',
];
const RESERVED_GRAPHQL_OBJECT_QUERY_NAMES = ['get', 'find'];
const RESERVED_GRAPHQL_OBJECT_MUTATION_NAMES = ['create', 'update', 'delete'];
const RESERVED_GRAPHQL_QUERY_NAMES = ['health', 'viewer', 'get', 'find'];
const RESERVED_GRAPHQL_MUTATION_NAMES = [
'signUp',
'logIn',
'logOut',
'createFile',
'callCloudCode',
'create',
'update',
'delete',
];
class ParseGraphQLSchema {
databaseController: DatabaseController;
@@ -87,9 +90,7 @@ class ParseGraphQLSchema {
this.graphQLAutoSchema = null;
this.graphQLSchema = null;
this.graphQLTypes = [];
this.graphQLObjectsQueries = {};
this.graphQLQueries = {};
this.graphQLObjectsMutations = {};
this.graphQLMutations = {};
this.graphQLSubscriptions = {};
this.graphQLSchemaDirectivesDefinitions = null;
@@ -104,6 +105,7 @@ class ParseGraphQLSchema {
parseClassMutations.load(this, parseClass, parseClassConfig);
}
);
defaultGraphQLTypes.loadArrayResult(this, parseClasses);
defaultGraphQLQueries.load(this);
defaultGraphQLMutations.load(this);
@@ -211,29 +213,28 @@ class ParseGraphQLSchema {
return type;
}
addGraphQLObjectQuery(
addGraphQLQuery(
fieldName,
field,
throwError = false,
ignoreReserved = false
) {
if (
(!ignoreReserved &&
RESERVED_GRAPHQL_OBJECT_QUERY_NAMES.includes(fieldName)) ||
this.graphQLObjectsQueries[fieldName]
(!ignoreReserved && RESERVED_GRAPHQL_QUERY_NAMES.includes(fieldName)) ||
this.graphQLQueries[fieldName]
) {
const message = `Object query ${fieldName} could not be added to the auto schema because it collided with an existing field.`;
const message = `Query ${fieldName} could not be added to the auto schema because it collided with an existing field.`;
if (throwError) {
throw new Error(message);
}
this.log.warn(message);
return undefined;
}
this.graphQLObjectsQueries[fieldName] = field;
this.graphQLQueries[fieldName] = field;
return field;
}
addGraphQLObjectMutation(
addGraphQLMutation(
fieldName,
field,
throwError = false,
@@ -241,17 +242,17 @@ class ParseGraphQLSchema {
) {
if (
(!ignoreReserved &&
RESERVED_GRAPHQL_OBJECT_MUTATION_NAMES.includes(fieldName)) ||
this.graphQLObjectsMutations[fieldName]
RESERVED_GRAPHQL_MUTATION_NAMES.includes(fieldName)) ||
this.graphQLMutations[fieldName]
) {
const message = `Object mutation ${fieldName} could not be added to the auto schema because it collided with an existing field.`;
const message = `Mutation ${fieldName} could not be added to the auto schema because it collided with an existing field.`;
if (throwError) {
throw new Error(message);
}
this.log.warn(message);
return undefined;
}
this.graphQLObjectsMutations[fieldName] = field;
this.graphQLMutations[fieldName] = field;
return field;
}

View File

@@ -47,7 +47,9 @@ class ParseGraphQLServer {
},
};
} catch (e) {
this.log.error(e);
this.log.error(
e.stack || (typeof e.toString === 'function' && e.toString()) || e
);
throw e;
}
}

View File

@@ -3,12 +3,17 @@ import * as objectsQueries from './objectsQueries';
import * as usersQueries from './usersQueries';
const load = parseGraphQLSchema => {
parseGraphQLSchema.graphQLQueries.health = {
description:
'The health query can be used to check if the server is up and running.',
type: new GraphQLNonNull(GraphQLBoolean),
resolve: () => true,
};
parseGraphQLSchema.addGraphQLQuery(
'health',
{
description:
'The health query can be used to check if the server is up and running.',
type: new GraphQLNonNull(GraphQLBoolean),
resolve: () => true,
},
true,
true
);
objectsQueries.load(parseGraphQLSchema);
usersQueries.load(parseGraphQLSchema);

View File

@@ -1,93 +1,83 @@
import { GraphQLObjectType, GraphQLNonNull } from 'graphql';
import { GraphQLNonNull } from 'graphql';
import { GraphQLUpload } from 'graphql-upload';
import Parse from 'parse/node';
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
import logger from '../../logger';
const load = parseGraphQLSchema => {
const fields = {};
parseGraphQLSchema.addGraphQLMutation(
'createFile',
{
description:
'The create mutation can be used to create and upload a new file.',
args: {
upload: {
description: 'This is the new file to be created and uploaded',
type: new GraphQLNonNull(GraphQLUpload),
},
},
type: new GraphQLNonNull(defaultGraphQLTypes.FILE_INFO),
async resolve(_source, args, context) {
try {
const { upload } = args;
const { config } = context;
fields.create = {
description:
'The create mutation can be used to create and upload a new file.',
args: {
upload: {
description: 'This is the new file to be created and uploaded',
type: new GraphQLNonNull(GraphQLUpload),
const { createReadStream, filename, mimetype } = await upload;
let data = null;
if (createReadStream) {
const stream = createReadStream();
data = await new Promise((resolve, reject) => {
let data = '';
stream
.on('error', reject)
.on('data', chunk => (data += chunk))
.on('end', () => resolve(data));
});
}
if (!data || !data.length) {
throw new Parse.Error(
Parse.Error.FILE_SAVE_ERROR,
'Invalid file upload.'
);
}
if (filename.length > 128) {
throw new Parse.Error(
Parse.Error.INVALID_FILE_NAME,
'Filename too long.'
);
}
if (!filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) {
throw new Parse.Error(
Parse.Error.INVALID_FILE_NAME,
'Filename contains invalid characters.'
);
}
try {
return await config.filesController.createFile(
config,
filename,
data,
mimetype
);
} catch (e) {
logger.error('Error creating a file: ', e);
throw new Parse.Error(
Parse.Error.FILE_SAVE_ERROR,
`Could not store file: ${filename}.`
);
}
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
},
type: new GraphQLNonNull(defaultGraphQLTypes.FILE_INFO),
async resolve(_source, args, context) {
try {
const { upload } = args;
const { config } = context;
const { createReadStream, filename, mimetype } = await upload;
let data = null;
if (createReadStream) {
const stream = createReadStream();
data = await new Promise((resolve, reject) => {
let data = '';
stream
.on('error', reject)
.on('data', chunk => (data += chunk))
.on('end', () => resolve(data));
});
}
if (!data || !data.length) {
throw new Parse.Error(
Parse.Error.FILE_SAVE_ERROR,
'Invalid file upload.'
);
}
if (filename.length > 128) {
throw new Parse.Error(
Parse.Error.INVALID_FILE_NAME,
'Filename too long.'
);
}
if (!filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) {
throw new Parse.Error(
Parse.Error.INVALID_FILE_NAME,
'Filename contains invalid characters.'
);
}
try {
return await config.filesController.createFile(
config,
filename,
data,
mimetype
);
} catch (e) {
logger.error('Error creating a file: ', e);
throw new Parse.Error(
Parse.Error.FILE_SAVE_ERROR,
`Could not store file: ${filename}.`
);
}
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
};
const filesMutation = new GraphQLObjectType({
name: 'FilesMutation',
description: 'FilesMutation is the top level type for files mutations.',
fields,
});
parseGraphQLSchema.addGraphQLType(filesMutation, true, true);
parseGraphQLSchema.graphQLMutations.files = {
description: 'This is the top level for files mutations.',
type: filesMutation,
resolve: () => new Object(),
};
true,
true
);
};
export { load };

View File

@@ -1,57 +1,46 @@
import { GraphQLObjectType, GraphQLNonNull, GraphQLString } from 'graphql';
import { GraphQLNonNull, GraphQLString } from 'graphql';
import { FunctionsRouter } from '../../Routers/FunctionsRouter';
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
const load = parseGraphQLSchema => {
const fields = {};
fields.call = {
description:
'The call mutation can be used to invoke a cloud code function.',
args: {
functionName: {
description: 'This is the name of the function to be called.',
type: new GraphQLNonNull(GraphQLString),
parseGraphQLSchema.addGraphQLMutation(
'callCloudCode',
{
description:
'The call mutation can be used to invoke a cloud code function.',
args: {
functionName: {
description: 'This is the name of the function to be called.',
type: new GraphQLNonNull(GraphQLString),
},
params: {
description: 'These are the params to be passed to the function.',
type: defaultGraphQLTypes.OBJECT,
},
},
params: {
description: 'These are the params to be passed to the function.',
type: defaultGraphQLTypes.OBJECT,
type: defaultGraphQLTypes.ANY,
async resolve(_source, args, context) {
try {
const { functionName, params } = args;
const { config, auth, info } = context;
return (await FunctionsRouter.handleCloudFunction({
params: {
functionName,
},
config,
auth,
info,
body: params,
})).response.result;
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
},
type: defaultGraphQLTypes.ANY,
async resolve(_source, args, context) {
try {
const { functionName, params } = args;
const { config, auth, info } = context;
return (await FunctionsRouter.handleCloudFunction({
params: {
functionName,
},
config,
auth,
info,
body: params,
})).response.result;
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
};
const functionsMutation = new GraphQLObjectType({
name: 'FunctionsMutation',
description:
'FunctionsMutation is the top level type for functions mutations.',
fields,
});
parseGraphQLSchema.addGraphQLType(functionsMutation, true, true);
parseGraphQLSchema.graphQLMutations.functions = {
description: 'This is the top level for functions mutations.',
type: functionsMutation,
resolve: () => new Object(),
};
true,
true
);
};
export { load };

View File

@@ -1,4 +1,4 @@
import { GraphQLNonNull, GraphQLBoolean, GraphQLObjectType } from 'graphql';
import { GraphQLNonNull, GraphQLBoolean } from 'graphql';
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
import rest from '../../rest';
import { transformMutationInputToParse } from '../transformers/mutation';
@@ -44,7 +44,7 @@ const deleteObject = async (className, objectId, config, auth, info) => {
};
const load = parseGraphQLSchema => {
parseGraphQLSchema.addGraphQLObjectMutation(
parseGraphQLSchema.addGraphQLMutation(
'create',
{
description:
@@ -69,7 +69,7 @@ const load = parseGraphQLSchema => {
true
);
parseGraphQLSchema.addGraphQLObjectMutation(
parseGraphQLSchema.addGraphQLMutation(
'update',
{
description:
@@ -102,7 +102,7 @@ const load = parseGraphQLSchema => {
true
);
parseGraphQLSchema.addGraphQLObjectMutation(
parseGraphQLSchema.addGraphQLMutation(
'delete',
{
description:
@@ -126,19 +126,6 @@ const load = parseGraphQLSchema => {
true,
true
);
const objectsMutation = new GraphQLObjectType({
name: 'ObjectsMutation',
description: 'ObjectsMutation is the top level type for objects mutations.',
fields: parseGraphQLSchema.graphQLObjectsMutations,
});
parseGraphQLSchema.addGraphQLType(objectsMutation, true, true);
parseGraphQLSchema.graphQLMutations.objects = {
description: 'This is the top level for objects mutations.',
type: objectsMutation,
resolve: () => new Object(),
};
};
export { createObject, updateObject, deleteObject, load };

View File

@@ -1,9 +1,4 @@
import {
GraphQLNonNull,
GraphQLBoolean,
GraphQLString,
GraphQLObjectType,
} from 'graphql';
import { GraphQLNonNull, GraphQLBoolean, GraphQLString } from 'graphql';
import getFieldNames from 'graphql-list-fields';
import Parse from 'parse/node';
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
@@ -134,7 +129,7 @@ const findObjects = async (
};
const load = parseGraphQLSchema => {
parseGraphQLSchema.addGraphQLObjectQuery(
parseGraphQLSchema.addGraphQLQuery(
'get',
{
description:
@@ -181,7 +176,7 @@ const load = parseGraphQLSchema => {
true
);
parseGraphQLSchema.addGraphQLObjectQuery(
parseGraphQLSchema.addGraphQLQuery(
'find',
{
description:
@@ -252,19 +247,6 @@ const load = parseGraphQLSchema => {
true,
true
);
const objectsQuery = new GraphQLObjectType({
name: 'ObjectsQuery',
description: 'ObjectsQuery is the top level type for objects queries.',
fields: parseGraphQLSchema.graphQLObjectsQueries,
});
parseGraphQLSchema.addGraphQLType(objectsQuery, true, true);
parseGraphQLSchema.graphQLQueries.objects = {
description: 'This is the top level for objects queries.',
type: objectsQuery,
resolve: () => new Object(),
};
};
export { getObject, findObjects, load };

View File

@@ -94,7 +94,7 @@ const load = function(
if (isCreateEnabled) {
const createGraphQLMutationName = `create${graphQLClassName}`;
parseGraphQLSchema.addGraphQLObjectMutation(createGraphQLMutationName, {
parseGraphQLSchema.addGraphQLMutation(createGraphQLMutationName, {
description: `The ${createGraphQLMutationName} mutation can be used to create a new object of the ${graphQLClassName} class.`,
args: {
fields: {
@@ -155,7 +155,7 @@ const load = function(
if (isUpdateEnabled) {
const updateGraphQLMutationName = `update${graphQLClassName}`;
parseGraphQLSchema.addGraphQLObjectMutation(updateGraphQLMutationName, {
parseGraphQLSchema.addGraphQLMutation(updateGraphQLMutationName, {
description: `The ${updateGraphQLMutationName} mutation can be used to update an object of the ${graphQLClassName} class.`,
args: {
objectId: defaultGraphQLTypes.OBJECT_ID_ATT,
@@ -215,7 +215,7 @@ const load = function(
if (isDestroyEnabled) {
const deleteGraphQLMutationName = `delete${graphQLClassName}`;
parseGraphQLSchema.addGraphQLObjectMutation(deleteGraphQLMutationName, {
parseGraphQLSchema.addGraphQLMutation(deleteGraphQLMutationName, {
description: `The ${deleteGraphQLMutationName} mutation can be used to delete an object of the ${graphQLClassName} class.`,
args: {
objectId: defaultGraphQLTypes.OBJECT_ID_ATT,

View File

@@ -54,7 +54,7 @@ const load = function(
if (isGetEnabled) {
const getGraphQLQueryName =
graphQLClassName.charAt(0).toLowerCase() + graphQLClassName.slice(1);
parseGraphQLSchema.addGraphQLObjectQuery(getGraphQLQueryName, {
parseGraphQLSchema.addGraphQLQuery(getGraphQLQueryName, {
description: `The ${getGraphQLQueryName} query can be used to get an object of the ${graphQLClassName} class by its id.`,
args: {
objectId: defaultGraphQLTypes.OBJECT_ID_ATT,
@@ -78,7 +78,7 @@ const load = function(
const findGraphQLQueryName = pluralize(
graphQLClassName.charAt(0).toLowerCase() + graphQLClassName.slice(1)
);
parseGraphQLSchema.addGraphQLObjectQuery(findGraphQLQueryName, {
parseGraphQLSchema.addGraphQLQuery(findGraphQLQueryName, {
description: `The ${findGraphQLQueryName} query can be used to find objects of the ${graphQLClassName} class.`,
args: classGraphQLFindArgs,
type: new GraphQLNonNull(

View File

@@ -3,7 +3,6 @@ import { SchemaDirectiveVisitor } from 'graphql-tools';
import { FunctionsRouter } from '../../Routers/FunctionsRouter';
export const definitions = gql`
directive @namespace on FIELD_DEFINITION
directive @resolve(to: String) on FIELD_DEFINITION
directive @mock(with: Any!) on FIELD_DEFINITION
`;
@@ -11,14 +10,6 @@ export const definitions = gql`
const load = parseGraphQLSchema => {
parseGraphQLSchema.graphQLSchemaDirectivesDefinitions = definitions;
class NamespaceDirectiveVisitor extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
field.resolve = () => ({});
}
}
parseGraphQLSchema.graphQLSchemaDirectives.namespace = NamespaceDirectiveVisitor;
class ResolveDirectiveVisitor extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
field.resolve = async (_source, args, context) => {

View File

@@ -1,4 +1,4 @@
import { GraphQLNonNull, GraphQLObjectType } from 'graphql';
import { GraphQLNonNull } from 'graphql';
import UsersRouter from '../../Routers/UsersRouter';
import * as objectsMutations from './objectsMutations';
import { getUserFromSessionToken } from './usersQueries';
@@ -9,110 +9,111 @@ const load = parseGraphQLSchema => {
if (parseGraphQLSchema.isUsersClassDisabled) {
return;
}
const fields = {};
fields.signUp = {
description: 'The signUp mutation can be used to sign the user up.',
args: {
fields: {
descriptions: 'These are the fields of the user.',
type: parseGraphQLSchema.parseClassTypes['_User'].signUpInputType,
parseGraphQLSchema.addGraphQLMutation(
'signUp',
{
description: 'The signUp mutation can be used to sign the user up.',
args: {
fields: {
descriptions: 'These are the fields of the user.',
type: parseGraphQLSchema.parseClassTypes['_User'].signUpInputType,
},
},
type: new GraphQLNonNull(parseGraphQLSchema.viewerType),
async resolve(_source, args, context, mutationInfo) {
try {
const { fields } = args;
const { config, auth, info } = context;
const { sessionToken } = await objectsMutations.createObject(
'_User',
fields,
config,
auth,
info
);
info.sessionToken = sessionToken;
return await getUserFromSessionToken(config, info, mutationInfo);
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
},
type: new GraphQLNonNull(parseGraphQLSchema.viewerType),
async resolve(_source, args, context, mutationInfo) {
try {
const { fields } = args;
true,
true
);
const { config, auth, info } = context;
parseGraphQLSchema.addGraphQLMutation(
'logIn',
{
description: 'The logIn mutation can be used to log the user in.',
args: {
fields: {
description: 'This is data needed to login',
type: parseGraphQLSchema.parseClassTypes['_User'].logInInputType,
},
},
type: new GraphQLNonNull(parseGraphQLSchema.viewerType),
async resolve(_source, args, context) {
try {
const {
fields: { username, password },
} = args;
const { config, auth, info } = context;
const { sessionToken } = await objectsMutations.createObject(
'_User',
fields,
config,
auth,
info
);
info.sessionToken = sessionToken;
return await getUserFromSessionToken(config, info, mutationInfo);
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
};
fields.logIn = {
description: 'The logIn mutation can be used to log the user in.',
args: {
fields: {
description: 'This is data needed to login',
type: parseGraphQLSchema.parseClassTypes['_User'].logInInputType,
return (await usersRouter.handleLogIn({
body: {
username,
password,
},
query: {},
config,
auth,
info,
})).response;
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
},
type: new GraphQLNonNull(parseGraphQLSchema.viewerType),
async resolve(_source, args, context) {
try {
const {
fields: { username, password },
} = args;
const { config, auth, info } = context;
true,
true
);
return (await usersRouter.handleLogIn({
body: {
username,
password,
},
query: {},
config,
auth,
info,
})).response;
} catch (e) {
parseGraphQLSchema.handleError(e);
}
parseGraphQLSchema.addGraphQLMutation(
'logOut',
{
description: 'The logOut mutation can be used to log the user out.',
type: new GraphQLNonNull(parseGraphQLSchema.viewerType),
async resolve(_source, _args, context, mutationInfo) {
try {
const { config, auth, info } = context;
const viewer = await getUserFromSessionToken(
config,
info,
mutationInfo
);
await usersRouter.handleLogOut({
config,
auth,
info,
});
return viewer;
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
},
};
fields.logOut = {
description: 'The logOut mutation can be used to log the user out.',
type: new GraphQLNonNull(parseGraphQLSchema.viewerType),
async resolve(_source, _args, context, mutationInfo) {
try {
const { config, auth, info } = context;
const viewer = await getUserFromSessionToken(
config,
info,
mutationInfo
);
await usersRouter.handleLogOut({
config,
auth,
info,
});
return viewer;
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
};
const usersMutation = new GraphQLObjectType({
name: 'UsersMutation',
description: 'UsersMutation is the top level type for files mutations.',
fields,
});
parseGraphQLSchema.addGraphQLType(usersMutation, true, true);
parseGraphQLSchema.graphQLMutations.users = {
description: 'This is the top level for users mutations.',
type: usersMutation,
resolve: () => new Object(),
};
true,
true
);
};
export { load };

View File

@@ -1,4 +1,4 @@
import { GraphQLNonNull, GraphQLObjectType } from 'graphql';
import { GraphQLNonNull } from 'graphql';
import getFieldNames from 'graphql-list-fields';
import Parse from 'parse/node';
import rest from '../../rest';
@@ -49,34 +49,25 @@ const load = parseGraphQLSchema => {
if (parseGraphQLSchema.isUsersClassDisabled) {
return;
}
const fields = {};
fields.viewer = {
description:
'The viewer query can be used to return the current user data.',
type: new GraphQLNonNull(parseGraphQLSchema.viewerType),
async resolve(_source, _args, context, queryInfo) {
try {
const { config, info } = context;
return await getUserFromSessionToken(config, info, queryInfo);
} catch (e) {
parseGraphQLSchema.handleError(e);
}
parseGraphQLSchema.addGraphQLQuery(
'viewer',
{
description:
'The viewer query can be used to return the current user data.',
type: new GraphQLNonNull(parseGraphQLSchema.viewerType),
async resolve(_source, _args, context, queryInfo) {
try {
const { config, info } = context;
return await getUserFromSessionToken(config, info, queryInfo);
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
},
};
const usersQuery = new GraphQLObjectType({
name: 'UsersQuery',
description: 'UsersQuery is the top level type for users queries.',
fields,
});
parseGraphQLSchema.addGraphQLType(usersQuery, true, true);
parseGraphQLSchema.graphQLQueries.users = {
description: 'This is the top level for users queries.',
type: usersQuery,
resolve: () => new Object(),
};
true,
true
);
};
export { load, getUserFromSessionToken };