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', () => { describe('ParseGraphQLSchema', () => {
let parseServer; let parseServer;
let databaseController; let databaseController;
let parseGraphQLController;
let parseGraphQLSchema; let parseGraphQLSchema;
beforeAll(async () => { beforeAll(async () => {
@@ -11,28 +12,37 @@ describe('ParseGraphQLSchema', () => {
schemaCacheTTL: 100, schemaCacheTTL: 100,
}); });
databaseController = parseServer.config.databaseController; databaseController = parseServer.config.databaseController;
parseGraphQLSchema = new ParseGraphQLSchema( parseGraphQLController = parseServer.config.parseGraphQLController;
parseGraphQLSchema = new ParseGraphQLSchema({
databaseController, databaseController,
defaultLogger parseGraphQLController,
); log: defaultLogger,
});
}); });
describe('constructor', () => { 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( expect(() => new ParseGraphQLSchema()).toThrow(
'You must provide a databaseController instance!' 'You must provide a parseGraphQLController instance!'
); );
expect(() => new ParseGraphQLSchema({})).toThrow( expect(
'You must provide a log instance!' () => new ParseGraphQLSchema({ parseGraphQLController: {} })
); ).toThrow('You must provide a databaseController instance!');
expect(() => new ParseGraphQLSchema({}, {})).not.toThrow(); expect(
() =>
new ParseGraphQLSchema({
parseGraphQLController: {},
databaseController: {},
})
).toThrow('You must provide a log instance!');
}); });
}); });
describe('load', () => { describe('load', () => {
it('should cache schema', async () => { it('should cache schema', async () => {
const graphQLSchema = await parseGraphQLSchema.load(); 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)); await new Promise(resolve => setTimeout(resolve, 200));
expect(graphQLSchema).toBe(await parseGraphQLSchema.load()); 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 () => { it('should load a brand new GraphQL Schema if Parse Schema changes', async () => {
await parseGraphQLSchema.load(); await parseGraphQLSchema.load();
const parseClasses = parseGraphQLSchema.parseClasses; const parseClasses = parseGraphQLSchema.parseClasses;
const parseClassesString = parseGraphQLSchema.parseClasses; const parseClassesString = parseGraphQLSchema.parseClassesString;
const parseClassTypes = parseGraphQLSchema.parseClasses; const parseClassTypes = parseGraphQLSchema.parseClassTypes;
const graphQLSchema = parseGraphQLSchema.parseClasses; const graphQLSchema = parseGraphQLSchema.graphQLSchema;
const graphQLTypes = parseGraphQLSchema.parseClasses; const graphQLTypes = parseGraphQLSchema.graphQLTypes;
const graphQLQueries = parseGraphQLSchema.parseClasses; const graphQLQueries = parseGraphQLSchema.graphQLQueries;
const graphQLMutations = parseGraphQLSchema.parseClasses; const graphQLMutations = parseGraphQLSchema.graphQLMutations;
const graphQLSubscriptions = parseGraphQLSchema.parseClasses; const graphQLSubscriptions = parseGraphQLSchema.graphQLSubscriptions;
const newClassObject = new Parse.Object('NewClass'); const newClassObject = new Parse.Object('NewClass');
await newClassObject.save(); await newClassObject.save();
await databaseController.schemaCache.clear(); await databaseController.schemaCache.clear();
await new Promise(resolve => setTimeout(resolve, 200)); await new Promise(resolve => setTimeout(resolve, 200));
await parseGraphQLSchema.load(); await parseGraphQLSchema.load();
expect(parseClasses).not.toBe(parseGraphQLSchema.parseClasses); expect(parseClasses).not.toBe(parseGraphQLSchema.parseClasses);
expect(parseClassesString).not.toBe(parseGraphQLSchema.parseClasses); expect(parseClassesString).not.toBe(
expect(parseClassTypes).not.toBe(parseGraphQLSchema.parseClasses); parseGraphQLSchema.parseClassesString
expect(graphQLSchema).not.toBe(parseGraphQLSchema.parseClasses); );
expect(graphQLTypes).not.toBe(parseGraphQLSchema.parseClasses); expect(parseClassTypes).not.toBe(parseGraphQLSchema.parseClassTypes);
expect(graphQLQueries).not.toBe(parseGraphQLSchema.parseClasses); expect(graphQLSchema).not.toBe(parseGraphQLSchema.graphQLSchema);
expect(graphQLMutations).not.toBe(parseGraphQLSchema.parseClasses); expect(graphQLTypes).not.toBe(parseGraphQLSchema.graphQLTypes);
expect(graphQLSubscriptions).not.toBe(parseGraphQLSchema.parseClasses); 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) { switch (key) {
case 'objectId': case 'objectId':
case '_id': case '_id':
if (className === '_GlobalConfig') { if (['_GlobalConfig', '_GraphQLConfig'].includes(className)) {
return { return {
key: key, key: key,
value: parseInt(restValue), value: parseInt(restValue),
@@ -252,7 +252,7 @@ function transformQueryKeyValue(className, key, value, schema, count = false) {
} }
break; break;
case 'objectId': { case 'objectId': {
if (className === '_GlobalConfig') { if (['_GlobalConfig', '_GraphQLConfig'].includes(className)) {
value = parseInt(value); value = parseInt(value);
} }
return { key: '_id', value }; return { key: '_id', value };

View File

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

View File

@@ -45,6 +45,7 @@ export class CacheController extends AdaptableController {
this.role = new SubCache('role', this); this.role = new SubCache('role', this);
this.user = new SubCache('user', this); this.user = new SubCache('user', this);
this.graphQL = new SubCache('graphQL', this);
} }
get(key) { 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' }, objectId: { type: 'String' },
params: { type: 'Object' }, params: { type: 'Object' },
}, },
_GraphQLConfig: {
objectId: { type: 'String' },
config: { type: 'Object' },
},
_Audience: { _Audience: {
objectId: { type: 'String' }, objectId: { type: 'String' },
name: { type: 'String' }, name: { type: 'String' },
@@ -163,6 +167,7 @@ const volatileClasses = Object.freeze([
'_PushStatus', '_PushStatus',
'_Hooks', '_Hooks',
'_GlobalConfig', '_GlobalConfig',
'_GraphQLConfig',
'_JobSchedule', '_JobSchedule',
'_Audience', '_Audience',
]); ]);
@@ -475,6 +480,10 @@ const _GlobalConfigSchema = {
className: '_GlobalConfig', className: '_GlobalConfig',
fields: defaultColumns._GlobalConfig, fields: defaultColumns._GlobalConfig,
}; };
const _GraphQLConfigSchema = {
className: '_GraphQLConfig',
fields: defaultColumns._GraphQLConfig,
};
const _PushStatusSchema = convertSchemaToAdapterSchema( const _PushStatusSchema = convertSchemaToAdapterSchema(
injectDefaultSchema({ injectDefaultSchema({
className: '_PushStatus', className: '_PushStatus',
@@ -509,6 +518,7 @@ const VolatileClassesSchemas = [
_JobScheduleSchema, _JobScheduleSchema,
_PushStatusSchema, _PushStatusSchema,
_GlobalConfigSchema, _GlobalConfigSchema,
_GraphQLConfigSchema,
_AudienceSchema, _AudienceSchema,
]; ];

View File

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

View File

@@ -8,36 +8,57 @@ import * as parseClassQueries from './loaders/parseClassQueries';
import * as parseClassMutations from './loaders/parseClassMutations'; import * as parseClassMutations from './loaders/parseClassMutations';
import * as defaultGraphQLQueries from './loaders/defaultGraphQLQueries'; import * as defaultGraphQLQueries from './loaders/defaultGraphQLQueries';
import * as defaultGraphQLMutations from './loaders/defaultGraphQLMutations'; import * as defaultGraphQLMutations from './loaders/defaultGraphQLMutations';
import ParseGraphQLController, {
ParseGraphQLConfig,
} from '../Controllers/ParseGraphQLController';
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';
class ParseGraphQLSchema { 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 = this.databaseController =
databaseController || params.databaseController ||
requiredParameter('You must provide a databaseController instance!'); requiredParameter('You must provide a databaseController instance!');
this.log = log || requiredParameter('You must provide a log instance!'); this.log =
this.graphQLCustomTypeDefs = graphQLCustomTypeDefs; params.log || requiredParameter('You must provide a log instance!');
this.graphQLCustomTypeDefs = params.graphQLCustomTypeDefs;
} }
async load() { async load() {
const schemaController = await this.databaseController.loadSchema(); const { parseGraphQLConfig } = await this._initializeSchemaAndConfig();
const parseClasses = await schemaController.getAllClasses();
const parseClasses = await this._getClassesForSchema(parseGraphQLConfig);
const parseClassesString = JSON.stringify(parseClasses); const parseClassesString = JSON.stringify(parseClasses);
if (this.graphQLSchema) { if (
if (this.parseClasses === parseClasses) { this.graphQLSchema &&
return this.graphQLSchema; !this._hasSchemaInputChanged({
} parseClasses,
parseClassesString,
if (this.parseClassesString === parseClassesString) { parseGraphQLConfig,
this.parseClasses = parseClasses; })
return this.graphQLSchema; ) {
} return this.graphQLSchema;
} }
this.parseClasses = parseClasses; this.parseClasses = parseClasses;
this.parseClassesString = parseClassesString; this.parseClassesString = parseClassesString;
this.parseGraphQLConfig = parseGraphQLConfig;
this.parseClassTypes = {}; this.parseClassTypes = {};
this.meType = null; this.meType = null;
this.graphQLAutoSchema = null; this.graphQLAutoSchema = null;
@@ -53,16 +74,15 @@ class ParseGraphQLSchema {
defaultGraphQLTypes.load(this); defaultGraphQLTypes.load(this);
parseClasses.forEach(parseClass => { this._getParseClassesWithConfig(parseClasses, parseGraphQLConfig).forEach(
parseClassTypes.load(this, parseClass); ([parseClass, parseClassConfig]) => {
parseClassTypes.load(this, parseClass, parseClassConfig);
parseClassQueries.load(this, parseClass); parseClassQueries.load(this, parseClass, parseClassConfig);
parseClassMutations.load(this, parseClass, parseClassConfig);
parseClassMutations.load(this, parseClass); }
}); );
defaultGraphQLQueries.load(this); defaultGraphQLQueries.load(this);
defaultGraphQLMutations.load(this); defaultGraphQLMutations.load(this);
let graphQLQuery = undefined; let graphQLQuery = undefined;
@@ -160,6 +180,104 @@ class ParseGraphQLSchema {
} }
throw toGraphQLError(error); 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 }; export { ParseGraphQLSchema };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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