GraphQL Configuration Options (#5782)

* add parse-graph-ql configuration for class schema customisation

Not yet tested - essentially an RFC

* refactor and add graphql router, controller and config cache

* fix(GraphQLController): add missing check isEnabled

* chore(GraphQLController): remove awaits from cache put

* chore(GraphQLController): remove check for if its enabled

* refactor(GraphQLController): only use cache if mounted

* chore(GraphQLController): group all validation errors and throw at once

* chore(GraphQLSchema): move transformations into controller validation

* refactor(GraphQL): improve ctrl validation and fix schema usage of config

* refactor(GraphQLSchema): remove code related to additional schema

This code has been moved into a separate feature branch.

* fix(GraphQLSchema): fix incorrect default return type for class configs

* refactor(GraphQLSchema): update staleness check code to account for config

* fix(GraphQLServer): fix regressed tests due to internal schema changes

This will be followed up with a backwards compatability fix for the `ClassFields` issue to avoid breakages for our users

* refactor: rename to ParseGraphQLController for consistency

* fix(ParseGraphQLCtrl): numerous fixes for validity checking

Also includes some minor code refactoring

* chore(GraphQL): minor syntax cleanup

* fix(SchemaController): add _GraphQLConfig to volatile classes

* refactor(ParseGraphQLServer): return update config value in setGraphQLConfig

* testing(ParseGraphQL): add test cases for new graphQLConfig

* fix(GraphQLController): fix issue where config with multiple items was not being mapped to the db

* fix(postgres): add _GraphQLConfig default schema on load

fixes failing postgres tests

* GraphQL @mock directive (#5836)

* Add mock directive
* Include tests for @mock directive

* Fix existing tests due to the change from ClassFields to ClassCreateFields

* fix(parseClassMutations): safer type transformation based on input type

* fix(parseClassMutations): only define necessary input fields

* fix(GraphQL): fix incorrect import paths
This commit is contained in:
Omair Vaiyani
2019-07-25 20:46:25 +01:00
committed by Antonio Davi Macedo Coelho de Castro
parent bbcc20fd60
commit d3810c2eba
18 changed files with 2956 additions and 290 deletions

View File

@@ -0,0 +1,973 @@
const {
default: ParseGraphQLController,
GraphQLConfigClassName,
GraphQLConfigId,
GraphQLConfigKey,
} = require('../lib/Controllers/ParseGraphQLController');
const { isEqual } = require('lodash');
describe('ParseGraphQLController', () => {
let parseServer;
let databaseController;
let cacheController;
let databaseUpdateArgs;
// Holds the graphQLConfig in memory instead of using the db
let graphQLConfigRecord;
const setConfigOnDb = graphQLConfigData => {
graphQLConfigRecord = {
objectId: GraphQLConfigId,
[GraphQLConfigKey]: graphQLConfigData,
};
};
const removeConfigFromDb = () => {
graphQLConfigRecord = null;
};
const getConfigFromDb = () => {
return graphQLConfigRecord;
};
beforeAll(async () => {
parseServer = await global.reconfigureServer({
schemaCacheTTL: 100,
});
databaseController = parseServer.config.databaseController;
cacheController = parseServer.config.cacheController;
const defaultFind = databaseController.find.bind(databaseController);
databaseController.find = async (className, query, ...args) => {
if (
className === GraphQLConfigClassName &&
isEqual(query, { objectId: GraphQLConfigId })
) {
const graphQLConfigRecord = getConfigFromDb();
return graphQLConfigRecord ? [graphQLConfigRecord] : [];
} else {
return defaultFind(className, query, ...args);
}
};
const defaultUpdate = databaseController.update.bind(databaseController);
databaseController.update = async (
className,
query,
update,
fullQueryOptions
) => {
databaseUpdateArgs = [className, query, update, fullQueryOptions];
if (
className === GraphQLConfigClassName &&
isEqual(query, { objectId: GraphQLConfigId }) &&
update &&
!!update[GraphQLConfigKey] &&
fullQueryOptions &&
isEqual(fullQueryOptions, { upsert: true })
) {
setConfigOnDb(update[GraphQLConfigKey]);
} else {
return defaultUpdate(...databaseUpdateArgs);
}
};
});
beforeEach(() => {
databaseUpdateArgs = null;
});
describe('constructor', () => {
it('should require a databaseController', () => {
expect(() => new ParseGraphQLController()).toThrow(
'ParseGraphQLController requires a "databaseController" to be instantiated.'
);
expect(() => new ParseGraphQLController({ cacheController })).toThrow(
'ParseGraphQLController requires a "databaseController" to be instantiated.'
);
expect(
() =>
new ParseGraphQLController({
cacheController,
mountGraphQL: false,
})
).toThrow(
'ParseGraphQLController requires a "databaseController" to be instantiated.'
);
});
it('should construct without a cacheController', () => {
expect(
() =>
new ParseGraphQLController({
databaseController,
})
).not.toThrow();
expect(
() =>
new ParseGraphQLController({
databaseController,
mountGraphQL: true,
})
).not.toThrow();
});
it('should set isMounted to true if config.mountGraphQL is true', () => {
const mountedController = new ParseGraphQLController({
databaseController,
mountGraphQL: true,
});
expect(mountedController.isMounted).toBe(true);
const unmountedController = new ParseGraphQLController({
databaseController,
mountGraphQL: false,
});
expect(unmountedController.isMounted).toBe(false);
const unmountedController2 = new ParseGraphQLController({
databaseController,
});
expect(unmountedController2.isMounted).toBe(false);
});
});
describe('getGraphQLConfig', () => {
it('should return an empty graphQLConfig if collection has none', async () => {
removeConfigFromDb();
const parseGraphQLController = new ParseGraphQLController({
databaseController,
mountGraphQL: false,
});
const graphQLConfig = await parseGraphQLController.getGraphQLConfig();
expect(graphQLConfig).toEqual({});
});
it('should return an existing graphQLConfig', async () => {
setConfigOnDb({ enabledForClasses: ['_User'] });
const parseGraphQLController = new ParseGraphQLController({
databaseController,
mountGraphQL: false,
});
const graphQLConfig = await parseGraphQLController.getGraphQLConfig();
expect(graphQLConfig).toEqual({ enabledForClasses: ['_User'] });
});
it('should use the cache if mounted, and return the stored graphQLConfig', async () => {
removeConfigFromDb();
cacheController.graphQL.clear();
const parseGraphQLController = new ParseGraphQLController({
databaseController,
cacheController,
mountGraphQL: true,
});
cacheController.graphQL.put(parseGraphQLController.configCacheKey, {
enabledForClasses: ['SuperCar'],
});
const graphQLConfig = await parseGraphQLController.getGraphQLConfig();
expect(graphQLConfig).toEqual({ enabledForClasses: ['SuperCar'] });
});
it('should use the database when mounted and cache is empty', async () => {
setConfigOnDb({ disabledForClasses: ['SuperCar'] });
cacheController.graphQL.clear();
const parseGraphQLController = new ParseGraphQLController({
databaseController,
cacheController,
mountGraphQL: true,
});
const graphQLConfig = await parseGraphQLController.getGraphQLConfig();
expect(graphQLConfig).toEqual({ disabledForClasses: ['SuperCar'] });
});
it('should store the graphQLConfig in cache if mounted', async () => {
setConfigOnDb({ enabledForClasses: ['SuperCar'] });
cacheController.graphQL.clear();
const parseGraphQLController = new ParseGraphQLController({
databaseController,
cacheController,
mountGraphQL: true,
});
const cachedValueBefore = await cacheController.graphQL.get(
parseGraphQLController.configCacheKey
);
expect(cachedValueBefore).toBeNull();
await parseGraphQLController.getGraphQLConfig();
const cachedValueAfter = await cacheController.graphQL.get(
parseGraphQLController.configCacheKey
);
expect(cachedValueAfter).toEqual({ enabledForClasses: ['SuperCar'] });
});
});
describe('updateGraphQLConfig', () => {
const successfulUpdateResponse = { response: { result: true } };
it('should throw if graphQLConfig is not provided', async function() {
const parseGraphQLController = new ParseGraphQLController({
databaseController,
});
expectAsync(
parseGraphQLController.updateGraphQLConfig()
).toBeRejectedWith('You must provide a graphQLConfig!');
});
it('should correct update the graphQLConfig object using the databaseController', async () => {
const parseGraphQLController = new ParseGraphQLController({
databaseController,
});
const graphQLConfig = {
enabledForClasses: ['ClassA', 'ClassB'],
disabledForClasses: [],
classConfigs: [
{ className: 'ClassA', query: { get: false } },
{ className: 'ClassB', mutation: { destroy: false }, type: {} },
],
};
await parseGraphQLController.updateGraphQLConfig(graphQLConfig);
expect(databaseUpdateArgs).toBeTruthy();
const [className, query, update, op] = databaseUpdateArgs;
expect(className).toBe(GraphQLConfigClassName);
expect(query).toEqual({ objectId: GraphQLConfigId });
expect(update).toEqual({
[GraphQLConfigKey]: graphQLConfig,
});
expect(op).toEqual({ upsert: true });
});
it('should throw if graphQLConfig is not an object', async () => {
const parseGraphQLController = new ParseGraphQLController({
databaseController,
});
expectAsync(
parseGraphQLController.updateGraphQLConfig([])
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig(function() {})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig(Promise.resolve({}))
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig('')
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({})
).toBeResolvedTo(successfulUpdateResponse);
});
it('should throw if graphQLConfig has an invalid root key', async () => {
const parseGraphQLController = new ParseGraphQLController({
databaseController,
});
expectAsync(
parseGraphQLController.updateGraphQLConfig({ invalidKey: true })
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({})
).toBeResolvedTo(successfulUpdateResponse);
});
it('should throw if graphQLConfig has invalid class filters', async () => {
const parseGraphQLController = new ParseGraphQLController({
databaseController,
});
expectAsync(
parseGraphQLController.updateGraphQLConfig({ enabledForClasses: {} })
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
enabledForClasses: [undefined],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
disabledForClasses: [null],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
enabledForClasses: ['_User', null],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({ disabledForClasses: [''] })
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
enabledForClasses: [],
disabledForClasses: ['_User'],
})
).toBeResolvedTo(successfulUpdateResponse);
});
it('should throw if classConfigs array is invalid', async () => {
const parseGraphQLController = new ParseGraphQLController({
databaseController,
});
expectAsync(
parseGraphQLController.updateGraphQLConfig({ classConfigs: {} })
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({ classConfigs: [null] })
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [undefined],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [{ className: 'ValidClass' }, null],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({ classConfigs: [] })
).toBeResolvedTo(successfulUpdateResponse);
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
},
],
})
).toBeResolvedTo(successfulUpdateResponse);
});
it('should throw if a classConfig has invalid type settings', async () => {
const parseGraphQLController = new ParseGraphQLController({
databaseController,
});
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
type: [],
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
type: {
invalidKey: true,
},
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
type: {},
},
],
})
).toBeResolvedTo(successfulUpdateResponse);
});
it('should throw if a classConfig has invalid type.inputFields settings', async () => {
const parseGraphQLController = new ParseGraphQLController({
databaseController,
});
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: 'SuperCar',
type: {
inputFields: [],
},
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: 'SuperCar',
type: {
inputFields: {
invalidKey: true,
},
},
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: 'SuperCar',
type: {
inputFields: {
create: {},
},
},
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: 'SuperCar',
type: {
inputFields: {
update: [null],
},
},
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: 'SuperCar',
type: {
inputFields: {
create: [],
update: [],
},
},
},
],
})
).toBeResolvedTo(successfulUpdateResponse);
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: 'SuperCar',
type: {
inputFields: {
create: ['make', 'model'],
update: [],
},
},
},
],
})
).toBeResolvedTo(successfulUpdateResponse);
});
it('should throw if a classConfig has invalid type.outputFields settings', async () => {
const parseGraphQLController = new ParseGraphQLController({
databaseController,
});
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
type: {
outputFields: {},
},
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
type: {
outputFields: [null],
},
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
type: {
outputFields: ['name', undefined],
},
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
type: {
outputFields: [''],
},
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
type: {
outputFields: [],
},
},
],
})
).toBeResolvedTo(successfulUpdateResponse);
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
type: {
outputFields: ['name'],
},
},
],
})
).toBeResolvedTo(successfulUpdateResponse);
});
it('should throw if a classConfig has invalid type.constraintFields settings', async () => {
const parseGraphQLController = new ParseGraphQLController({
databaseController,
});
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
type: {
constraintFields: {},
},
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
type: {
constraintFields: [null],
},
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
type: {
constraintFields: ['name', undefined],
},
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
type: {
constraintFields: [''],
},
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
type: {
constraintFields: [],
},
},
],
})
).toBeResolvedTo(successfulUpdateResponse);
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
type: {
constraintFields: ['name'],
},
},
],
})
).toBeResolvedTo(successfulUpdateResponse);
});
it('should throw if a classConfig has invalid type.sortFields settings', async () => {
const parseGraphQLController = new ParseGraphQLController({
databaseController,
});
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
type: {
sortFields: {},
},
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
type: {
sortFields: [null],
},
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
type: {
sortFields: [
{
field: undefined,
asc: true,
desc: true,
},
],
},
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
type: {
sortFields: [
{
field: '',
asc: true,
desc: false,
},
],
},
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
type: {
sortFields: [
{
field: 'name',
asc: true,
desc: 'false',
},
],
},
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
type: {
sortFields: [
{
field: 'name',
asc: true,
desc: true,
},
null,
],
},
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
type: {
sortFields: [],
},
},
],
})
).toBeResolvedTo(successfulUpdateResponse);
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
type: {
sortFields: [
{
field: 'name',
asc: true,
desc: true,
},
],
},
},
],
})
).toBeResolvedTo(successfulUpdateResponse);
});
it('should throw if a classConfig has invalid query params', async () => {
const parseGraphQLController = new ParseGraphQLController({
databaseController,
});
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
query: [],
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
query: {
invalidKey: true,
},
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
query: {
get: 1,
},
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
query: {
find: 'true',
},
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
query: {
get: false,
find: true,
},
},
],
})
).toBeResolvedTo(successfulUpdateResponse);
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
query: {},
},
],
})
).toBeResolvedTo(successfulUpdateResponse);
});
it('should throw if a classConfig has invalid mutation params', async () => {
const parseGraphQLController = new ParseGraphQLController({
databaseController,
});
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
mutation: [],
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
mutation: {
invalidKey: true,
},
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
mutation: {
destroy: 1,
},
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
mutation: {
update: 'true',
},
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
mutation: {},
},
],
})
).toBeResolvedTo(successfulUpdateResponse);
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
mutation: {
create: true,
update: true,
destroy: false,
},
},
],
})
).toBeResolvedTo(successfulUpdateResponse);
});
it('should throw if _User create fields is missing username or password', async () => {
const parseGraphQLController = new ParseGraphQLController({
databaseController,
});
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
type: {
inputFields: {
create: ['username', 'no-password'],
},
},
},
],
})
).toBeRejected();
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className: '_User',
type: {
inputFields: {
create: ['username', 'password'],
},
},
},
],
})
).toBeResolved(successfulUpdateResponse);
});
it('should update the cache if mounted', async () => {
removeConfigFromDb();
cacheController.graphQL.clear();
const mountedController = new ParseGraphQLController({
databaseController,
cacheController,
mountGraphQL: true,
});
const unmountedController = new ParseGraphQLController({
databaseController,
cacheController,
mountGraphQL: false,
});
let cacheBeforeValue;
let cacheAfterValue;
cacheBeforeValue = await cacheController.graphQL.get(
mountedController.configCacheKey
);
expect(cacheBeforeValue).toBeNull();
await mountedController.updateGraphQLConfig({
enabledForClasses: ['SuperCar'],
});
cacheAfterValue = await cacheController.graphQL.get(
mountedController.configCacheKey
);
expect(cacheAfterValue).toEqual({ enabledForClasses: ['SuperCar'] });
// reset
removeConfigFromDb();
cacheController.graphQL.clear();
cacheBeforeValue = await cacheController.graphQL.get(
unmountedController.configCacheKey
);
expect(cacheBeforeValue).toBeNull();
await unmountedController.updateGraphQLConfig({
enabledForClasses: ['SuperCar'],
});
cacheAfterValue = await cacheController.graphQL.get(
unmountedController.configCacheKey
);
expect(cacheAfterValue).toBeNull();
});
});
});

View File

@@ -4,6 +4,7 @@ const { ParseGraphQLSchema } = require('../lib/GraphQL/ParseGraphQLSchema');
describe('ParseGraphQLSchema', () => {
let parseServer;
let databaseController;
let parseGraphQLController;
let parseGraphQLSchema;
beforeAll(async () => {
@@ -11,28 +12,37 @@ describe('ParseGraphQLSchema', () => {
schemaCacheTTL: 100,
});
databaseController = parseServer.config.databaseController;
parseGraphQLSchema = new ParseGraphQLSchema(
parseGraphQLController = parseServer.config.parseGraphQLController;
parseGraphQLSchema = new ParseGraphQLSchema({
databaseController,
defaultLogger
);
parseGraphQLController,
log: defaultLogger,
});
});
describe('constructor', () => {
it('should require a databaseController and a log instance', () => {
it('should require a parseGraphQLController, databaseController and a log instance', () => {
expect(() => new ParseGraphQLSchema()).toThrow(
'You must provide a databaseController instance!'
'You must provide a parseGraphQLController instance!'
);
expect(() => new ParseGraphQLSchema({})).toThrow(
'You must provide a log instance!'
);
expect(() => new ParseGraphQLSchema({}, {})).not.toThrow();
expect(
() => new ParseGraphQLSchema({ parseGraphQLController: {} })
).toThrow('You must provide a databaseController instance!');
expect(
() =>
new ParseGraphQLSchema({
parseGraphQLController: {},
databaseController: {},
})
).toThrow('You must provide a log instance!');
});
});
describe('load', () => {
it('should cache schema', async () => {
const graphQLSchema = await parseGraphQLSchema.load();
expect(graphQLSchema).toBe(await parseGraphQLSchema.load());
const updatedGraphQLSchema = await parseGraphQLSchema.load();
expect(graphQLSchema).toBe(updatedGraphQLSchema);
await new Promise(resolve => setTimeout(resolve, 200));
expect(graphQLSchema).toBe(await parseGraphQLSchema.load());
});
@@ -40,26 +50,72 @@ describe('ParseGraphQLSchema', () => {
it('should load a brand new GraphQL Schema if Parse Schema changes', async () => {
await parseGraphQLSchema.load();
const parseClasses = parseGraphQLSchema.parseClasses;
const parseClassesString = parseGraphQLSchema.parseClasses;
const parseClassTypes = parseGraphQLSchema.parseClasses;
const graphQLSchema = parseGraphQLSchema.parseClasses;
const graphQLTypes = parseGraphQLSchema.parseClasses;
const graphQLQueries = parseGraphQLSchema.parseClasses;
const graphQLMutations = parseGraphQLSchema.parseClasses;
const graphQLSubscriptions = parseGraphQLSchema.parseClasses;
const parseClassesString = parseGraphQLSchema.parseClassesString;
const parseClassTypes = parseGraphQLSchema.parseClassTypes;
const graphQLSchema = parseGraphQLSchema.graphQLSchema;
const graphQLTypes = parseGraphQLSchema.graphQLTypes;
const graphQLQueries = parseGraphQLSchema.graphQLQueries;
const graphQLMutations = parseGraphQLSchema.graphQLMutations;
const graphQLSubscriptions = parseGraphQLSchema.graphQLSubscriptions;
const newClassObject = new Parse.Object('NewClass');
await newClassObject.save();
await databaseController.schemaCache.clear();
await new Promise(resolve => setTimeout(resolve, 200));
await parseGraphQLSchema.load();
expect(parseClasses).not.toBe(parseGraphQLSchema.parseClasses);
expect(parseClassesString).not.toBe(parseGraphQLSchema.parseClasses);
expect(parseClassTypes).not.toBe(parseGraphQLSchema.parseClasses);
expect(graphQLSchema).not.toBe(parseGraphQLSchema.parseClasses);
expect(graphQLTypes).not.toBe(parseGraphQLSchema.parseClasses);
expect(graphQLQueries).not.toBe(parseGraphQLSchema.parseClasses);
expect(graphQLMutations).not.toBe(parseGraphQLSchema.parseClasses);
expect(graphQLSubscriptions).not.toBe(parseGraphQLSchema.parseClasses);
expect(parseClassesString).not.toBe(
parseGraphQLSchema.parseClassesString
);
expect(parseClassTypes).not.toBe(parseGraphQLSchema.parseClassTypes);
expect(graphQLSchema).not.toBe(parseGraphQLSchema.graphQLSchema);
expect(graphQLTypes).not.toBe(parseGraphQLSchema.graphQLTypes);
expect(graphQLQueries).not.toBe(parseGraphQLSchema.graphQLQueries);
expect(graphQLMutations).not.toBe(parseGraphQLSchema.graphQLMutations);
expect(graphQLSubscriptions).not.toBe(
parseGraphQLSchema.graphQLSubscriptions
);
});
it('should load a brand new GraphQL Schema if graphQLConfig changes', async () => {
const parseGraphQLController = {
graphQLConfig: { enabledForClasses: [] },
getGraphQLConfig() {
return this.graphQLConfig;
},
};
const parseGraphQLSchema = new ParseGraphQLSchema({
databaseController,
parseGraphQLController,
log: defaultLogger,
});
await parseGraphQLSchema.load();
const parseClasses = parseGraphQLSchema.parseClasses;
const parseClassesString = parseGraphQLSchema.parseClassesString;
const parseClassTypes = parseGraphQLSchema.parseClassTypes;
const graphQLSchema = parseGraphQLSchema.graphQLSchema;
const graphQLTypes = parseGraphQLSchema.graphQLTypes;
const graphQLQueries = parseGraphQLSchema.graphQLQueries;
const graphQLMutations = parseGraphQLSchema.graphQLMutations;
const graphQLSubscriptions = parseGraphQLSchema.graphQLSubscriptions;
parseGraphQLController.graphQLConfig = {
enabledForClasses: ['_User'],
};
await new Promise(resolve => setTimeout(resolve, 200));
await parseGraphQLSchema.load();
expect(parseClasses).not.toBe(parseGraphQLSchema.parseClasses);
expect(parseClassesString).not.toBe(
parseGraphQLSchema.parseClassesString
);
expect(parseClassTypes).not.toBe(parseGraphQLSchema.parseClassTypes);
expect(graphQLSchema).not.toBe(parseGraphQLSchema.graphQLSchema);
expect(graphQLTypes).not.toBe(parseGraphQLSchema.graphQLTypes);
expect(graphQLQueries).not.toBe(parseGraphQLSchema.graphQLQueries);
expect(graphQLMutations).not.toBe(parseGraphQLSchema.graphQLMutations);
expect(graphQLSubscriptions).not.toBe(
parseGraphQLSchema.graphQLSubscriptions
);
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -47,7 +47,7 @@ const transformKeyValueForUpdate = (
switch (key) {
case 'objectId':
case '_id':
if (className === '_GlobalConfig') {
if (['_GlobalConfig', '_GraphQLConfig'].includes(className)) {
return {
key: key,
value: parseInt(restValue),
@@ -252,7 +252,7 @@ function transformQueryKeyValue(className, key, value, schema, count = false) {
}
break;
case 'objectId': {
if (className === '_GlobalConfig') {
if (['_GlobalConfig', '_GraphQLConfig'].includes(className)) {
value = parseInt(value);
}
return { key: '_id', value };

View File

@@ -1133,6 +1133,7 @@ export class PostgresStorageAdapter implements StorageAdapter {
'_JobSchedule',
'_Hooks',
'_GlobalConfig',
'_GraphQLConfig',
'_Audience',
...results.map(result => result.className),
...joins,

View File

@@ -45,6 +45,7 @@ export class CacheController extends AdaptableController {
this.role = new SubCache('role', this);
this.user = new SubCache('user', this);
this.graphQL = new SubCache('graphQL', this);
}
get(key) {

View File

@@ -0,0 +1,375 @@
import requiredParameter from '../../lib/requiredParameter';
import DatabaseController from './DatabaseController';
import CacheController from './CacheController';
const GraphQLConfigClassName = '_GraphQLConfig';
const GraphQLConfigId = '1';
const GraphQLConfigKey = 'config';
class ParseGraphQLController {
databaseController: DatabaseController;
cacheController: CacheController;
isMounted: boolean;
configCacheKey: string;
constructor(
params: {
databaseController: DatabaseController,
cacheController: CacheController,
} = {}
) {
this.databaseController =
params.databaseController ||
requiredParameter(
`ParseGraphQLController requires a "databaseController" to be instantiated.`
);
this.cacheController = params.cacheController;
this.isMounted = !!params.mountGraphQL;
this.configCacheKey = GraphQLConfigKey;
}
async getGraphQLConfig(): Promise<ParseGraphQLConfig> {
if (this.isMounted) {
const _cachedConfig = await this._getCachedGraphQLConfig();
if (_cachedConfig) {
return _cachedConfig;
}
}
const results = await this.databaseController.find(
GraphQLConfigClassName,
{ objectId: GraphQLConfigId },
{ limit: 1 }
);
let graphQLConfig;
if (results.length != 1) {
// If there is no config in the database - return empty config.
return {};
} else {
graphQLConfig = results[0][GraphQLConfigKey];
}
if (this.isMounted) {
this._putCachedGraphQLConfig(graphQLConfig);
}
return graphQLConfig;
}
async updateGraphQLConfig(
graphQLConfig: ParseGraphQLConfig
): Promise<ParseGraphQLConfig> {
// throws if invalid
this._validateGraphQLConfig(
graphQLConfig || requiredParameter('You must provide a graphQLConfig!')
);
// Transform in dot notation to make sure it works
const update = Object.keys(graphQLConfig).reduce(
(acc, key) => {
return {
[GraphQLConfigKey]: {
...acc[GraphQLConfigKey],
[key]: graphQLConfig[key],
},
};
},
{ [GraphQLConfigKey]: {} }
);
await this.databaseController.update(
GraphQLConfigClassName,
{ objectId: GraphQLConfigId },
update,
{ upsert: true }
);
if (this.isMounted) {
this._putCachedGraphQLConfig(graphQLConfig);
}
return { response: { result: true } };
}
_getCachedGraphQLConfig() {
return this.cacheController.graphQL.get(this.configCacheKey);
}
_putCachedGraphQLConfig(graphQLConfig: ParseGraphQLConfig) {
return this.cacheController.graphQL.put(
this.configCacheKey,
graphQLConfig,
60000
);
}
_validateGraphQLConfig(graphQLConfig: ?ParseGraphQLConfig): void {
const errorMessages: string = [];
if (!graphQLConfig) {
errorMessages.push('cannot be undefined, null or empty');
} else if (!isValidSimpleObject(graphQLConfig)) {
errorMessages.push('must be a valid object');
} else {
const {
enabledForClasses = null,
disabledForClasses = null,
classConfigs = null,
...invalidKeys
} = graphQLConfig;
if (Object.keys(invalidKeys).length) {
errorMessages.push(
`encountered invalid keys: [${Object.keys(invalidKeys)}]`
);
}
if (
enabledForClasses !== null &&
!isValidStringArray(enabledForClasses)
) {
errorMessages.push(`"enabledForClasses" is not a valid array`);
}
if (
disabledForClasses !== null &&
!isValidStringArray(disabledForClasses)
) {
errorMessages.push(`"disabledForClasses" is not a valid array`);
}
if (classConfigs !== null) {
if (Array.isArray(classConfigs)) {
classConfigs.forEach(classConfig => {
const errorMessage = this._validateClassConfig(classConfig);
if (errorMessage) {
errorMessages.push(
`classConfig:${classConfig.className} is invalid because ${errorMessage}`
);
}
});
} else {
errorMessages.push(`"classConfigs" is not a valid array`);
}
}
}
if (errorMessages.length) {
throw new Error(`Invalid graphQLConfig: ${errorMessages.join('; ')}`);
}
}
_validateClassConfig(classConfig: ?ParseGraphQLClassConfig): string | void {
if (!isValidSimpleObject(classConfig)) {
return 'it must be a valid object';
} else {
const {
className,
type = null,
query = null,
mutation = null,
...invalidKeys
} = classConfig;
if (Object.keys(invalidKeys).length) {
return `"invalidKeys" [${Object.keys(
invalidKeys
)}] should not be present`;
}
if (typeof className !== 'string' || !className.trim().length) {
// TODO consider checking class exists in schema?
return `"className" must be a valid string`;
}
if (type !== null) {
if (!isValidSimpleObject(type)) {
return `"type" must be a valid object`;
}
const {
inputFields = null,
outputFields = null,
constraintFields = null,
sortFields = null,
...invalidKeys
} = type;
if (Object.keys(invalidKeys).length) {
return `"type" contains invalid keys, [${Object.keys(invalidKeys)}]`;
} else if (outputFields !== null && !isValidStringArray(outputFields)) {
return `"outputFields" must be a valid string array`;
} else if (
constraintFields !== null &&
!isValidStringArray(constraintFields)
) {
return `"constraintFields" must be a valid string array`;
}
if (sortFields !== null) {
if (Array.isArray(sortFields)) {
let errorMessage;
sortFields.every((sortField, index) => {
if (!isValidSimpleObject(sortField)) {
errorMessage = `"sortField" at index ${index} is not a valid object`;
return false;
} else {
const { field, asc, desc, ...invalidKeys } = sortField;
if (Object.keys(invalidKeys).length) {
errorMessage = `"sortField" at index ${index} contains invalid keys, [${Object.keys(
invalidKeys
)}]`;
return false;
} else {
if (typeof field !== 'string' || field.trim().length === 0) {
errorMessage = `"sortField" at index ${index} did not provide the "field" as a string`;
return false;
} else if (
typeof asc !== 'boolean' ||
typeof desc !== 'boolean'
) {
errorMessage = `"sortField" at index ${index} did not provide "asc" or "desc" as booleans`;
return false;
}
}
}
return true;
});
if (errorMessage) {
return errorMessage;
}
} else {
return `"sortFields" must be a valid array.`;
}
}
if (inputFields !== null) {
if (isValidSimpleObject(inputFields)) {
const {
create = null,
update = null,
...invalidKeys
} = inputFields;
if (Object.keys(invalidKeys).length) {
return `"inputFields" contains invalid keys: [${Object.keys(
invalidKeys
)}]`;
} else {
if (update !== null && !isValidStringArray(update)) {
return `"inputFields.update" must be a valid string array`;
} else if (create !== null) {
if (!isValidStringArray(create)) {
return `"inputFields.create" must be a valid string array`;
} else if (className === '_User') {
if (
!create.includes('username') ||
!create.includes('password')
) {
return `"inputFields.create" must include required fields, username and password`;
}
}
}
}
} else {
return `"inputFields" must be a valid object`;
}
}
}
if (query !== null) {
if (isValidSimpleObject(query)) {
const { find = null, get = null, ...invalidKeys } = query;
if (Object.keys(invalidKeys).length) {
return `"query" contains invalid keys, [${Object.keys(
invalidKeys
)}]`;
} else if (find !== null && typeof find !== 'boolean') {
return `"query.find" must be a boolean`;
} else if (get !== null && typeof get !== 'boolean') {
return `"query.get" must be a boolean`;
}
} else {
return `"query" must be a valid object`;
}
}
if (mutation !== null) {
if (isValidSimpleObject(mutation)) {
const {
create = null,
update = null,
destroy = null,
...invalidKeys
} = mutation;
if (Object.keys(invalidKeys).length) {
return `"mutation" contains invalid keys, [${Object.keys(
invalidKeys
)}]`;
}
if (create !== null && typeof create !== 'boolean') {
return `"mutation.create" must be a boolean`;
}
if (update !== null && typeof update !== 'boolean') {
return `"mutation.update" must be a boolean`;
}
if (destroy !== null && typeof destroy !== 'boolean') {
return `"mutation.destroy" must be a boolean`;
}
} else {
return `"mutation" must be a valid object`;
}
}
}
}
}
const isValidStringArray = function(array): boolean {
return Array.isArray(array)
? !array.some(s => typeof s !== 'string' || s.trim().length < 1)
: false;
};
/**
* Ensures the obj is a simple JSON/{}
* object, i.e. not an array, null, date
* etc.
*/
const isValidSimpleObject = function(obj): boolean {
return (
typeof obj === 'object' &&
!Array.isArray(obj) &&
obj !== null &&
obj instanceof Date !== true &&
obj instanceof Promise !== true
);
};
export interface ParseGraphQLConfig {
enabledForClasses?: string[];
disabledForClasses?: string[];
classConfigs?: ParseGraphQLClassConfig[];
}
export interface ParseGraphQLClassConfig {
className: string;
/* The `type` object contains options for how the class types are generated */
type: ?{
/* Fields that are allowed when creating or updating an object. */
inputFields: ?{
/* Leave blank to allow all available fields in the schema. */
create?: string[],
update?: string[],
},
/* Fields on the edges that can be resolved from a query, i.e. the Result Type. */
outputFields: ?(string[]),
/* Fields by which a query can be filtered, i.e. the `where` object. */
constraintFields: ?(string[]),
/* Fields by which a query can be sorted; */
sortFields: ?({
field: string,
asc: boolean,
desc: boolean,
}[]),
};
/* The `query` object contains options for which class queries are generated */
query: ?{
get: ?boolean,
find: ?boolean,
};
/* The `mutation` object contains options for which class mutations are generated */
mutation: ?{
create: ?boolean,
update: ?boolean,
// delete is a reserved key word in js
destroy: ?boolean,
};
}
export default ParseGraphQLController;
export { GraphQLConfigClassName, GraphQLConfigId, GraphQLConfigKey };

View File

@@ -132,6 +132,10 @@ const defaultColumns: { [string]: SchemaFields } = Object.freeze({
objectId: { type: 'String' },
params: { type: 'Object' },
},
_GraphQLConfig: {
objectId: { type: 'String' },
config: { type: 'Object' },
},
_Audience: {
objectId: { type: 'String' },
name: { type: 'String' },
@@ -163,6 +167,7 @@ const volatileClasses = Object.freeze([
'_PushStatus',
'_Hooks',
'_GlobalConfig',
'_GraphQLConfig',
'_JobSchedule',
'_Audience',
]);
@@ -475,6 +480,10 @@ const _GlobalConfigSchema = {
className: '_GlobalConfig',
fields: defaultColumns._GlobalConfig,
};
const _GraphQLConfigSchema = {
className: '_GraphQLConfig',
fields: defaultColumns._GraphQLConfig,
};
const _PushStatusSchema = convertSchemaToAdapterSchema(
injectDefaultSchema({
className: '_PushStatus',
@@ -509,6 +518,7 @@ const VolatileClassesSchemas = [
_JobScheduleSchema,
_PushStatusSchema,
_GlobalConfigSchema,
_GraphQLConfigSchema,
_AudienceSchema,
];

View File

@@ -25,6 +25,7 @@ import { AnalyticsAdapter } from '../Adapters/Analytics/AnalyticsAdapter';
import MongoStorageAdapter from '../Adapters/Storage/Mongo/MongoStorageAdapter';
import PostgresStorageAdapter from '../Adapters/Storage/Postgres/PostgresStorageAdapter';
import ParsePushAdapter from '@parse/push-adapter';
import ParseGraphQLController from './ParseGraphQLController';
export function getControllers(options: ParseServerOptions) {
const loggerController = getLoggerController(options);
@@ -43,6 +44,10 @@ export function getControllers(options: ParseServerOptions) {
const databaseController = getDatabaseController(options, cacheController);
const hooksController = getHooksController(options, databaseController);
const authDataManager = getAuthDataManager(options);
const parseGraphQLController = getParseGraphQLController(options, {
databaseController,
cacheController,
});
return {
loggerController,
filesController,
@@ -54,6 +59,7 @@ export function getControllers(options: ParseServerOptions) {
pushControllerQueue,
analyticsController,
cacheController,
parseGraphQLController,
liveQueryController,
databaseController,
hooksController,
@@ -123,6 +129,16 @@ export function getCacheController(
return new CacheController(cacheControllerAdapter, appId);
}
export function getParseGraphQLController(
options: ParseServerOptions,
controllerDeps
): ParseGraphQLController {
return new ParseGraphQLController({
mountGraphQL: options.mountGraphQL,
...controllerDeps,
});
}
export function getAnalyticsController(
options: ParseServerOptions
): AnalyticsController {

View File

@@ -8,36 +8,57 @@ import * as parseClassQueries from './loaders/parseClassQueries';
import * as parseClassMutations from './loaders/parseClassMutations';
import * as defaultGraphQLQueries from './loaders/defaultGraphQLQueries';
import * as defaultGraphQLMutations from './loaders/defaultGraphQLMutations';
import ParseGraphQLController, {
ParseGraphQLConfig,
} from '../Controllers/ParseGraphQLController';
import DatabaseController from '../Controllers/DatabaseController';
import { toGraphQLError } from './parseGraphQLUtils';
import * as schemaDirectives from './loaders/schemaDirectives';
class ParseGraphQLSchema {
constructor(databaseController, log, graphQLCustomTypeDefs) {
databaseController: DatabaseController;
parseGraphQLController: ParseGraphQLController;
parseGraphQLConfig: ParseGraphQLConfig;
graphQLCustomTypeDefs: any;
constructor(
params: {
databaseController: DatabaseController,
parseGraphQLController: ParseGraphQLController,
log: any,
} = {}
) {
this.parseGraphQLController =
params.parseGraphQLController ||
requiredParameter('You must provide a parseGraphQLController instance!');
this.databaseController =
databaseController ||
params.databaseController ||
requiredParameter('You must provide a databaseController instance!');
this.log = log || requiredParameter('You must provide a log instance!');
this.graphQLCustomTypeDefs = graphQLCustomTypeDefs;
this.log =
params.log || requiredParameter('You must provide a log instance!');
this.graphQLCustomTypeDefs = params.graphQLCustomTypeDefs;
}
async load() {
const schemaController = await this.databaseController.loadSchema();
const parseClasses = await schemaController.getAllClasses();
const { parseGraphQLConfig } = await this._initializeSchemaAndConfig();
const parseClasses = await this._getClassesForSchema(parseGraphQLConfig);
const parseClassesString = JSON.stringify(parseClasses);
if (this.graphQLSchema) {
if (this.parseClasses === parseClasses) {
return this.graphQLSchema;
}
if (this.parseClassesString === parseClassesString) {
this.parseClasses = parseClasses;
return this.graphQLSchema;
}
if (
this.graphQLSchema &&
!this._hasSchemaInputChanged({
parseClasses,
parseClassesString,
parseGraphQLConfig,
})
) {
return this.graphQLSchema;
}
this.parseClasses = parseClasses;
this.parseClassesString = parseClassesString;
this.parseGraphQLConfig = parseGraphQLConfig;
this.parseClassTypes = {};
this.meType = null;
this.graphQLAutoSchema = null;
@@ -53,16 +74,15 @@ class ParseGraphQLSchema {
defaultGraphQLTypes.load(this);
parseClasses.forEach(parseClass => {
parseClassTypes.load(this, parseClass);
parseClassQueries.load(this, parseClass);
parseClassMutations.load(this, parseClass);
});
this._getParseClassesWithConfig(parseClasses, parseGraphQLConfig).forEach(
([parseClass, parseClassConfig]) => {
parseClassTypes.load(this, parseClass, parseClassConfig);
parseClassQueries.load(this, parseClass, parseClassConfig);
parseClassMutations.load(this, parseClass, parseClassConfig);
}
);
defaultGraphQLQueries.load(this);
defaultGraphQLMutations.load(this);
let graphQLQuery = undefined;
@@ -160,6 +180,104 @@ class ParseGraphQLSchema {
}
throw toGraphQLError(error);
}
async _initializeSchemaAndConfig() {
const [schemaController, parseGraphQLConfig] = await Promise.all([
this.databaseController.loadSchema(),
this.parseGraphQLController.getGraphQLConfig(),
]);
this.schemaController = schemaController;
return {
parseGraphQLConfig,
};
}
/**
* Gets all classes found by the `schemaController`
* minus those filtered out by the app's parseGraphQLConfig.
*/
async _getClassesForSchema(parseGraphQLConfig: ParseGraphQLConfig) {
const { enabledForClasses, disabledForClasses } = parseGraphQLConfig;
const allClasses = await this.schemaController.getAllClasses();
if (Array.isArray(enabledForClasses) || Array.isArray(disabledForClasses)) {
let includedClasses = allClasses;
if (enabledForClasses) {
includedClasses = allClasses.filter(clazz => {
return enabledForClasses.includes(clazz.className);
});
}
if (disabledForClasses) {
// Classes included in `enabledForClasses` that
// are also present in `disabledForClasses` will
// still be filtered out
includedClasses = includedClasses.filter(clazz => {
return !disabledForClasses.includes(clazz.className);
});
}
this.isUsersClassDisabled = !includedClasses.some(clazz => {
return clazz.className === '_User';
});
return includedClasses;
} else {
return allClasses;
}
}
/**
* This method returns a list of tuples
* that provide the parseClass along with
* its parseClassConfig where provided.
*/
_getParseClassesWithConfig(
parseClasses,
parseGraphQLConfig: ParseGraphQLConfig
) {
const { classConfigs } = parseGraphQLConfig;
return parseClasses.map(parseClass => {
let parseClassConfig;
if (classConfigs) {
parseClassConfig = classConfigs.find(
c => c.className === parseClass.className
);
}
return [parseClass, parseClassConfig];
});
}
/**
* Checks for changes to the parseClasses
* objects (i.e. database schema) or to
* the parseGraphQLConfig object. If no
* changes are found, return true;
*/
_hasSchemaInputChanged(params: {
parseClasses: any,
parseClassesString: string,
parseGraphQLConfig: ?ParseGraphQLConfig,
}): boolean {
const { parseClasses, parseClassesString, parseGraphQLConfig } = params;
if (
JSON.stringify(this.parseGraphQLConfig) ===
JSON.stringify(parseGraphQLConfig)
) {
if (this.parseClasses === parseClasses) {
return false;
}
if (this.parseClassesString === parseClassesString) {
this.parseClasses = parseClasses;
return false;
}
}
return true;
}
}
export { ParseGraphQLSchema };

View File

@@ -9,8 +9,13 @@ import { handleParseErrors, handleParseHeaders } from '../middlewares';
import requiredParameter from '../requiredParameter';
import defaultLogger from '../logger';
import { ParseGraphQLSchema } from './ParseGraphQLSchema';
import ParseGraphQLController, {
ParseGraphQLConfig,
} from '../Controllers/ParseGraphQLController';
class ParseGraphQLServer {
parseGraphQLController: ParseGraphQLController;
constructor(parseServer, config) {
this.parseServer =
parseServer ||
@@ -19,12 +24,15 @@ class ParseGraphQLServer {
requiredParameter('You must provide a config.graphQLPath!');
}
this.config = config;
this.parseGraphQLSchema = new ParseGraphQLSchema(
this.parseServer.config.databaseController,
(this.parseServer.config && this.parseServer.config.loggerController) ||
this.parseGraphQLController = this.parseServer.config.parseGraphQLController;
this.parseGraphQLSchema = new ParseGraphQLSchema({
parseGraphQLController: this.parseGraphQLController,
databaseController: this.parseServer.config.databaseController,
log:
(this.parseServer.config && this.parseServer.config.loggerController) ||
defaultLogger,
this.config.graphQLCustomTypeDefs
);
graphQLCustomTypeDefs: this.config.graphQLCustomTypeDefs,
});
}
async _getGraphQLOptions(req) {
@@ -111,6 +119,10 @@ class ParseGraphQLServer {
}
);
}
setGraphQLConfig(graphQLConfig: ParseGraphQLConfig): Promise {
return this.parseGraphQLController.updateGraphQLConfig(graphQLConfig);
}
}
export { ParseGraphQLServer };

View File

@@ -1,23 +1,58 @@
import { GraphQLNonNull, GraphQLBoolean } from 'graphql';
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
import * as objectsMutations from './objectsMutations';
import { ParseGraphQLClassConfig } from '../../Controllers/ParseGraphQLController';
const load = (parseGraphQLSchema, parseClass) => {
const className = parseClass.className;
const getParseClassMutationConfig = function(
parseClassConfig: ?ParseGraphQLClassConfig
) {
return (parseClassConfig && parseClassConfig.mutation) || {};
};
const classGraphQLInputType =
parseGraphQLSchema.parseClassTypes[className].classGraphQLInputType;
const fields = {
description: 'These are the fields of the object.',
type: classGraphQLInputType,
const load = function(
parseGraphQLSchema,
parseClass,
parseClassConfig: ?ParseGraphQLClassConfig
) {
const { className } = parseClass;
const {
create: isCreateEnabled = true,
update: isUpdateEnabled = true,
destroy: isDestroyEnabled = true,
} = getParseClassMutationConfig(parseClassConfig);
const {
classGraphQLCreateType,
classGraphQLUpdateType,
} = parseGraphQLSchema.parseClassTypes[className];
const createFields = {
description: 'These are the fields used to create the object.',
type: classGraphQLCreateType,
};
const updateFields = {
description: 'These are the fields used to update the object.',
type: classGraphQLUpdateType,
};
const classGraphQLInputTypeFields = classGraphQLInputType.getFields();
const transformTypes = fields => {
const classGraphQLCreateTypeFields = isCreateEnabled
? classGraphQLCreateType.getFields()
: null;
const classGraphQLUpdateTypeFields = isUpdateEnabled
? classGraphQLUpdateType.getFields()
: null;
const transformTypes = (inputType: 'create' | 'update', fields) => {
if (fields) {
Object.keys(fields).forEach(field => {
if (classGraphQLInputTypeFields[field]) {
switch (classGraphQLInputTypeFields[field].type) {
let inputTypeField;
if (inputType === 'create') {
inputTypeField = classGraphQLCreateTypeFields[field];
} else {
inputTypeField = classGraphQLUpdateTypeFields[field];
}
if (inputTypeField) {
switch (inputTypeField.type) {
case defaultGraphQLTypes.GEO_POINT:
fields[field].__type = 'GeoPoint';
break;
@@ -36,86 +71,92 @@ const load = (parseGraphQLSchema, parseClass) => {
}
};
const createGraphQLMutationName = `create${className}`;
parseGraphQLSchema.graphQLObjectsMutations[createGraphQLMutationName] = {
description: `The ${createGraphQLMutationName} mutation can be used to create a new object of the ${className} class.`,
args: {
fields,
},
type: new GraphQLNonNull(defaultGraphQLTypes.CREATE_RESULT),
async resolve(_source, args, context) {
try {
const { fields } = args;
const { config, auth, info } = context;
if (isCreateEnabled) {
const createGraphQLMutationName = `create${className}`;
parseGraphQLSchema.graphQLObjectsMutations[createGraphQLMutationName] = {
description: `The ${createGraphQLMutationName} mutation can be used to create a new object of the ${className} class.`,
args: {
fields: createFields,
},
type: new GraphQLNonNull(defaultGraphQLTypes.CREATE_RESULT),
async resolve(_source, args, context) {
try {
const { fields } = args;
const { config, auth, info } = context;
transformTypes(fields);
transformTypes('create', fields);
return await objectsMutations.createObject(
className,
fields,
config,
auth,
info
);
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
};
return await objectsMutations.createObject(
className,
fields,
config,
auth,
info
);
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
};
}
const updateGraphQLMutationName = `update${className}`;
parseGraphQLSchema.graphQLObjectsMutations[updateGraphQLMutationName] = {
description: `The ${updateGraphQLMutationName} mutation can be used to update an object of the ${className} class.`,
args: {
objectId: defaultGraphQLTypes.OBJECT_ID_ATT,
fields,
},
type: defaultGraphQLTypes.UPDATE_RESULT,
async resolve(_source, args, context) {
try {
const { objectId, fields } = args;
const { config, auth, info } = context;
if (isUpdateEnabled) {
const updateGraphQLMutationName = `update${className}`;
parseGraphQLSchema.graphQLObjectsMutations[updateGraphQLMutationName] = {
description: `The ${updateGraphQLMutationName} mutation can be used to update an object of the ${className} class.`,
args: {
objectId: defaultGraphQLTypes.OBJECT_ID_ATT,
fields: updateFields,
},
type: defaultGraphQLTypes.UPDATE_RESULT,
async resolve(_source, args, context) {
try {
const { objectId, fields } = args;
const { config, auth, info } = context;
transformTypes(fields);
transformTypes('update', fields);
return await objectsMutations.updateObject(
className,
objectId,
fields,
config,
auth,
info
);
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
};
return await objectsMutations.updateObject(
className,
objectId,
fields,
config,
auth,
info
);
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
};
}
const deleteGraphQLMutationName = `delete${className}`;
parseGraphQLSchema.graphQLObjectsMutations[deleteGraphQLMutationName] = {
description: `The ${deleteGraphQLMutationName} mutation can be used to delete an object of the ${className} class.`,
args: {
objectId: defaultGraphQLTypes.OBJECT_ID_ATT,
},
type: new GraphQLNonNull(GraphQLBoolean),
async resolve(_source, args, context) {
try {
const { objectId } = args;
const { config, auth, info } = context;
if (isDestroyEnabled) {
const deleteGraphQLMutationName = `delete${className}`;
parseGraphQLSchema.graphQLObjectsMutations[deleteGraphQLMutationName] = {
description: `The ${deleteGraphQLMutationName} mutation can be used to delete an object of the ${className} class.`,
args: {
objectId: defaultGraphQLTypes.OBJECT_ID_ATT,
},
type: new GraphQLNonNull(GraphQLBoolean),
async resolve(_source, args, context) {
try {
const { objectId } = args;
const { config, auth, info } = context;
return await objectsMutations.deleteObject(
className,
objectId,
config,
auth,
info
);
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
};
return await objectsMutations.deleteObject(
className,
objectId,
config,
auth,
info
);
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
};
}
};
export { load };

View File

@@ -3,9 +3,24 @@ import getFieldNames from 'graphql-list-fields';
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
import * as objectsQueries from './objectsQueries';
import * as parseClassTypes from './parseClassTypes';
import { ParseGraphQLClassConfig } from '../../Controllers/ParseGraphQLController';
const load = (parseGraphQLSchema, parseClass) => {
const className = parseClass.className;
const getParseClassQueryConfig = function(
parseClassConfig: ?ParseGraphQLClassConfig
) {
return (parseClassConfig && parseClassConfig.query) || {};
};
const load = function(
parseGraphQLSchema,
parseClass,
parseClassConfig: ?ParseGraphQLClassConfig
) {
const { className } = parseClass;
const {
get: isGetEnabled = true,
find: isFindEnabled = true,
} = getParseClassQueryConfig(parseClassConfig);
const {
classGraphQLOutputType,
@@ -13,90 +28,94 @@ const load = (parseGraphQLSchema, parseClass) => {
classGraphQLFindResultType,
} = parseGraphQLSchema.parseClassTypes[className];
const getGraphQLQueryName = `get${className}`;
parseGraphQLSchema.graphQLObjectsQueries[getGraphQLQueryName] = {
description: `The ${getGraphQLQueryName} query can be used to get an object of the ${className} class by its id.`,
args: {
objectId: defaultGraphQLTypes.OBJECT_ID_ATT,
readPreference: defaultGraphQLTypes.READ_PREFERENCE_ATT,
includeReadPreference: defaultGraphQLTypes.INCLUDE_READ_PREFERENCE_ATT,
},
type: new GraphQLNonNull(classGraphQLOutputType),
async resolve(_source, args, context, queryInfo) {
try {
const { objectId, readPreference, includeReadPreference } = args;
const { config, auth, info } = context;
const selectedFields = getFieldNames(queryInfo);
if (isGetEnabled) {
const getGraphQLQueryName = `get${className}`;
parseGraphQLSchema.graphQLObjectsQueries[getGraphQLQueryName] = {
description: `The ${getGraphQLQueryName} query can be used to get an object of the ${className} class by its id.`,
args: {
objectId: defaultGraphQLTypes.OBJECT_ID_ATT,
readPreference: defaultGraphQLTypes.READ_PREFERENCE_ATT,
includeReadPreference: defaultGraphQLTypes.INCLUDE_READ_PREFERENCE_ATT,
},
type: new GraphQLNonNull(classGraphQLOutputType),
async resolve(_source, args, context, queryInfo) {
try {
const { objectId, readPreference, includeReadPreference } = args;
const { config, auth, info } = context;
const selectedFields = getFieldNames(queryInfo);
const { keys, include } = parseClassTypes.extractKeysAndInclude(
selectedFields
);
const { keys, include } = parseClassTypes.extractKeysAndInclude(
selectedFields
);
return await objectsQueries.getObject(
className,
objectId,
keys,
include,
readPreference,
includeReadPreference,
config,
auth,
info
);
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
};
return await objectsQueries.getObject(
className,
objectId,
keys,
include,
readPreference,
includeReadPreference,
config,
auth,
info
);
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
};
}
const findGraphQLQueryName = `find${className}`;
parseGraphQLSchema.graphQLObjectsQueries[findGraphQLQueryName] = {
description: `The ${findGraphQLQueryName} query can be used to find objects of the ${className} class.`,
args: classGraphQLFindArgs,
type: new GraphQLNonNull(classGraphQLFindResultType),
async resolve(_source, args, context, queryInfo) {
try {
const {
where,
order,
skip,
limit,
readPreference,
includeReadPreference,
subqueryReadPreference,
} = args;
const { config, auth, info } = context;
const selectedFields = getFieldNames(queryInfo);
if (isFindEnabled) {
const findGraphQLQueryName = `find${className}`;
parseGraphQLSchema.graphQLObjectsQueries[findGraphQLQueryName] = {
description: `The ${findGraphQLQueryName} query can be used to find objects of the ${className} class.`,
args: classGraphQLFindArgs,
type: new GraphQLNonNull(classGraphQLFindResultType),
async resolve(_source, args, context, queryInfo) {
try {
const {
where,
order,
skip,
limit,
readPreference,
includeReadPreference,
subqueryReadPreference,
} = args;
const { config, auth, info } = context;
const selectedFields = getFieldNames(queryInfo);
const { keys, include } = parseClassTypes.extractKeysAndInclude(
selectedFields
.filter(field => field.includes('.'))
.map(field => field.slice(field.indexOf('.') + 1))
);
const parseOrder = order && order.join(',');
const { keys, include } = parseClassTypes.extractKeysAndInclude(
selectedFields
.filter(field => field.includes('.'))
.map(field => field.slice(field.indexOf('.') + 1))
);
const parseOrder = order && order.join(',');
return await objectsQueries.findObjects(
className,
where,
parseOrder,
skip,
limit,
keys,
include,
false,
readPreference,
includeReadPreference,
subqueryReadPreference,
config,
auth,
info,
selectedFields.map(field => field.split('.', 1)[0])
);
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
};
return await objectsQueries.findObjects(
className,
where,
parseOrder,
skip,
limit,
keys,
include,
false,
readPreference,
includeReadPreference,
subqueryReadPreference,
config,
auth,
info,
selectedFields.map(field => field.split('.', 1)[0])
);
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
};
}
};
export { load };

View File

@@ -13,6 +13,7 @@ import {
import getFieldNames from 'graphql-list-fields';
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
import * as objectsQueries from './objectsQueries';
import { ParseGraphQLClassConfig } from '../../Controllers/ParseGraphQLController';
const mapInputType = (parseType, targetClass, parseClassTypes) => {
switch (parseType) {
@@ -161,14 +162,105 @@ const extractKeysAndInclude = selectedFields => {
return { keys, include };
};
const load = (parseGraphQLSchema, parseClass) => {
const className = parseClass.className;
const getParseClassTypeConfig = function(
parseClassConfig: ?ParseGraphQLClassConfig
) {
return (parseClassConfig && parseClassConfig.type) || {};
};
const getInputFieldsAndConstraints = function(
parseClass,
parseClassConfig: ?ParseGraphQLClassConfig
) {
const classFields = Object.keys(parseClass.fields);
const {
inputFields: allowedInputFields,
outputFields: allowedOutputFields,
constraintFields: allowedConstraintFields,
sortFields: allowedSortFields,
} = getParseClassTypeConfig(parseClassConfig);
const classCustomFields = classFields.filter(
field => !Object.keys(defaultGraphQLTypes.CLASS_FIELDS).includes(field)
);
let classOutputFields;
let classCreateFields;
let classUpdateFields;
let classConstraintFields;
let classSortFields;
// All allowed customs fields
const classCustomFields = classFields.filter(field => {
return !Object.keys(defaultGraphQLTypes.CLASS_FIELDS).includes(field);
});
if (allowedInputFields && allowedInputFields.create) {
classCreateFields = classCustomFields.filter(field => {
return allowedInputFields.create.includes(field);
});
} else {
classCreateFields = classCustomFields;
}
if (allowedInputFields && allowedInputFields.update) {
classUpdateFields = classCustomFields.filter(field => {
return allowedInputFields.update.includes(field);
});
} else {
classUpdateFields = classCustomFields;
}
if (allowedOutputFields) {
classOutputFields = classCustomFields.filter(field => {
return allowedOutputFields.includes(field);
});
} else {
classOutputFields = classCustomFields;
}
if (allowedConstraintFields) {
classConstraintFields = classCustomFields.filter(field => {
return allowedConstraintFields.includes(field);
});
} else {
classConstraintFields = classFields;
}
if (allowedSortFields) {
classSortFields = allowedSortFields;
if (!classSortFields.length) {
// must have at least 1 order field
// otherwise the FindArgs Input Type will throw.
classSortFields.push({
field: 'objectId',
asc: true,
desc: true,
});
}
} else {
classSortFields = classFields.map(field => {
return { field, asc: true, desc: true };
});
}
return {
classCreateFields,
classUpdateFields,
classConstraintFields,
classOutputFields,
classSortFields,
};
};
const load = (
parseGraphQLSchema,
parseClass,
parseClassConfig: ?ParseGraphQLClassConfig
) => {
const { className } = parseClass;
const {
classCreateFields,
classUpdateFields,
classOutputFields,
classConstraintFields,
classSortFields,
} = getInputFieldsAndConstraints(parseClass, parseClassConfig);
const classGraphQLScalarTypeName = `${className}Pointer`;
const parseScalarValue = value => {
@@ -271,12 +363,12 @@ const load = (parseGraphQLSchema, parseClass) => {
});
parseGraphQLSchema.graphQLTypes.push(classGraphQLRelationOpType);
const classGraphQLInputTypeName = `${className}Fields`;
const classGraphQLInputType = new GraphQLInputObjectType({
name: classGraphQLInputTypeName,
description: `The ${classGraphQLInputTypeName} input type is used in operations that involve inputting objects of ${className} class.`,
const classGraphQLCreateTypeName = `${className}CreateFields`;
const classGraphQLCreateType = new GraphQLInputObjectType({
name: classGraphQLCreateTypeName,
description: `The ${classGraphQLCreateTypeName} input type is used in operations that involve creation of objects in the ${className} class.`,
fields: () =>
classCustomFields.reduce(
classCreateFields.reduce(
(fields, field) => {
const type = mapInputType(
parseClass.fields[field].type,
@@ -300,7 +392,38 @@ const load = (parseGraphQLSchema, parseClass) => {
}
),
});
parseGraphQLSchema.graphQLTypes.push(classGraphQLInputType);
parseGraphQLSchema.graphQLTypes.push(classGraphQLCreateType);
const classGraphQLUpdateTypeName = `${className}UpdateFields`;
const classGraphQLUpdateType = new GraphQLInputObjectType({
name: classGraphQLUpdateTypeName,
description: `The ${classGraphQLUpdateTypeName} input type is used in operations that involve creation of objects in the ${className} class.`,
fields: () =>
classUpdateFields.reduce(
(fields, field) => {
const type = mapInputType(
parseClass.fields[field].type,
parseClass.fields[field].targetClass,
parseGraphQLSchema.parseClassTypes
);
if (type) {
return {
...fields,
[field]: {
description: `This is the object ${field}.`,
type,
},
};
} else {
return fields;
}
},
{
ACL: defaultGraphQLTypes.ACL_ATT,
}
),
});
parseGraphQLSchema.graphQLTypes.push(classGraphQLUpdateType);
const classGraphQLConstraintTypeName = `${className}PointerConstraint`;
const classGraphQLConstraintType = new GraphQLInputObjectType({
@@ -333,7 +456,7 @@ const load = (parseGraphQLSchema, parseClass) => {
name: classGraphQLConstraintsTypeName,
description: `The ${classGraphQLConstraintsTypeName} input type is used in operations that involve filtering objects of ${className} class.`,
fields: () => ({
...classFields.reduce((fields, field) => {
...classConstraintFields.reduce((fields, field) => {
const type = mapConstraintType(
parseClass.fields[field].type,
parseClass.fields[field].targetClass,
@@ -371,12 +494,18 @@ const load = (parseGraphQLSchema, parseClass) => {
const classGraphQLOrderType = new GraphQLEnumType({
name: classGraphQLOrderTypeName,
description: `The ${classGraphQLOrderTypeName} input type is used when sorting objects of the ${className} class.`,
values: classFields.reduce((orderFields, field) => {
return {
...orderFields,
[`${field}_ASC`]: { value: field },
[`${field}_DESC`]: { value: `-${field}` },
values: classSortFields.reduce((sortFields, fieldConfig) => {
const { field, asc, desc } = fieldConfig;
const updatedSortFields = {
...sortFields,
};
if (asc) {
updatedSortFields[`${field}_ASC`] = { value: field };
}
if (desc) {
updatedSortFields[`${field}_DESC`] = { value: `-${field}` };
}
return updatedSortFields;
}, {}),
});
parseGraphQLSchema.graphQLTypes.push(classGraphQLOrderType);
@@ -400,7 +529,7 @@ const load = (parseGraphQLSchema, parseClass) => {
const classGraphQLOutputTypeName = `${className}Class`;
const outputFields = () => {
return classCustomFields.reduce((fields, field) => {
return classOutputFields.reduce((fields, field) => {
const type = mapOutputType(
parseClass.fields[field].type,
parseClass.fields[field].targetClass,
@@ -531,7 +660,8 @@ const load = (parseGraphQLSchema, parseClass) => {
parseGraphQLSchema.parseClassTypes[className] = {
classGraphQLScalarType,
classGraphQLRelationOpType,
classGraphQLInputType,
classGraphQLCreateType,
classGraphQLUpdateType,
classGraphQLConstraintType,
classGraphQLConstraintsType,
classGraphQLFindArgs,
@@ -552,37 +682,32 @@ const load = (parseGraphQLSchema, parseClass) => {
parseGraphQLSchema.meType = meType;
parseGraphQLSchema.graphQLTypes.push(meType);
const userSignUpInputTypeName = `_UserSignUpFields`;
const userSignUpInputTypeName = '_UserSignUpFields';
const userSignUpInputType = new GraphQLInputObjectType({
name: userSignUpInputTypeName,
description: `The ${userSignUpInputTypeName} input type is used in operations that involve inputting objects of ${className} class when signing up.`,
fields: () =>
classCustomFields.reduce(
(fields, field) => {
const type = mapInputType(
parseClass.fields[field].type,
parseClass.fields[field].targetClass,
parseGraphQLSchema.parseClassTypes
);
if (type) {
return {
...fields,
[field]: {
description: `This is the object ${field}.`,
type:
field === 'username' || field === 'password'
? new GraphQLNonNull(type)
: type,
},
};
} else {
return fields;
}
},
{
ACL: defaultGraphQLTypes.ACL_ATT,
classCreateFields.reduce((fields, field) => {
const type = mapInputType(
parseClass.fields[field].type,
parseClass.fields[field].targetClass,
parseGraphQLSchema.parseClassTypes
);
if (type) {
return {
...fields,
[field]: {
description: `This is the object ${field}.`,
type:
field === 'username' || field === 'password'
? new GraphQLNonNull(type)
: type,
},
};
} else {
return fields;
}
),
}, {}),
});
parseGraphQLSchema.parseClassTypes[
'_User'

View File

@@ -11,6 +11,9 @@ import * as objectsMutations from './objectsMutations';
const usersRouter = new UsersRouter();
const load = parseGraphQLSchema => {
if (parseGraphQLSchema.isUsersClassDisabled) {
return;
}
const fields = {};
fields.signUp = {

View File

@@ -6,6 +6,9 @@ import Auth from '../../Auth';
import { extractKeysAndInclude } from './parseClassTypes';
const load = parseGraphQLSchema => {
if (parseGraphQLSchema.isUsersClassDisabled) {
return;
}
const fields = {};
fields.me = {

View File

@@ -21,6 +21,7 @@ import { FeaturesRouter } from './Routers/FeaturesRouter';
import { FilesRouter } from './Routers/FilesRouter';
import { FunctionsRouter } from './Routers/FunctionsRouter';
import { GlobalConfigRouter } from './Routers/GlobalConfigRouter';
import { GraphQLRouter } from './Routers/GraphQLRouter';
import { HooksRouter } from './Routers/HooksRouter';
import { IAPValidationRouter } from './Routers/IAPValidationRouter';
import { InstallationsRouter } from './Routers/InstallationsRouter';
@@ -231,6 +232,7 @@ class ParseServer {
new IAPValidationRouter(),
new FeaturesRouter(),
new GlobalConfigRouter(),
new GraphQLRouter(),
new PurgeRouter(),
new HooksRouter(),
new CloudCodeRouter(),

View File

@@ -0,0 +1,50 @@
import Parse from 'parse/node';
import PromiseRouter from '../PromiseRouter';
import * as middleware from '../middlewares';
const GraphQLConfigPath = '/graphql-config';
export class GraphQLRouter extends PromiseRouter {
async getGraphQLConfig(req) {
const result = await req.config.parseGraphQLController.getGraphQLConfig();
return {
response: result,
};
}
async updateGraphQLConfig(req) {
if (req.auth.isReadOnly) {
throw new Parse.Error(
Parse.Error.OPERATION_FORBIDDEN,
"read-only masterKey isn't allowed to update the GraphQL config."
);
}
const data = await req.config.parseGraphQLController.updateGraphQLConfig(
req.body.params
);
return {
response: data,
};
}
mountRoutes() {
this.route(
'GET',
GraphQLConfigPath,
middleware.promiseEnforceMasterKeyAccess,
req => {
return this.getGraphQLConfig(req);
}
);
this.route(
'PUT',
GraphQLConfigPath,
middleware.promiseEnforceMasterKeyAccess,
req => {
return this.updateGraphQLConfig(req);
}
);
}
}
export default GraphQLRouter;