Allow to resolve automatically Parse Type fields from Custom Schema (#6562)
* add package * Allow real GraphQL Schema via ParseServer.start * Allow resolve fields from auto graphQL Schema * Simple merge * fix + improve * Add tests
This commit is contained in:
@@ -122,6 +122,7 @@
|
|||||||
"url": "https://opencollective.com/parse-server",
|
"url": "https://opencollective.com/parse-server",
|
||||||
"logo": "https://opencollective.com/parse-server/logo.txt?reverse=true&variant=binary"
|
"logo": "https://opencollective.com/parse-server/logo.txt?reverse=true&variant=binary"
|
||||||
},
|
},
|
||||||
|
"publishConfig": { "registry": "https://npm.pkg.github.com/" },
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/parse-server"
|
"url": "https://opencollective.com/parse-server"
|
||||||
|
|||||||
@@ -10769,57 +10769,70 @@ describe('ParseGraphQLServer', () => {
|
|||||||
robot: { value: 'robot' },
|
robot: { value: 'robot' },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
parseGraphQLServer = new ParseGraphQLServer(parseServer, {
|
const SomeClassType = new GraphQLObjectType({
|
||||||
graphQLPath: '/graphql',
|
name: 'SomeClass',
|
||||||
graphQLCustomTypeDefs: new GraphQLSchema({
|
fields: {
|
||||||
query: new GraphQLObjectType({
|
nameUpperCase: {
|
||||||
name: 'Query',
|
type: new GraphQLNonNull(GraphQLString),
|
||||||
fields: {
|
resolve: (p) => p.name.toUpperCase(),
|
||||||
customQuery: {
|
|
||||||
type: new GraphQLNonNull(GraphQLString),
|
|
||||||
args: {
|
|
||||||
message: { type: new GraphQLNonNull(GraphQLString) },
|
|
||||||
},
|
|
||||||
resolve: (p, { message }) => message,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}),
|
type: { type: TypeEnum },
|
||||||
types: [
|
language: {
|
||||||
new GraphQLInputObjectType({
|
type: new GraphQLEnumType({
|
||||||
name: 'CreateSomeClassFieldsInput',
|
name: 'LanguageEnum',
|
||||||
fields: {
|
values: {
|
||||||
type: { type: TypeEnum },
|
fr: { value: 'fr' },
|
||||||
},
|
en: { value: 'en' },
|
||||||
}),
|
|
||||||
new GraphQLInputObjectType({
|
|
||||||
name: 'UpdateSomeClassFieldsInput',
|
|
||||||
fields: {
|
|
||||||
type: { type: TypeEnum },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
new GraphQLObjectType({
|
|
||||||
name: 'SomeClass',
|
|
||||||
fields: {
|
|
||||||
nameUpperCase: {
|
|
||||||
type: new GraphQLNonNull(GraphQLString),
|
|
||||||
resolve: (p) => p.name.toUpperCase(),
|
|
||||||
},
|
},
|
||||||
type: { type: TypeEnum },
|
}),
|
||||||
language: {
|
resolve: () => 'fr',
|
||||||
type: new GraphQLEnumType({
|
},
|
||||||
name: 'LanguageEnum',
|
},
|
||||||
values: {
|
|
||||||
fr: { value: 'fr' },
|
|
||||||
en: { value: 'en' },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
resolve: () => 'fr',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
});
|
parseGraphQLServer = new ParseGraphQLServer(parseServer, {
|
||||||
|
graphQLPath: '/graphql',
|
||||||
|
graphQLCustomTypeDefs: new GraphQLSchema({
|
||||||
|
query: new GraphQLObjectType({
|
||||||
|
name: 'Query',
|
||||||
|
fields: {
|
||||||
|
customQuery: {
|
||||||
|
type: new GraphQLNonNull(GraphQLString),
|
||||||
|
args: {
|
||||||
|
message: { type: new GraphQLNonNull(GraphQLString) },
|
||||||
|
},
|
||||||
|
resolve: (p, { message }) => message,
|
||||||
|
},
|
||||||
|
customQueryWithAutoTypeReturn: {
|
||||||
|
type: SomeClassType,
|
||||||
|
args: {
|
||||||
|
id: { type: new GraphQLNonNull(GraphQLString) },
|
||||||
|
},
|
||||||
|
resolve: async (p, { id }) => {
|
||||||
|
const obj = new Parse.Object('SomeClass');
|
||||||
|
obj.id = id;
|
||||||
|
await obj.fetch();
|
||||||
|
return obj.toJSON();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
types: [
|
||||||
|
new GraphQLInputObjectType({
|
||||||
|
name: 'CreateSomeClassFieldsInput',
|
||||||
|
fields: {
|
||||||
|
type: { type: TypeEnum },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new GraphQLInputObjectType({
|
||||||
|
name: 'UpdateSomeClassFieldsInput',
|
||||||
|
fields: {
|
||||||
|
type: { type: TypeEnum },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
SomeClassType,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
parseGraphQLServer.applyGraphQL(expressApp);
|
parseGraphQLServer.applyGraphQL(expressApp);
|
||||||
await new Promise((resolve) =>
|
await new Promise((resolve) =>
|
||||||
@@ -10857,6 +10870,33 @@ describe('ParseGraphQLServer', () => {
|
|||||||
expect(result.data.customQuery).toEqual('hello');
|
expect(result.data.customQuery).toEqual('hello');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can resolve a custom query with auto type return', async () => {
|
||||||
|
const obj = new Parse.Object('SomeClass');
|
||||||
|
await obj.save({ name: 'aname', type: 'robot' });
|
||||||
|
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear();
|
||||||
|
const result = await apolloClient.query({
|
||||||
|
variables: { id: obj.id },
|
||||||
|
query: gql`
|
||||||
|
query CustomQuery($id: String!) {
|
||||||
|
customQueryWithAutoTypeReturn(id: $id) {
|
||||||
|
objectId
|
||||||
|
nameUpperCase
|
||||||
|
name
|
||||||
|
type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
expect(result.data.customQueryWithAutoTypeReturn.objectId).toEqual(
|
||||||
|
obj.id
|
||||||
|
);
|
||||||
|
expect(result.data.customQueryWithAutoTypeReturn.name).toEqual('aname');
|
||||||
|
expect(result.data.customQueryWithAutoTypeReturn.nameUpperCase).toEqual(
|
||||||
|
'ANAME'
|
||||||
|
);
|
||||||
|
expect(result.data.customQueryWithAutoTypeReturn.type).toEqual('robot');
|
||||||
|
});
|
||||||
|
|
||||||
it('can resolve a custom extend type', async () => {
|
it('can resolve a custom extend type', async () => {
|
||||||
const obj = new Parse.Object('SomeClass');
|
const obj = new Parse.Object('SomeClass');
|
||||||
await obj.save({ name: 'aname', type: 'robot' });
|
await obj.save({ name: 'aname', type: 'robot' });
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ class ParseGraphQLSchema {
|
|||||||
if (typeof this.graphQLCustomTypeDefs.getTypeMap === 'function') {
|
if (typeof this.graphQLCustomTypeDefs.getTypeMap === 'function') {
|
||||||
const customGraphQLSchemaTypeMap = this.graphQLCustomTypeDefs.getTypeMap();
|
const customGraphQLSchemaTypeMap = this.graphQLCustomTypeDefs.getTypeMap();
|
||||||
Object.values(customGraphQLSchemaTypeMap).forEach(
|
Object.values(customGraphQLSchemaTypeMap).forEach(
|
||||||
customGraphQLSchemaType => {
|
(customGraphQLSchemaType) => {
|
||||||
if (
|
if (
|
||||||
!customGraphQLSchemaType ||
|
!customGraphQLSchemaType ||
|
||||||
!customGraphQLSchemaType.name ||
|
!customGraphQLSchemaType.name ||
|
||||||
@@ -215,40 +215,45 @@ class ParseGraphQLSchema {
|
|||||||
autoGraphQLSchemaType &&
|
autoGraphQLSchemaType &&
|
||||||
typeof customGraphQLSchemaType.getFields === 'function'
|
typeof customGraphQLSchemaType.getFields === 'function'
|
||||||
) {
|
) {
|
||||||
const findAndAddLastType = type => {
|
const findAndReplaceLastType = (parent, key) => {
|
||||||
if (type.name) {
|
if (parent[key].name) {
|
||||||
if (!this.graphQLAutoSchema.getType(type)) {
|
if (
|
||||||
// To avoid schema stitching (Unknow type) bug on variables
|
this.graphQLAutoSchema.getType(parent[key].name) &&
|
||||||
// transfer the final type to the Auto Schema
|
this.graphQLAutoSchema.getType(parent[key].name) !==
|
||||||
this.graphQLAutoSchema._typeMap[type.name] = type;
|
parent[key]
|
||||||
|
) {
|
||||||
|
// To avoid unresolved field on overloaded schema
|
||||||
|
// replace the final type with the auto schema one
|
||||||
|
parent[key] = this.graphQLAutoSchema.getType(
|
||||||
|
parent[key].name
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (type.ofType) {
|
if (parent[key].ofType) {
|
||||||
findAndAddLastType(type.ofType);
|
findAndReplaceLastType(parent[key], 'ofType');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.values(customGraphQLSchemaType.getFields()).forEach(
|
Object.values(customGraphQLSchemaType.getFields()).forEach(
|
||||||
field => {
|
(field) => {
|
||||||
findAndAddLastType(field.type);
|
findAndReplaceLastType(field, 'type');
|
||||||
if (field.args) {
|
|
||||||
field.args.forEach(arg => {
|
|
||||||
findAndAddLastType(arg.type);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
autoGraphQLSchemaType._fields = {
|
autoGraphQLSchemaType._fields = {
|
||||||
...autoGraphQLSchemaType._fields,
|
...autoGraphQLSchemaType.getFields(),
|
||||||
...customGraphQLSchemaType._fields,
|
...customGraphQLSchemaType.getFields(),
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
this.graphQLAutoSchema._typeMap[
|
||||||
|
customGraphQLSchemaType.name
|
||||||
|
] = customGraphQLSchemaType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.graphQLSchema = mergeSchemas({
|
this.graphQLSchema = mergeSchemas({
|
||||||
schemas: [
|
schemas: [
|
||||||
this.graphQLSchemaDirectivesDefinitions,
|
this.graphQLSchemaDirectivesDefinitions,
|
||||||
this.graphQLCustomTypeDefs,
|
|
||||||
this.graphQLAutoSchema,
|
this.graphQLAutoSchema,
|
||||||
],
|
],
|
||||||
mergeDirectives: true,
|
mergeDirectives: true,
|
||||||
@@ -271,24 +276,24 @@ class ParseGraphQLSchema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const graphQLSchemaTypeMap = this.graphQLSchema.getTypeMap();
|
const graphQLSchemaTypeMap = this.graphQLSchema.getTypeMap();
|
||||||
Object.keys(graphQLSchemaTypeMap).forEach(graphQLSchemaTypeName => {
|
Object.keys(graphQLSchemaTypeMap).forEach((graphQLSchemaTypeName) => {
|
||||||
const graphQLSchemaType = graphQLSchemaTypeMap[graphQLSchemaTypeName];
|
const graphQLSchemaType = graphQLSchemaTypeMap[graphQLSchemaTypeName];
|
||||||
if (
|
if (
|
||||||
typeof graphQLSchemaType.getFields === 'function' &&
|
typeof graphQLSchemaType.getFields === 'function' &&
|
||||||
this.graphQLCustomTypeDefs.definitions
|
this.graphQLCustomTypeDefs.definitions
|
||||||
) {
|
) {
|
||||||
const graphQLCustomTypeDef = this.graphQLCustomTypeDefs.definitions.find(
|
const graphQLCustomTypeDef = this.graphQLCustomTypeDefs.definitions.find(
|
||||||
definition => definition.name.value === graphQLSchemaTypeName
|
(definition) => definition.name.value === graphQLSchemaTypeName
|
||||||
);
|
);
|
||||||
if (graphQLCustomTypeDef) {
|
if (graphQLCustomTypeDef) {
|
||||||
const graphQLSchemaTypeFieldMap = graphQLSchemaType.getFields();
|
const graphQLSchemaTypeFieldMap = graphQLSchemaType.getFields();
|
||||||
Object.keys(graphQLSchemaTypeFieldMap).forEach(
|
Object.keys(graphQLSchemaTypeFieldMap).forEach(
|
||||||
graphQLSchemaTypeFieldName => {
|
(graphQLSchemaTypeFieldName) => {
|
||||||
const graphQLSchemaTypeField =
|
const graphQLSchemaTypeField =
|
||||||
graphQLSchemaTypeFieldMap[graphQLSchemaTypeFieldName];
|
graphQLSchemaTypeFieldMap[graphQLSchemaTypeFieldName];
|
||||||
if (!graphQLSchemaTypeField.astNode) {
|
if (!graphQLSchemaTypeField.astNode) {
|
||||||
const astNode = graphQLCustomTypeDef.fields.find(
|
const astNode = graphQLCustomTypeDef.fields.find(
|
||||||
field => field.name.value === graphQLSchemaTypeFieldName
|
(field) => field.name.value === graphQLSchemaTypeFieldName
|
||||||
);
|
);
|
||||||
if (astNode) {
|
if (astNode) {
|
||||||
graphQLSchemaTypeField.astNode = astNode;
|
graphQLSchemaTypeField.astNode = astNode;
|
||||||
@@ -319,7 +324,9 @@ class ParseGraphQLSchema {
|
|||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
(!ignoreReserved && RESERVED_GRAPHQL_TYPE_NAMES.includes(type.name)) ||
|
(!ignoreReserved && RESERVED_GRAPHQL_TYPE_NAMES.includes(type.name)) ||
|
||||||
this.graphQLTypes.find(existingType => existingType.name === type.name) ||
|
this.graphQLTypes.find(
|
||||||
|
(existingType) => existingType.name === type.name
|
||||||
|
) ||
|
||||||
(!ignoreConnection && type.name.endsWith('Connection'))
|
(!ignoreConnection && type.name.endsWith('Connection'))
|
||||||
) {
|
) {
|
||||||
const message = `Type ${type.name} could not be added to the auto schema because it collided with an existing type.`;
|
const message = `Type ${type.name} could not be added to the auto schema because it collided with an existing type.`;
|
||||||
@@ -409,7 +416,7 @@ class ParseGraphQLSchema {
|
|||||||
if (Array.isArray(enabledForClasses) || Array.isArray(disabledForClasses)) {
|
if (Array.isArray(enabledForClasses) || Array.isArray(disabledForClasses)) {
|
||||||
let includedClasses = allClasses;
|
let includedClasses = allClasses;
|
||||||
if (enabledForClasses) {
|
if (enabledForClasses) {
|
||||||
includedClasses = allClasses.filter(clazz => {
|
includedClasses = allClasses.filter((clazz) => {
|
||||||
return enabledForClasses.includes(clazz.className);
|
return enabledForClasses.includes(clazz.className);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -417,12 +424,12 @@ class ParseGraphQLSchema {
|
|||||||
// Classes included in `enabledForClasses` that
|
// Classes included in `enabledForClasses` that
|
||||||
// are also present in `disabledForClasses` will
|
// are also present in `disabledForClasses` will
|
||||||
// still be filtered out
|
// still be filtered out
|
||||||
includedClasses = includedClasses.filter(clazz => {
|
includedClasses = includedClasses.filter((clazz) => {
|
||||||
return !disabledForClasses.includes(clazz.className);
|
return !disabledForClasses.includes(clazz.className);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isUsersClassDisabled = !includedClasses.some(clazz => {
|
this.isUsersClassDisabled = !includedClasses.some((clazz) => {
|
||||||
return clazz.className === '_User';
|
return clazz.className === '_User';
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -467,11 +474,11 @@ class ParseGraphQLSchema {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return parseClasses.sort(sortClasses).map(parseClass => {
|
return parseClasses.sort(sortClasses).map((parseClass) => {
|
||||||
let parseClassConfig;
|
let parseClassConfig;
|
||||||
if (classConfigs) {
|
if (classConfigs) {
|
||||||
parseClassConfig = classConfigs.find(
|
parseClassConfig = classConfigs.find(
|
||||||
c => c.className === parseClass.className
|
(c) => c.className === parseClass.className
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return [parseClass, parseClassConfig];
|
return [parseClass, parseClassConfig];
|
||||||
@@ -479,7 +486,7 @@ class ParseGraphQLSchema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async _getFunctionNames() {
|
async _getFunctionNames() {
|
||||||
return await getFunctionNames(this.appId).filter(functionName => {
|
return await getFunctionNames(this.appId).filter((functionName) => {
|
||||||
if (/^[_a-zA-Z][_a-zA-Z0-9]*$/.test(functionName)) {
|
if (/^[_a-zA-Z][_a-zA-Z0-9]*$/.test(functionName)) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user