Improve callCloudCode mutation to receive a CloudCodeFunction enum instead of a String (#6029)

* Add listing test

* Improvements

* Fixinf package.json

* Fix package.json

* Fix tests
This commit is contained in:
Antonio Davi Macedo Coelho de Castro
2019-09-09 15:07:22 -07:00
committed by GitHub
parent 33d2b16476
commit a754b883b2
6 changed files with 245 additions and 54 deletions

View File

@@ -7,6 +7,7 @@ describe('ParseGraphQLSchema', () => {
let databaseController; let databaseController;
let parseGraphQLController; let parseGraphQLController;
let parseGraphQLSchema; let parseGraphQLSchema;
const appId = 'test';
beforeAll(async () => { beforeAll(async () => {
parseServer = await global.reconfigureServer({ parseServer = await global.reconfigureServer({
@@ -18,11 +19,12 @@ describe('ParseGraphQLSchema', () => {
databaseController, databaseController,
parseGraphQLController, parseGraphQLController,
log: defaultLogger, log: defaultLogger,
appId,
}); });
}); });
describe('constructor', () => { describe('constructor', () => {
it('should require a parseGraphQLController, databaseController and a log instance', () => { it('should require a parseGraphQLController, databaseController, a log instance, and the appId', () => {
expect(() => new ParseGraphQLSchema()).toThrow( expect(() => new ParseGraphQLSchema()).toThrow(
'You must provide a parseGraphQLController instance!' 'You must provide a parseGraphQLController instance!'
); );
@@ -36,6 +38,14 @@ describe('ParseGraphQLSchema', () => {
databaseController: {}, databaseController: {},
}) })
).toThrow('You must provide a log instance!'); ).toThrow('You must provide a log instance!');
expect(
() =>
new ParseGraphQLSchema({
parseGraphQLController: {},
databaseController: {},
log: {},
})
).toThrow('You must provide the appId!');
}); });
}); });
@@ -88,6 +98,7 @@ describe('ParseGraphQLSchema', () => {
databaseController, databaseController,
parseGraphQLController, parseGraphQLController,
log: defaultLogger, log: defaultLogger,
appId,
}); });
await parseGraphQLSchema.load(); await parseGraphQLSchema.load();
const parseClasses = parseGraphQLSchema.parseClasses; const parseClasses = parseGraphQLSchema.parseClasses;
@@ -134,6 +145,7 @@ describe('ParseGraphQLSchema', () => {
); );
}, },
}, },
appId,
}); });
await parseGraphQLSchema.load(); await parseGraphQLSchema.load();
const type = new GraphQLObjectType({ name: 'SomeClass' }); const type = new GraphQLObjectType({ name: 'SomeClass' });
@@ -156,6 +168,7 @@ describe('ParseGraphQLSchema', () => {
fail('Should not warn'); fail('Should not warn');
}, },
}, },
appId,
}); });
await parseGraphQLSchema.load(); await parseGraphQLSchema.load();
const type = new GraphQLObjectType({ name: 'SomeClass' }); const type = new GraphQLObjectType({ name: 'SomeClass' });
@@ -184,6 +197,7 @@ describe('ParseGraphQLSchema', () => {
); );
}, },
}, },
appId,
}); });
await parseGraphQLSchema.load(); await parseGraphQLSchema.load();
expect( expect(
@@ -203,6 +217,7 @@ describe('ParseGraphQLSchema', () => {
fail('Should not warn'); fail('Should not warn');
}, },
}, },
appId,
}); });
await parseGraphQLSchema.load(); await parseGraphQLSchema.load();
const type = new GraphQLObjectType({ name: 'String' }); const type = new GraphQLObjectType({ name: 'String' });
@@ -225,6 +240,7 @@ describe('ParseGraphQLSchema', () => {
); );
}, },
}, },
appId,
}); });
await parseGraphQLSchema.load(); await parseGraphQLSchema.load();
const field = {}; const field = {};
@@ -247,6 +263,7 @@ describe('ParseGraphQLSchema', () => {
fail('Should not warn'); fail('Should not warn');
}, },
}, },
appId,
}); });
await parseGraphQLSchema.load(); await parseGraphQLSchema.load();
const field = {}; const field = {};
@@ -274,6 +291,7 @@ describe('ParseGraphQLSchema', () => {
); );
}, },
}, },
appId,
}); });
await parseGraphQLSchema.load(); await parseGraphQLSchema.load();
expect(parseGraphQLSchema.addGraphQLQuery('viewer', {})).toBeUndefined(); expect(parseGraphQLSchema.addGraphQLQuery('viewer', {})).toBeUndefined();
@@ -289,6 +307,7 @@ describe('ParseGraphQLSchema', () => {
fail('Should not warn'); fail('Should not warn');
}, },
}, },
appId,
}); });
await parseGraphQLSchema.load(); await parseGraphQLSchema.load();
delete parseGraphQLSchema.graphQLQueries.viewer; delete parseGraphQLSchema.graphQLQueries.viewer;
@@ -314,6 +333,7 @@ describe('ParseGraphQLSchema', () => {
); );
}, },
}, },
appId,
}); });
await parseGraphQLSchema.load(); await parseGraphQLSchema.load();
const field = {}; const field = {};
@@ -338,6 +358,7 @@ describe('ParseGraphQLSchema', () => {
fail('Should not warn'); fail('Should not warn');
}, },
}, },
appId,
}); });
await parseGraphQLSchema.load(); await parseGraphQLSchema.load();
const field = {}; const field = {};
@@ -367,6 +388,7 @@ describe('ParseGraphQLSchema', () => {
); );
}, },
}, },
appId,
}); });
await parseGraphQLSchema.load(); await parseGraphQLSchema.load();
expect( expect(
@@ -384,6 +406,7 @@ describe('ParseGraphQLSchema', () => {
fail('Should not warn'); fail('Should not warn');
}, },
}, },
appId,
}); });
await parseGraphQLSchema.load(); await parseGraphQLSchema.load();
delete parseGraphQLSchema.graphQLMutations.signUp; delete parseGraphQLSchema.graphQLMutations.signUp;
@@ -405,6 +428,7 @@ describe('ParseGraphQLSchema', () => {
fail('Should not warn'); fail('Should not warn');
}, },
}, },
appId,
}); });
expect( expect(
parseGraphQLSchema parseGraphQLSchema
@@ -443,6 +467,7 @@ describe('ParseGraphQLSchema', () => {
databaseController, databaseController,
parseGraphQLController, parseGraphQLController,
log: defaultLogger, log: defaultLogger,
appId,
}); });
await parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLSchema.databaseController.schemaCache.clear();
const schema1 = await parseGraphQLSchema.load(); const schema1 = await parseGraphQLSchema.load();
@@ -476,6 +501,7 @@ describe('ParseGraphQLSchema', () => {
databaseController, databaseController,
parseGraphQLController, parseGraphQLController,
log: defaultLogger, log: defaultLogger,
appId,
}); });
const car1 = new Parse.Object('Car'); const car1 = new Parse.Object('Car');
await car1.save(); await car1.save();
@@ -511,6 +537,7 @@ describe('ParseGraphQLSchema', () => {
databaseController, databaseController,
parseGraphQLController, parseGraphQLController,
log: defaultLogger, log: defaultLogger,
appId,
}); });
const car = new Parse.Object('Car'); const car = new Parse.Object('Car');
await car.save(); await car.save();

View File

@@ -5177,19 +5177,23 @@ describe('ParseGraphQLServer', () => {
describe('Functions Mutations', () => { describe('Functions Mutations', () => {
it('can be called', async () => { it('can be called', async () => {
Parse.Cloud.define('hello', async () => { try {
return 'Hello world!'; Parse.Cloud.define('hello', async () => {
}); return 'Hello world!';
});
const result = await apolloClient.mutate({ const result = await apolloClient.mutate({
mutation: gql` mutation: gql`
mutation CallFunction { mutation CallFunction {
callCloudCode(functionName: "hello") callCloudCode(functionName: hello)
} }
`, `,
}); });
expect(result.data.callCloudCode).toEqual('Hello world!'); expect(result.data.callCloudCode).toEqual('Hello world!');
} catch (e) {
handleError(e);
}
}); });
it('can throw errors', async () => { it('can throw errors', async () => {
@@ -5201,7 +5205,7 @@ describe('ParseGraphQLServer', () => {
await apolloClient.mutate({ await apolloClient.mutate({
mutation: gql` mutation: gql`
mutation CallFunction { mutation CallFunction {
callCloudCode(functionName: "hello") callCloudCode(functionName: hello)
} }
`, `,
}); });
@@ -5302,7 +5306,7 @@ describe('ParseGraphQLServer', () => {
apolloClient.mutate({ apolloClient.mutate({
mutation: gql` mutation: gql`
mutation CallFunction($params: Object) { mutation CallFunction($params: Object) {
callCloudCode(functionName: "hello", params: $params) callCloudCode(functionName: hello, params: $params)
} }
`, `,
variables: { variables: {
@@ -5310,6 +5314,94 @@ describe('ParseGraphQLServer', () => {
}, },
}); });
}); });
it('should list all functions in the enum type', async () => {
try {
Parse.Cloud.define('a', async () => {
return 'hello a';
});
Parse.Cloud.define('b', async () => {
return 'hello b';
});
Parse.Cloud.define('_underscored', async () => {
return 'hello _underscored';
});
Parse.Cloud.define('contains1Number', async () => {
return 'hello contains1Number';
});
const functionEnum = (await apolloClient.query({
query: gql`
query ObjectType {
__type(name: "CloudCodeFunction") {
kind
enumValues {
name
}
}
}
`,
})).data['__type'];
expect(functionEnum.kind).toEqual('ENUM');
expect(
functionEnum.enumValues.map(value => value.name).sort()
).toEqual(['_underscored', 'a', 'b', 'contains1Number']);
} catch (e) {
handleError(e);
}
});
it('should warn functions not matching GraphQL allowed names', async () => {
try {
spyOn(
parseGraphQLServer.parseGraphQLSchema.log,
'warn'
).and.callThrough();
Parse.Cloud.define('a', async () => {
return 'hello a';
});
Parse.Cloud.define('double-barrelled', async () => {
return 'hello b';
});
Parse.Cloud.define('1NumberInTheBeggning', async () => {
return 'hello contains1Number';
});
const functionEnum = (await apolloClient.query({
query: gql`
query ObjectType {
__type(name: "CloudCodeFunction") {
kind
enumValues {
name
}
}
}
`,
})).data['__type'];
expect(functionEnum.kind).toEqual('ENUM');
expect(
functionEnum.enumValues.map(value => value.name).sort()
).toEqual(['a']);
expect(
parseGraphQLServer.parseGraphQLSchema.log.warn.calls
.all()
.map(call => call.args[0])
.sort()
).toEqual([
'Function 1NumberInTheBeggning could not be added to the auto schema because GraphQL names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/.',
'Function double-barrelled could not be added to the auto schema because GraphQL names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/.',
]);
} catch (e) {
handleError(e);
}
});
}); });
describe('Data Types', () => { describe('Data Types', () => {

View File

@@ -15,6 +15,7 @@ import DatabaseController from '../Controllers/DatabaseController';
import { toGraphQLError } from './parseGraphQLUtils'; import { toGraphQLError } from './parseGraphQLUtils';
import * as schemaDirectives from './loaders/schemaDirectives'; import * as schemaDirectives from './loaders/schemaDirectives';
import * as schemaTypes from './loaders/schemaTypes'; import * as schemaTypes from './loaders/schemaTypes';
import { getFunctionNames } from '../triggers';
const RESERVED_GRAPHQL_TYPE_NAMES = [ const RESERVED_GRAPHQL_TYPE_NAMES = [
'String', 'String',
@@ -29,6 +30,7 @@ const RESERVED_GRAPHQL_TYPE_NAMES = [
'Viewer', 'Viewer',
'SignUpFieldsInput', 'SignUpFieldsInput',
'LogInFieldsInput', 'LogInFieldsInput',
'CloudCodeFunction',
]; ];
const RESERVED_GRAPHQL_QUERY_NAMES = ['health', 'viewer', 'class', 'classes']; const RESERVED_GRAPHQL_QUERY_NAMES = ['health', 'viewer', 'class', 'classes'];
const RESERVED_GRAPHQL_MUTATION_NAMES = [ const RESERVED_GRAPHQL_MUTATION_NAMES = [
@@ -53,6 +55,7 @@ class ParseGraphQLSchema {
databaseController: DatabaseController, databaseController: DatabaseController,
parseGraphQLController: ParseGraphQLController, parseGraphQLController: ParseGraphQLController,
log: any, log: any,
appId: string,
} = {} } = {}
) { ) {
this.parseGraphQLController = this.parseGraphQLController =
@@ -64,13 +67,16 @@ class ParseGraphQLSchema {
this.log = this.log =
params.log || requiredParameter('You must provide a log instance!'); params.log || requiredParameter('You must provide a log instance!');
this.graphQLCustomTypeDefs = params.graphQLCustomTypeDefs; this.graphQLCustomTypeDefs = params.graphQLCustomTypeDefs;
this.appId =
params.appId || requiredParameter('You must provide the appId!');
} }
async load() { async load() {
const { parseGraphQLConfig } = await this._initializeSchemaAndConfig(); const { parseGraphQLConfig } = await this._initializeSchemaAndConfig();
const parseClasses = await this._getClassesForSchema(parseGraphQLConfig); const parseClasses = await this._getClassesForSchema(parseGraphQLConfig);
const parseClassesString = JSON.stringify(parseClasses); const parseClassesString = JSON.stringify(parseClasses);
const functionNames = await this._getFunctionNames();
const functionNamesString = JSON.stringify(functionNames);
if ( if (
this.graphQLSchema && this.graphQLSchema &&
@@ -78,6 +84,7 @@ class ParseGraphQLSchema {
parseClasses, parseClasses,
parseClassesString, parseClassesString,
parseGraphQLConfig, parseGraphQLConfig,
functionNamesString,
}) })
) { ) {
return this.graphQLSchema; return this.graphQLSchema;
@@ -86,6 +93,8 @@ class ParseGraphQLSchema {
this.parseClasses = parseClasses; this.parseClasses = parseClasses;
this.parseClassesString = parseClassesString; this.parseClassesString = parseClassesString;
this.parseGraphQLConfig = parseGraphQLConfig; this.parseGraphQLConfig = parseGraphQLConfig;
this.functionNames = functionNames;
this.functionNamesString = functionNamesString;
this.parseClassTypes = {}; this.parseClassTypes = {};
this.viewerType = null; this.viewerType = null;
this.graphQLAutoSchema = null; this.graphQLAutoSchema = null;
@@ -360,6 +369,19 @@ class ParseGraphQLSchema {
}); });
} }
async _getFunctionNames() {
return await getFunctionNames(this.appId).filter(functionName => {
if (/^[_a-zA-Z][_a-zA-Z0-9]*$/.test(functionName)) {
return true;
} else {
this.log.warn(
`Function ${functionName} could not be added to the auto schema because GraphQL names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/.`
);
return false;
}
});
}
/** /**
* Checks for changes to the parseClasses * Checks for changes to the parseClasses
* objects (i.e. database schema) or to * objects (i.e. database schema) or to
@@ -370,12 +392,19 @@ class ParseGraphQLSchema {
parseClasses: any, parseClasses: any,
parseClassesString: string, parseClassesString: string,
parseGraphQLConfig: ?ParseGraphQLConfig, parseGraphQLConfig: ?ParseGraphQLConfig,
functionNamesString: string,
}): boolean { }): boolean {
const { parseClasses, parseClassesString, parseGraphQLConfig } = params; const {
parseClasses,
parseClassesString,
parseGraphQLConfig,
functionNamesString,
} = params;
if ( if (
JSON.stringify(this.parseGraphQLConfig) === JSON.stringify(this.parseGraphQLConfig) ===
JSON.stringify(parseGraphQLConfig) JSON.stringify(parseGraphQLConfig) &&
this.functionNamesString === functionNamesString
) { ) {
if (this.parseClasses === parseClasses) { if (this.parseClasses === parseClasses) {
return false; return false;

View File

@@ -33,6 +33,7 @@ class ParseGraphQLServer {
databaseController: this.parseServer.config.databaseController, databaseController: this.parseServer.config.databaseController,
log: this.log, log: this.log,
graphQLCustomTypeDefs: this.config.graphQLCustomTypeDefs, graphQLCustomTypeDefs: this.config.graphQLCustomTypeDefs,
appId: this.parseServer.config.appId,
}); });
} }

View File

@@ -1,46 +1,65 @@
import { GraphQLNonNull, GraphQLString } from 'graphql'; import { GraphQLNonNull, GraphQLEnumType } from 'graphql';
import { FunctionsRouter } from '../../Routers/FunctionsRouter'; import { FunctionsRouter } from '../../Routers/FunctionsRouter';
import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as defaultGraphQLTypes from './defaultGraphQLTypes';
const load = parseGraphQLSchema => { const load = parseGraphQLSchema => {
parseGraphQLSchema.addGraphQLMutation( if (parseGraphQLSchema.functionNames.length > 0) {
'callCloudCode', const cloudCodeFunctionEnum = parseGraphQLSchema.addGraphQLType(
{ new GraphQLEnumType({
description: name: 'CloudCodeFunction',
'The call mutation can be used to invoke a cloud code function.', description:
args: { 'The CloudCodeFunction enum type contains a list of all available cloud code functions.',
functionName: { values: parseGraphQLSchema.functionNames.reduce(
description: 'This is the name of the function to be called.', (values, functionName) => ({
type: new GraphQLNonNull(GraphQLString), ...values,
}, [functionName]: { value: functionName },
params: { }),
description: 'These are the params to be passed to the function.', {}
type: defaultGraphQLTypes.OBJECT, ),
}, }),
}, true,
type: defaultGraphQLTypes.ANY, true
async resolve(_source, args, context) { );
try {
const { functionName, params } = args;
const { config, auth, info } = context;
return (await FunctionsRouter.handleCloudFunction({ parseGraphQLSchema.addGraphQLMutation(
params: { 'callCloudCode',
functionName, {
}, description:
config, 'The call mutation can be used to invoke a cloud code function.',
auth, args: {
info, functionName: {
body: params, description: 'This is the function to be called.',
})).response.result; type: new GraphQLNonNull(cloudCodeFunctionEnum),
} catch (e) { },
parseGraphQLSchema.handleError(e); 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);
}
},
}, },
}, true,
true, true
true );
); }
}; };
export { load }; export { load };

View File

@@ -148,6 +148,29 @@ export function getFunction(functionName, applicationId) {
return get(Category.Functions, functionName, applicationId); return get(Category.Functions, functionName, applicationId);
} }
export function getFunctionNames(applicationId) {
const store =
(_triggerStore[applicationId] &&
_triggerStore[applicationId][Category.Functions]) ||
{};
const functionNames = [];
const extractFunctionNames = (namespace, store) => {
Object.keys(store).forEach(name => {
const value = store[name];
if (namespace) {
name = `${namespace}.${name}`;
}
if (typeof value === 'function') {
functionNames.push(name);
} else {
extractFunctionNames(name, value);
}
});
};
extractFunctionNames(null, store);
return functionNames;
}
export function getJob(jobName, applicationId) { export function getJob(jobName, applicationId) {
return get(Category.Jobs, jobName, applicationId); return get(Category.Jobs, jobName, applicationId);
} }