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:
committed by
Antonio Davi Macedo Coelho de Castro
parent
bbcc20fd60
commit
d3810c2eba
@@ -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) {
|
||||
|
||||
375
src/Controllers/ParseGraphQLController.js
Normal file
375
src/Controllers/ParseGraphQLController.js
Normal 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 };
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user