2019-06-19 17:19:47 -07:00
|
|
|
import Parse from 'parse/node';
|
2020-10-25 15:06:58 -05:00
|
|
|
import { GraphQLSchema, GraphQLObjectType, DocumentNode, GraphQLNamedType } from 'graphql';
|
2022-06-10 14:01:45 +02:00
|
|
|
import { mergeSchemas } from '@graphql-tools/schema';
|
|
|
|
|
import { mergeTypeDefs } from '@graphql-tools/merge';
|
2021-10-11 14:51:28 +02:00
|
|
|
import { isDeepStrictEqual } from 'util';
|
2019-06-19 17:19:47 -07:00
|
|
|
import requiredParameter from '../requiredParameter';
|
|
|
|
|
import * as defaultGraphQLTypes from './loaders/defaultGraphQLTypes';
|
|
|
|
|
import * as parseClassTypes from './loaders/parseClassTypes';
|
|
|
|
|
import * as parseClassQueries from './loaders/parseClassQueries';
|
|
|
|
|
import * as parseClassMutations from './loaders/parseClassMutations';
|
|
|
|
|
import * as defaultGraphQLQueries from './loaders/defaultGraphQLQueries';
|
|
|
|
|
import * as defaultGraphQLMutations from './loaders/defaultGraphQLMutations';
|
2020-10-25 15:06:58 -05:00
|
|
|
import ParseGraphQLController, { ParseGraphQLConfig } from '../Controllers/ParseGraphQLController';
|
2019-07-25 20:46:25 +01:00
|
|
|
import DatabaseController from '../Controllers/DatabaseController';
|
2021-03-16 16:05:36 -05:00
|
|
|
import SchemaCache from '../Adapters/Cache/SchemaCache';
|
2019-07-12 17:58:47 -03:00
|
|
|
import { toGraphQLError } from './parseGraphQLUtils';
|
GraphQL Custom Schema (#5821)
This PR empowers the Parse GraphQL API with custom user-defined schema. The developers can now write their own types, queries, and mutations, which will merged with the ones that are automatically generated. The new types are resolved by the application's cloud code functions.
Therefore, regarding https://github.com/parse-community/parse-server/issues/5777, this PR closes the cloud functions needs and also addresses the graphql customization topic. In my view, I think that this PR, together with https://github.com/parse-community/parse-server/pull/5782 and https://github.com/parse-community/parse-server/pull/5818, when merged, closes the issue.
How it works:
1. When initializing ParseGraphQLServer, now the developer can pass a custom schema that will be merged to the auto-generated one:
```
parseGraphQLServer = new ParseGraphQLServer(parseServer, {
graphQLPath: '/graphql',
graphQLCustomTypeDefs: gql`
extend type Query {
custom: Custom @namespace
}
type Custom {
hello: String @resolve
hello2: String @resolve(to: "hello")
userEcho(user: _UserFields!): _UserClass! @resolve
}
`,
});
```
Note:
- This PR includes a @namespace directive that can be used to the top level field of the nested queries and mutations (it basically just returns an empty object);
- This PR includes a @resolve directive that can be used to notify the Parse GraphQL Server to resolve that field using a cloud code function. The `to` argument specifies the function name. If the `to` argument is not passed, the Parse GraphQL Server will look for a function with the same name of the field;
- This PR allows creating custom types using the auto-generated ones as in `userEcho(user: _UserFields!): _UserClass! @resolve`;
- This PR allows to extend the auto-generated types, as in `extend type Query { ... }`.
2. Once the schema was set, you just need to write regular cloud code functions:
```
Parse.Cloud.define('hello', async () => {
return 'Hello world!';
});
Parse.Cloud.define('userEcho', async req => {
return req.params.user;
});
```
3. Now you are ready to play with your new custom api:
```
query {
custom {
hello
hello2
userEcho(user: { username: "somefolk" }) {
username
}
}
}
```
should return
```
{
"data": {
"custom": {
"hello": "Hello world!",
"hello2": "Hello world!",
"userEcho": {
"username": "somefolk"
}
}
}
}
```
2019-07-18 12:43:49 -07:00
|
|
|
import * as schemaDirectives from './loaders/schemaDirectives';
|
2019-09-01 22:11:03 -07:00
|
|
|
import * as schemaTypes from './loaders/schemaTypes';
|
2019-09-09 15:07:22 -07:00
|
|
|
import { getFunctionNames } from '../triggers';
|
2019-12-01 21:43:08 -08:00
|
|
|
import * as defaultRelaySchema from './loaders/defaultRelaySchema';
|
2019-06-19 17:19:47 -07:00
|
|
|
|
2019-08-15 23:23:41 +02:00
|
|
|
const RESERVED_GRAPHQL_TYPE_NAMES = [
|
|
|
|
|
'String',
|
|
|
|
|
'Boolean',
|
|
|
|
|
'Int',
|
|
|
|
|
'Float',
|
|
|
|
|
'ID',
|
|
|
|
|
'ArrayResult',
|
|
|
|
|
'Query',
|
|
|
|
|
'Mutation',
|
|
|
|
|
'Subscription',
|
2019-12-01 21:43:08 -08:00
|
|
|
'CreateFileInput',
|
|
|
|
|
'CreateFilePayload',
|
2019-08-15 23:23:41 +02:00
|
|
|
'Viewer',
|
2019-12-01 21:43:08 -08:00
|
|
|
'SignUpInput',
|
|
|
|
|
'SignUpPayload',
|
|
|
|
|
'LogInInput',
|
|
|
|
|
'LogInPayload',
|
|
|
|
|
'LogOutInput',
|
|
|
|
|
'LogOutPayload',
|
2019-09-09 15:07:22 -07:00
|
|
|
'CloudCodeFunction',
|
2019-12-01 21:43:08 -08:00
|
|
|
'CallCloudCodeInput',
|
|
|
|
|
'CallCloudCodePayload',
|
|
|
|
|
'CreateClassInput',
|
|
|
|
|
'CreateClassPayload',
|
|
|
|
|
'UpdateClassInput',
|
|
|
|
|
'UpdateClassPayload',
|
|
|
|
|
'DeleteClassInput',
|
|
|
|
|
'DeleteClassPayload',
|
|
|
|
|
'PageInfo',
|
2019-08-15 23:23:41 +02:00
|
|
|
];
|
2019-09-01 22:11:03 -07:00
|
|
|
const RESERVED_GRAPHQL_QUERY_NAMES = ['health', 'viewer', 'class', 'classes'];
|
2019-08-17 11:02:19 -07:00
|
|
|
const RESERVED_GRAPHQL_MUTATION_NAMES = [
|
|
|
|
|
'signUp',
|
|
|
|
|
'logIn',
|
|
|
|
|
'logOut',
|
|
|
|
|
'createFile',
|
|
|
|
|
'callCloudCode',
|
2019-09-01 22:11:03 -07:00
|
|
|
'createClass',
|
|
|
|
|
'updateClass',
|
|
|
|
|
'deleteClass',
|
2019-08-17 11:02:19 -07:00
|
|
|
];
|
2019-08-15 23:23:41 +02:00
|
|
|
|
2019-06-19 17:19:47 -07:00
|
|
|
class ParseGraphQLSchema {
|
2019-07-25 20:46:25 +01:00
|
|
|
databaseController: DatabaseController;
|
|
|
|
|
parseGraphQLController: ParseGraphQLController;
|
|
|
|
|
parseGraphQLConfig: ParseGraphQLConfig;
|
2019-12-01 21:43:08 -08:00
|
|
|
log: any;
|
|
|
|
|
appId: string;
|
2020-10-25 15:06:58 -05:00
|
|
|
graphQLCustomTypeDefs: ?(string | GraphQLSchema | DocumentNode | GraphQLNamedType[]);
|
2021-03-16 16:05:36 -05:00
|
|
|
schemaCache: any;
|
2019-07-25 20:46:25 +01:00
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
params: {
|
|
|
|
|
databaseController: DatabaseController,
|
|
|
|
|
parseGraphQLController: ParseGraphQLController,
|
|
|
|
|
log: any,
|
2019-09-09 15:07:22 -07:00
|
|
|
appId: string,
|
2020-10-25 15:06:58 -05:00
|
|
|
graphQLCustomTypeDefs: ?(string | GraphQLSchema | DocumentNode | GraphQLNamedType[]),
|
2019-07-25 20:46:25 +01:00
|
|
|
} = {}
|
|
|
|
|
) {
|
|
|
|
|
this.parseGraphQLController =
|
|
|
|
|
params.parseGraphQLController ||
|
|
|
|
|
requiredParameter('You must provide a parseGraphQLController instance!');
|
2019-06-19 17:19:47 -07:00
|
|
|
this.databaseController =
|
2019-07-25 20:46:25 +01:00
|
|
|
params.databaseController ||
|
2019-06-19 17:19:47 -07:00
|
|
|
requiredParameter('You must provide a databaseController instance!');
|
2020-10-25 15:06:58 -05:00
|
|
|
this.log = params.log || requiredParameter('You must provide a log instance!');
|
2019-07-25 20:46:25 +01:00
|
|
|
this.graphQLCustomTypeDefs = params.graphQLCustomTypeDefs;
|
2020-10-25 15:06:58 -05:00
|
|
|
this.appId = params.appId || requiredParameter('You must provide the appId!');
|
2021-03-16 16:05:36 -05:00
|
|
|
this.schemaCache = SchemaCache;
|
2022-05-18 19:55:43 +02:00
|
|
|
this.logCache = {};
|
2019-06-19 17:19:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async load() {
|
2019-07-25 20:46:25 +01:00
|
|
|
const { parseGraphQLConfig } = await this._initializeSchemaAndConfig();
|
2022-05-06 02:09:09 +02:00
|
|
|
const parseClassesArray = await this._getClassesForSchema(parseGraphQLConfig);
|
2019-09-09 15:07:22 -07:00
|
|
|
const functionNames = await this._getFunctionNames();
|
2022-05-18 19:55:43 +02:00
|
|
|
const functionNamesString = functionNames.join();
|
2019-06-19 17:19:47 -07:00
|
|
|
|
2022-05-06 02:09:09 +02:00
|
|
|
const parseClasses = parseClassesArray.reduce((acc, clazz) => {
|
|
|
|
|
acc[clazz.className] = clazz;
|
|
|
|
|
return acc;
|
|
|
|
|
}, {});
|
2019-07-25 20:46:25 +01:00
|
|
|
if (
|
|
|
|
|
!this._hasSchemaInputChanged({
|
|
|
|
|
parseClasses,
|
|
|
|
|
parseGraphQLConfig,
|
2019-09-09 15:07:22 -07:00
|
|
|
functionNamesString,
|
2019-07-25 20:46:25 +01:00
|
|
|
})
|
|
|
|
|
) {
|
|
|
|
|
return this.graphQLSchema;
|
2019-06-19 17:19:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.parseClasses = parseClasses;
|
2019-07-25 20:46:25 +01:00
|
|
|
this.parseGraphQLConfig = parseGraphQLConfig;
|
2019-09-09 15:07:22 -07:00
|
|
|
this.functionNames = functionNames;
|
|
|
|
|
this.functionNamesString = functionNamesString;
|
2019-06-19 17:19:47 -07:00
|
|
|
this.parseClassTypes = {};
|
2019-08-15 23:23:41 +02:00
|
|
|
this.viewerType = null;
|
GraphQL Custom Schema (#5821)
This PR empowers the Parse GraphQL API with custom user-defined schema. The developers can now write their own types, queries, and mutations, which will merged with the ones that are automatically generated. The new types are resolved by the application's cloud code functions.
Therefore, regarding https://github.com/parse-community/parse-server/issues/5777, this PR closes the cloud functions needs and also addresses the graphql customization topic. In my view, I think that this PR, together with https://github.com/parse-community/parse-server/pull/5782 and https://github.com/parse-community/parse-server/pull/5818, when merged, closes the issue.
How it works:
1. When initializing ParseGraphQLServer, now the developer can pass a custom schema that will be merged to the auto-generated one:
```
parseGraphQLServer = new ParseGraphQLServer(parseServer, {
graphQLPath: '/graphql',
graphQLCustomTypeDefs: gql`
extend type Query {
custom: Custom @namespace
}
type Custom {
hello: String @resolve
hello2: String @resolve(to: "hello")
userEcho(user: _UserFields!): _UserClass! @resolve
}
`,
});
```
Note:
- This PR includes a @namespace directive that can be used to the top level field of the nested queries and mutations (it basically just returns an empty object);
- This PR includes a @resolve directive that can be used to notify the Parse GraphQL Server to resolve that field using a cloud code function. The `to` argument specifies the function name. If the `to` argument is not passed, the Parse GraphQL Server will look for a function with the same name of the field;
- This PR allows creating custom types using the auto-generated ones as in `userEcho(user: _UserFields!): _UserClass! @resolve`;
- This PR allows to extend the auto-generated types, as in `extend type Query { ... }`.
2. Once the schema was set, you just need to write regular cloud code functions:
```
Parse.Cloud.define('hello', async () => {
return 'Hello world!';
});
Parse.Cloud.define('userEcho', async req => {
return req.params.user;
});
```
3. Now you are ready to play with your new custom api:
```
query {
custom {
hello
hello2
userEcho(user: { username: "somefolk" }) {
username
}
}
}
```
should return
```
{
"data": {
"custom": {
"hello": "Hello world!",
"hello2": "Hello world!",
"userEcho": {
"username": "somefolk"
}
}
}
}
```
2019-07-18 12:43:49 -07:00
|
|
|
this.graphQLAutoSchema = null;
|
2019-06-19 17:19:47 -07:00
|
|
|
this.graphQLSchema = null;
|
|
|
|
|
this.graphQLTypes = [];
|
|
|
|
|
this.graphQLQueries = {};
|
|
|
|
|
this.graphQLMutations = {};
|
|
|
|
|
this.graphQLSubscriptions = {};
|
GraphQL Custom Schema (#5821)
This PR empowers the Parse GraphQL API with custom user-defined schema. The developers can now write their own types, queries, and mutations, which will merged with the ones that are automatically generated. The new types are resolved by the application's cloud code functions.
Therefore, regarding https://github.com/parse-community/parse-server/issues/5777, this PR closes the cloud functions needs and also addresses the graphql customization topic. In my view, I think that this PR, together with https://github.com/parse-community/parse-server/pull/5782 and https://github.com/parse-community/parse-server/pull/5818, when merged, closes the issue.
How it works:
1. When initializing ParseGraphQLServer, now the developer can pass a custom schema that will be merged to the auto-generated one:
```
parseGraphQLServer = new ParseGraphQLServer(parseServer, {
graphQLPath: '/graphql',
graphQLCustomTypeDefs: gql`
extend type Query {
custom: Custom @namespace
}
type Custom {
hello: String @resolve
hello2: String @resolve(to: "hello")
userEcho(user: _UserFields!): _UserClass! @resolve
}
`,
});
```
Note:
- This PR includes a @namespace directive that can be used to the top level field of the nested queries and mutations (it basically just returns an empty object);
- This PR includes a @resolve directive that can be used to notify the Parse GraphQL Server to resolve that field using a cloud code function. The `to` argument specifies the function name. If the `to` argument is not passed, the Parse GraphQL Server will look for a function with the same name of the field;
- This PR allows creating custom types using the auto-generated ones as in `userEcho(user: _UserFields!): _UserClass! @resolve`;
- This PR allows to extend the auto-generated types, as in `extend type Query { ... }`.
2. Once the schema was set, you just need to write regular cloud code functions:
```
Parse.Cloud.define('hello', async () => {
return 'Hello world!';
});
Parse.Cloud.define('userEcho', async req => {
return req.params.user;
});
```
3. Now you are ready to play with your new custom api:
```
query {
custom {
hello
hello2
userEcho(user: { username: "somefolk" }) {
username
}
}
}
```
should return
```
{
"data": {
"custom": {
"hello": "Hello world!",
"hello2": "Hello world!",
"userEcho": {
"username": "somefolk"
}
}
}
}
```
2019-07-18 12:43:49 -07:00
|
|
|
this.graphQLSchemaDirectivesDefinitions = null;
|
|
|
|
|
this.graphQLSchemaDirectives = {};
|
2019-12-01 21:43:08 -08:00
|
|
|
this.relayNodeInterface = null;
|
2019-06-19 17:19:47 -07:00
|
|
|
|
|
|
|
|
defaultGraphQLTypes.load(this);
|
2019-12-01 21:43:08 -08:00
|
|
|
defaultRelaySchema.load(this);
|
2019-09-01 22:11:03 -07:00
|
|
|
schemaTypes.load(this);
|
2019-06-19 17:19:47 -07:00
|
|
|
|
2022-05-06 02:09:09 +02:00
|
|
|
this._getParseClassesWithConfig(parseClassesArray, parseGraphQLConfig).forEach(
|
2019-07-25 20:46:25 +01:00
|
|
|
([parseClass, parseClassConfig]) => {
|
2021-10-11 14:51:28 +02:00
|
|
|
// Some times schema return the _auth_data_ field
|
|
|
|
|
// it will lead to unstable graphql generation order
|
|
|
|
|
if (parseClass.className === '_User') {
|
|
|
|
|
Object.keys(parseClass.fields).forEach(fieldName => {
|
|
|
|
|
if (fieldName.startsWith('_auth_data_')) {
|
|
|
|
|
delete parseClass.fields[fieldName];
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fields order inside the schema seems to not be consistent across
|
|
|
|
|
// restart so we need to ensure an alphabetical order
|
|
|
|
|
// also it's better for the playground documentation
|
|
|
|
|
const orderedFields = {};
|
|
|
|
|
Object.keys(parseClass.fields)
|
|
|
|
|
.sort()
|
|
|
|
|
.forEach(fieldName => {
|
|
|
|
|
orderedFields[fieldName] = parseClass.fields[fieldName];
|
|
|
|
|
});
|
|
|
|
|
parseClass.fields = orderedFields;
|
2019-07-25 20:46:25 +01:00
|
|
|
parseClassTypes.load(this, parseClass, parseClassConfig);
|
|
|
|
|
parseClassQueries.load(this, parseClass, parseClassConfig);
|
|
|
|
|
parseClassMutations.load(this, parseClass, parseClassConfig);
|
|
|
|
|
}
|
|
|
|
|
);
|
2019-08-17 11:02:19 -07:00
|
|
|
|
2022-05-06 02:09:09 +02:00
|
|
|
defaultGraphQLTypes.loadArrayResult(this, parseClassesArray);
|
2019-06-19 17:19:47 -07:00
|
|
|
defaultGraphQLQueries.load(this);
|
|
|
|
|
defaultGraphQLMutations.load(this);
|
|
|
|
|
|
|
|
|
|
let graphQLQuery = undefined;
|
|
|
|
|
if (Object.keys(this.graphQLQueries).length > 0) {
|
|
|
|
|
graphQLQuery = new GraphQLObjectType({
|
|
|
|
|
name: 'Query',
|
|
|
|
|
description: 'Query is the top level type for queries.',
|
|
|
|
|
fields: this.graphQLQueries,
|
|
|
|
|
});
|
2019-08-15 23:23:41 +02:00
|
|
|
this.addGraphQLType(graphQLQuery, true, true);
|
2019-06-19 17:19:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let graphQLMutation = undefined;
|
|
|
|
|
if (Object.keys(this.graphQLMutations).length > 0) {
|
|
|
|
|
graphQLMutation = new GraphQLObjectType({
|
|
|
|
|
name: 'Mutation',
|
|
|
|
|
description: 'Mutation is the top level type for mutations.',
|
|
|
|
|
fields: this.graphQLMutations,
|
|
|
|
|
});
|
2019-08-15 23:23:41 +02:00
|
|
|
this.addGraphQLType(graphQLMutation, true, true);
|
2019-06-19 17:19:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let graphQLSubscription = undefined;
|
|
|
|
|
if (Object.keys(this.graphQLSubscriptions).length > 0) {
|
|
|
|
|
graphQLSubscription = new GraphQLObjectType({
|
|
|
|
|
name: 'Subscription',
|
|
|
|
|
description: 'Subscription is the top level type for subscriptions.',
|
|
|
|
|
fields: this.graphQLSubscriptions,
|
|
|
|
|
});
|
2019-08-15 23:23:41 +02:00
|
|
|
this.addGraphQLType(graphQLSubscription, true, true);
|
2019-06-19 17:19:47 -07:00
|
|
|
}
|
|
|
|
|
|
GraphQL Custom Schema (#5821)
This PR empowers the Parse GraphQL API with custom user-defined schema. The developers can now write their own types, queries, and mutations, which will merged with the ones that are automatically generated. The new types are resolved by the application's cloud code functions.
Therefore, regarding https://github.com/parse-community/parse-server/issues/5777, this PR closes the cloud functions needs and also addresses the graphql customization topic. In my view, I think that this PR, together with https://github.com/parse-community/parse-server/pull/5782 and https://github.com/parse-community/parse-server/pull/5818, when merged, closes the issue.
How it works:
1. When initializing ParseGraphQLServer, now the developer can pass a custom schema that will be merged to the auto-generated one:
```
parseGraphQLServer = new ParseGraphQLServer(parseServer, {
graphQLPath: '/graphql',
graphQLCustomTypeDefs: gql`
extend type Query {
custom: Custom @namespace
}
type Custom {
hello: String @resolve
hello2: String @resolve(to: "hello")
userEcho(user: _UserFields!): _UserClass! @resolve
}
`,
});
```
Note:
- This PR includes a @namespace directive that can be used to the top level field of the nested queries and mutations (it basically just returns an empty object);
- This PR includes a @resolve directive that can be used to notify the Parse GraphQL Server to resolve that field using a cloud code function. The `to` argument specifies the function name. If the `to` argument is not passed, the Parse GraphQL Server will look for a function with the same name of the field;
- This PR allows creating custom types using the auto-generated ones as in `userEcho(user: _UserFields!): _UserClass! @resolve`;
- This PR allows to extend the auto-generated types, as in `extend type Query { ... }`.
2. Once the schema was set, you just need to write regular cloud code functions:
```
Parse.Cloud.define('hello', async () => {
return 'Hello world!';
});
Parse.Cloud.define('userEcho', async req => {
return req.params.user;
});
```
3. Now you are ready to play with your new custom api:
```
query {
custom {
hello
hello2
userEcho(user: { username: "somefolk" }) {
username
}
}
}
```
should return
```
{
"data": {
"custom": {
"hello": "Hello world!",
"hello2": "Hello world!",
"userEcho": {
"username": "somefolk"
}
}
}
}
```
2019-07-18 12:43:49 -07:00
|
|
|
this.graphQLAutoSchema = new GraphQLSchema({
|
2019-06-19 17:19:47 -07:00
|
|
|
types: this.graphQLTypes,
|
|
|
|
|
query: graphQLQuery,
|
|
|
|
|
mutation: graphQLMutation,
|
|
|
|
|
subscription: graphQLSubscription,
|
|
|
|
|
});
|
|
|
|
|
|
GraphQL Custom Schema (#5821)
This PR empowers the Parse GraphQL API with custom user-defined schema. The developers can now write their own types, queries, and mutations, which will merged with the ones that are automatically generated. The new types are resolved by the application's cloud code functions.
Therefore, regarding https://github.com/parse-community/parse-server/issues/5777, this PR closes the cloud functions needs and also addresses the graphql customization topic. In my view, I think that this PR, together with https://github.com/parse-community/parse-server/pull/5782 and https://github.com/parse-community/parse-server/pull/5818, when merged, closes the issue.
How it works:
1. When initializing ParseGraphQLServer, now the developer can pass a custom schema that will be merged to the auto-generated one:
```
parseGraphQLServer = new ParseGraphQLServer(parseServer, {
graphQLPath: '/graphql',
graphQLCustomTypeDefs: gql`
extend type Query {
custom: Custom @namespace
}
type Custom {
hello: String @resolve
hello2: String @resolve(to: "hello")
userEcho(user: _UserFields!): _UserClass! @resolve
}
`,
});
```
Note:
- This PR includes a @namespace directive that can be used to the top level field of the nested queries and mutations (it basically just returns an empty object);
- This PR includes a @resolve directive that can be used to notify the Parse GraphQL Server to resolve that field using a cloud code function. The `to` argument specifies the function name. If the `to` argument is not passed, the Parse GraphQL Server will look for a function with the same name of the field;
- This PR allows creating custom types using the auto-generated ones as in `userEcho(user: _UserFields!): _UserClass! @resolve`;
- This PR allows to extend the auto-generated types, as in `extend type Query { ... }`.
2. Once the schema was set, you just need to write regular cloud code functions:
```
Parse.Cloud.define('hello', async () => {
return 'Hello world!';
});
Parse.Cloud.define('userEcho', async req => {
return req.params.user;
});
```
3. Now you are ready to play with your new custom api:
```
query {
custom {
hello
hello2
userEcho(user: { username: "somefolk" }) {
username
}
}
}
```
should return
```
{
"data": {
"custom": {
"hello": "Hello world!",
"hello2": "Hello world!",
"userEcho": {
"username": "somefolk"
}
}
}
}
```
2019-07-18 12:43:49 -07:00
|
|
|
if (this.graphQLCustomTypeDefs) {
|
|
|
|
|
schemaDirectives.load(this);
|
2020-02-22 00:12:49 +01:00
|
|
|
if (typeof this.graphQLCustomTypeDefs.getTypeMap === 'function') {
|
2022-06-10 14:01:45 +02:00
|
|
|
// In following code we use underscore attr to keep the direct variable reference
|
2021-10-11 14:51:28 +02:00
|
|
|
const customGraphQLSchemaTypeMap = this.graphQLCustomTypeDefs._typeMap;
|
2020-07-13 10:13:47 +02:00
|
|
|
const findAndReplaceLastType = (parent, key) => {
|
|
|
|
|
if (parent[key].name) {
|
|
|
|
|
if (
|
2021-10-11 14:51:28 +02:00
|
|
|
this.graphQLAutoSchema._typeMap[parent[key].name] &&
|
|
|
|
|
this.graphQLAutoSchema._typeMap[parent[key].name] !== parent[key]
|
2020-07-13 10:13:47 +02:00
|
|
|
) {
|
|
|
|
|
// To avoid unresolved field on overloaded schema
|
|
|
|
|
// replace the final type with the auto schema one
|
2021-10-11 14:51:28 +02:00
|
|
|
parent[key] = this.graphQLAutoSchema._typeMap[parent[key].name];
|
2020-07-13 10:13:47 +02:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (parent[key].ofType) {
|
|
|
|
|
findAndReplaceLastType(parent[key], 'ofType');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
2021-10-11 14:51:28 +02:00
|
|
|
// Add non shared types from custom schema to auto schema
|
|
|
|
|
// note: some non shared types can use some shared types
|
|
|
|
|
// so this code need to be ran before the shared types addition
|
|
|
|
|
// we use sort to ensure schema consistency over restarts
|
|
|
|
|
Object.keys(customGraphQLSchemaTypeMap)
|
|
|
|
|
.sort()
|
|
|
|
|
.forEach(customGraphQLSchemaTypeKey => {
|
|
|
|
|
const customGraphQLSchemaType = customGraphQLSchemaTypeMap[customGraphQLSchemaTypeKey];
|
|
|
|
|
if (
|
|
|
|
|
!customGraphQLSchemaType ||
|
|
|
|
|
!customGraphQLSchemaType.name ||
|
|
|
|
|
customGraphQLSchemaType.name.startsWith('__')
|
|
|
|
|
) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const autoGraphQLSchemaType = this.graphQLAutoSchema._typeMap[
|
|
|
|
|
customGraphQLSchemaType.name
|
|
|
|
|
];
|
|
|
|
|
if (!autoGraphQLSchemaType) {
|
|
|
|
|
this.graphQLAutoSchema._typeMap[
|
|
|
|
|
customGraphQLSchemaType.name
|
|
|
|
|
] = customGraphQLSchemaType;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
// Handle shared types
|
|
|
|
|
// We pass through each type and ensure that all sub field types are replaced
|
|
|
|
|
// we use sort to ensure schema consistency over restarts
|
|
|
|
|
Object.keys(customGraphQLSchemaTypeMap)
|
|
|
|
|
.sort()
|
|
|
|
|
.forEach(customGraphQLSchemaTypeKey => {
|
|
|
|
|
const customGraphQLSchemaType = customGraphQLSchemaTypeMap[customGraphQLSchemaTypeKey];
|
|
|
|
|
if (
|
|
|
|
|
!customGraphQLSchemaType ||
|
|
|
|
|
!customGraphQLSchemaType.name ||
|
|
|
|
|
customGraphQLSchemaType.name.startsWith('__')
|
|
|
|
|
) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const autoGraphQLSchemaType = this.graphQLAutoSchema._typeMap[
|
|
|
|
|
customGraphQLSchemaType.name
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if (autoGraphQLSchemaType && typeof customGraphQLSchemaType.getFields === 'function') {
|
|
|
|
|
Object.keys(customGraphQLSchemaType._fields)
|
|
|
|
|
.sort()
|
|
|
|
|
.forEach(fieldKey => {
|
|
|
|
|
const field = customGraphQLSchemaType._fields[fieldKey];
|
|
|
|
|
findAndReplaceLastType(field, 'type');
|
|
|
|
|
autoGraphQLSchemaType._fields[field.name] = field;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
this.graphQLSchema = this.graphQLAutoSchema;
|
2020-02-22 00:12:49 +01:00
|
|
|
} else if (typeof this.graphQLCustomTypeDefs === 'function') {
|
|
|
|
|
this.graphQLSchema = await this.graphQLCustomTypeDefs({
|
|
|
|
|
directivesDefinitionsSchema: this.graphQLSchemaDirectivesDefinitions,
|
|
|
|
|
autoSchema: this.graphQLAutoSchema,
|
2022-06-10 14:01:45 +02:00
|
|
|
graphQLSchemaDirectives: this.graphQLSchemaDirectives,
|
2020-02-22 00:12:49 +01:00
|
|
|
});
|
|
|
|
|
} else {
|
2022-06-10 14:01:45 +02:00
|
|
|
this.graphQLSchema = mergeSchemas({
|
|
|
|
|
schemas: [this.graphQLAutoSchema],
|
|
|
|
|
typeDefs: mergeTypeDefs([
|
2020-02-22 00:12:49 +01:00
|
|
|
this.graphQLCustomTypeDefs,
|
2022-06-10 14:01:45 +02:00
|
|
|
this.graphQLSchemaDirectivesDefinitions,
|
|
|
|
|
]),
|
2020-02-22 00:12:49 +01:00
|
|
|
});
|
2022-06-10 14:01:45 +02:00
|
|
|
this.graphQLSchema = this.graphQLSchemaDirectives(this.graphQLSchema);
|
2020-02-22 00:12:49 +01:00
|
|
|
}
|
GraphQL Custom Schema (#5821)
This PR empowers the Parse GraphQL API with custom user-defined schema. The developers can now write their own types, queries, and mutations, which will merged with the ones that are automatically generated. The new types are resolved by the application's cloud code functions.
Therefore, regarding https://github.com/parse-community/parse-server/issues/5777, this PR closes the cloud functions needs and also addresses the graphql customization topic. In my view, I think that this PR, together with https://github.com/parse-community/parse-server/pull/5782 and https://github.com/parse-community/parse-server/pull/5818, when merged, closes the issue.
How it works:
1. When initializing ParseGraphQLServer, now the developer can pass a custom schema that will be merged to the auto-generated one:
```
parseGraphQLServer = new ParseGraphQLServer(parseServer, {
graphQLPath: '/graphql',
graphQLCustomTypeDefs: gql`
extend type Query {
custom: Custom @namespace
}
type Custom {
hello: String @resolve
hello2: String @resolve(to: "hello")
userEcho(user: _UserFields!): _UserClass! @resolve
}
`,
});
```
Note:
- This PR includes a @namespace directive that can be used to the top level field of the nested queries and mutations (it basically just returns an empty object);
- This PR includes a @resolve directive that can be used to notify the Parse GraphQL Server to resolve that field using a cloud code function. The `to` argument specifies the function name. If the `to` argument is not passed, the Parse GraphQL Server will look for a function with the same name of the field;
- This PR allows creating custom types using the auto-generated ones as in `userEcho(user: _UserFields!): _UserClass! @resolve`;
- This PR allows to extend the auto-generated types, as in `extend type Query { ... }`.
2. Once the schema was set, you just need to write regular cloud code functions:
```
Parse.Cloud.define('hello', async () => {
return 'Hello world!';
});
Parse.Cloud.define('userEcho', async req => {
return req.params.user;
});
```
3. Now you are ready to play with your new custom api:
```
query {
custom {
hello
hello2
userEcho(user: { username: "somefolk" }) {
username
}
}
}
```
should return
```
{
"data": {
"custom": {
"hello": "Hello world!",
"hello2": "Hello world!",
"userEcho": {
"username": "somefolk"
}
}
}
}
```
2019-07-18 12:43:49 -07:00
|
|
|
} else {
|
|
|
|
|
this.graphQLSchema = this.graphQLAutoSchema;
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-19 17:19:47 -07:00
|
|
|
return this.graphQLSchema;
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-18 19:55:43 +02:00
|
|
|
_logOnce(severity, message) {
|
|
|
|
|
if (this.logCache[message]) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.log[severity](message);
|
|
|
|
|
this.logCache[message] = true;
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-25 15:06:58 -05:00
|
|
|
addGraphQLType(type, throwError = false, ignoreReserved = false, ignoreConnection = false) {
|
2019-08-15 23:23:41 +02:00
|
|
|
if (
|
|
|
|
|
(!ignoreReserved && RESERVED_GRAPHQL_TYPE_NAMES.includes(type.name)) ||
|
2020-07-13 17:13:08 -05:00
|
|
|
this.graphQLTypes.find(existingType => existingType.name === type.name) ||
|
2019-12-01 21:43:08 -08:00
|
|
|
(!ignoreConnection && type.name.endsWith('Connection'))
|
2019-08-15 23:23:41 +02:00
|
|
|
) {
|
|
|
|
|
const message = `Type ${type.name} could not be added to the auto schema because it collided with an existing type.`;
|
|
|
|
|
if (throwError) {
|
|
|
|
|
throw new Error(message);
|
|
|
|
|
}
|
2022-05-18 19:55:43 +02:00
|
|
|
this._logOnce('warn', message);
|
2019-08-15 23:23:41 +02:00
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
this.graphQLTypes.push(type);
|
|
|
|
|
return type;
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-25 15:06:58 -05:00
|
|
|
addGraphQLQuery(fieldName, field, throwError = false, ignoreReserved = false) {
|
2019-08-15 23:23:41 +02:00
|
|
|
if (
|
2019-08-17 11:02:19 -07:00
|
|
|
(!ignoreReserved && RESERVED_GRAPHQL_QUERY_NAMES.includes(fieldName)) ||
|
|
|
|
|
this.graphQLQueries[fieldName]
|
2019-08-15 23:23:41 +02:00
|
|
|
) {
|
2019-08-17 11:02:19 -07:00
|
|
|
const message = `Query ${fieldName} could not be added to the auto schema because it collided with an existing field.`;
|
2019-08-15 23:23:41 +02:00
|
|
|
if (throwError) {
|
|
|
|
|
throw new Error(message);
|
|
|
|
|
}
|
2022-05-18 19:55:43 +02:00
|
|
|
this._logOnce('warn', message);
|
2019-08-15 23:23:41 +02:00
|
|
|
return undefined;
|
|
|
|
|
}
|
2019-08-17 11:02:19 -07:00
|
|
|
this.graphQLQueries[fieldName] = field;
|
2019-08-15 23:23:41 +02:00
|
|
|
return field;
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-25 15:06:58 -05:00
|
|
|
addGraphQLMutation(fieldName, field, throwError = false, ignoreReserved = false) {
|
2019-08-15 23:23:41 +02:00
|
|
|
if (
|
2020-10-25 15:06:58 -05:00
|
|
|
(!ignoreReserved && RESERVED_GRAPHQL_MUTATION_NAMES.includes(fieldName)) ||
|
2019-08-17 11:02:19 -07:00
|
|
|
this.graphQLMutations[fieldName]
|
2019-08-15 23:23:41 +02:00
|
|
|
) {
|
2019-08-17 11:02:19 -07:00
|
|
|
const message = `Mutation ${fieldName} could not be added to the auto schema because it collided with an existing field.`;
|
2019-08-15 23:23:41 +02:00
|
|
|
if (throwError) {
|
|
|
|
|
throw new Error(message);
|
|
|
|
|
}
|
2022-05-18 19:55:43 +02:00
|
|
|
this._logOnce('warn', message);
|
2019-08-15 23:23:41 +02:00
|
|
|
return undefined;
|
|
|
|
|
}
|
2019-08-17 11:02:19 -07:00
|
|
|
this.graphQLMutations[fieldName] = field;
|
2019-08-15 23:23:41 +02:00
|
|
|
return field;
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-19 17:19:47 -07:00
|
|
|
handleError(error) {
|
|
|
|
|
if (error instanceof Parse.Error) {
|
|
|
|
|
this.log.error('Parse error: ', error);
|
|
|
|
|
} else {
|
|
|
|
|
this.log.error('Uncaught internal server error.', error, error.stack);
|
|
|
|
|
}
|
2019-07-12 17:58:47 -03:00
|
|
|
throw toGraphQLError(error);
|
2019-06-19 17:19:47 -07:00
|
|
|
}
|
2019-07-25 20:46:25 +01:00
|
|
|
|
|
|
|
|
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) {
|
2020-07-13 17:13:08 -05:00
|
|
|
includedClasses = allClasses.filter(clazz => {
|
2019-07-25 20:46:25 +01:00
|
|
|
return enabledForClasses.includes(clazz.className);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (disabledForClasses) {
|
|
|
|
|
// Classes included in `enabledForClasses` that
|
|
|
|
|
// are also present in `disabledForClasses` will
|
|
|
|
|
// still be filtered out
|
2020-07-13 17:13:08 -05:00
|
|
|
includedClasses = includedClasses.filter(clazz => {
|
2019-07-25 20:46:25 +01:00
|
|
|
return !disabledForClasses.includes(clazz.className);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-13 17:13:08 -05:00
|
|
|
this.isUsersClassDisabled = !includedClasses.some(clazz => {
|
2019-07-25 20:46:25 +01:00
|
|
|
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.
|
|
|
|
|
*/
|
2020-10-25 15:06:58 -05:00
|
|
|
_getParseClassesWithConfig(parseClasses, parseGraphQLConfig: ParseGraphQLConfig) {
|
2019-07-25 20:46:25 +01:00
|
|
|
const { classConfigs } = parseGraphQLConfig;
|
2019-08-15 23:23:41 +02:00
|
|
|
|
|
|
|
|
// Make sures that the default classes and classes that
|
|
|
|
|
// starts with capitalized letter will be generated first.
|
|
|
|
|
const sortClasses = (a, b) => {
|
|
|
|
|
a = a.className;
|
|
|
|
|
b = b.className;
|
|
|
|
|
if (a[0] === '_') {
|
|
|
|
|
if (b[0] !== '_') {
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (b[0] === '_') {
|
|
|
|
|
if (a[0] !== '_') {
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (a === b) {
|
|
|
|
|
return 0;
|
|
|
|
|
} else if (a < b) {
|
|
|
|
|
return -1;
|
|
|
|
|
} else {
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2020-07-13 17:13:08 -05:00
|
|
|
return parseClasses.sort(sortClasses).map(parseClass => {
|
2019-07-25 20:46:25 +01:00
|
|
|
let parseClassConfig;
|
|
|
|
|
if (classConfigs) {
|
2020-10-25 15:06:58 -05:00
|
|
|
parseClassConfig = classConfigs.find(c => c.className === parseClass.className);
|
2019-07-25 20:46:25 +01:00
|
|
|
}
|
|
|
|
|
return [parseClass, parseClassConfig];
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-09 15:07:22 -07:00
|
|
|
async _getFunctionNames() {
|
2020-07-13 17:13:08 -05:00
|
|
|
return await getFunctionNames(this.appId).filter(functionName => {
|
2019-09-09 15:07:22 -07:00
|
|
|
if (/^[_a-zA-Z][_a-zA-Z0-9]*$/.test(functionName)) {
|
|
|
|
|
return true;
|
|
|
|
|
} else {
|
2022-05-18 19:55:43 +02:00
|
|
|
this._logOnce(
|
|
|
|
|
'warn',
|
2019-09-09 15:07:22 -07:00
|
|
|
`Function ${functionName} could not be added to the auto schema because GraphQL names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/.`
|
|
|
|
|
);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-25 20:46:25 +01:00
|
|
|
/**
|
|
|
|
|
* 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,
|
|
|
|
|
parseGraphQLConfig: ?ParseGraphQLConfig,
|
2019-09-09 15:07:22 -07:00
|
|
|
functionNamesString: string,
|
2019-07-25 20:46:25 +01:00
|
|
|
}): boolean {
|
2021-10-11 14:51:28 +02:00
|
|
|
const { parseClasses, parseGraphQLConfig, functionNamesString } = params;
|
|
|
|
|
|
|
|
|
|
// First init
|
2022-05-06 02:09:09 +02:00
|
|
|
if (!this.graphQLSchema) {
|
2021-10-11 14:51:28 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-25 20:46:25 +01:00
|
|
|
if (
|
2021-10-11 14:51:28 +02:00
|
|
|
isDeepStrictEqual(this.parseGraphQLConfig, parseGraphQLConfig) &&
|
|
|
|
|
this.functionNamesString === functionNamesString &&
|
2022-05-06 02:09:09 +02:00
|
|
|
isDeepStrictEqual(this.parseClasses, parseClasses)
|
2019-07-25 20:46:25 +01:00
|
|
|
) {
|
2021-10-11 14:51:28 +02:00
|
|
|
return false;
|
2019-07-25 20:46:25 +01:00
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2019-06-19 17:19:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export { ParseGraphQLSchema };
|