511 lines
14 KiB
JavaScript
Raw Normal View History

'use strict';
/**
* GraphQL.js service
*
* @description: A set of functions similar to controller's actions to avoid code duplication.
*/
const _ = require('lodash');
2019-11-28 16:10:19 +01:00
const DynamicZoneScalar = require('../types/dynamiczoneScalar');
const Aggregator = require('./Aggregator');
const Types = require('./Types.js');
const Schema = require('./Schema.js');
const {
mergeSchemas,
convertToParams,
convertToQuery,
amountLimiting,
} = require('./utils');
const { toSDL, getTypeDescription } = require('./schema-definitions');
const { toSingular, toPlural } = require('./naming');
const isQueryEnabled = (schema, name) => {
return _.get(schema, ['resolver', 'Query', name]) !== false;
};
const isMutationEnabled = (schema, name) => {
return _.get(schema, ['resolver', 'Mutation', name]) !== false;
};
const buildTypeDefObj = model => {
const { associations = [], attributes, primaryKey, globalId } = model;
const typeDef = {
id: 'ID!',
[primaryKey]: 'ID!',
};
// Add timestamps attributes.
if (_.isArray(_.get(model, 'options.timestamps'))) {
const [createdAtKey, updatedAtKey] = model.options.timestamps;
typeDef[createdAtKey] = 'DateTime!';
typeDef[updatedAtKey] = 'DateTime!';
}
Object.keys(attributes)
.filter(attributeName => attributes[attributeName].private !== true)
.forEach(attributeName => {
const attribute = attributes[attributeName];
// Convert our type to the GraphQL type.
typeDef[attributeName] = Types.convertType({
attribute,
modelName: globalId,
attributeName,
});
});
// Change field definition for collection relations
associations
.filter(association => association.type === 'collection')
.forEach(association => {
typeDef[
`${association.alias}(sort: String, limit: Int, start: Int, where: JSON)`
] = typeDef[association.alias];
delete typeDef[association.alias];
});
return typeDef;
2019-07-18 10:55:13 +02:00
};
2019-07-18 10:55:13 +02:00
const generateEnumDefinitions = (attributes, globalId) => {
return Object.keys(attributes)
.filter(attribute => attributes[attribute].type === 'enumeration')
.map(attribute => {
const definition = attributes[attribute];
2019-07-18 10:55:13 +02:00
const name = Types.convertEnumType(definition, globalId, attribute);
const values = definition.enum.map(v => `\t${v}`).join('\n');
return `enum ${name} {\n${values}\n}\n`;
})
2019-07-18 10:55:13 +02:00
.join('');
};
2019-11-28 15:33:37 +01:00
const generateDynamicZoneDefinitions = (attributes, globalId, schema) => {
Object.keys(attributes)
.filter(attribute => attributes[attribute].type === 'dynamiczone')
.forEach(attribute => {
const { components } = attributes[attribute];
const typeName = `${globalId}${_.upperFirst(
_.camelCase(attribute)
)}DynamicZone`;
if (components.length === 0) {
// Create dummy type because graphql doesn't support empty ones
2019-11-28 16:17:46 +01:00
schema.definition += `type ${typeName} { _:Boolean}`;
schema.definition += `\nscalar EmptyQuery\n`;
} else {
const componentsTypeNames = components.map(componentUID => {
const compo = strapi.components[componentUID];
if (!compo) {
throw new Error(
`Trying to creating dynamiczone type with unkown component ${componentUID}`
);
}
return compo.globalId;
});
const unionType = `union ${typeName} = ${componentsTypeNames.join(
' | '
)}`;
schema.definition += `\n${unionType}\n`;
}
2019-11-28 15:33:37 +01:00
const inputTypeName = `${typeName}Input`;
2019-11-28 16:17:46 +01:00
schema.definition += `\nscalar ${inputTypeName}\n`;
2019-11-28 15:33:37 +01:00
schema.resolvers[typeName] = {
__resolveType(obj) {
return strapi.components[obj.__component].globalId;
},
};
2019-11-28 16:10:19 +01:00
schema.resolvers[inputTypeName] = new DynamicZoneScalar({
2019-11-28 15:33:37 +01:00
name: inputTypeName,
2019-11-28 16:10:19 +01:00
attribute,
globalId,
components,
2019-11-28 15:33:37 +01:00
});
});
};
2019-12-10 16:21:21 +01:00
const buildAssocResolvers = model => {
2019-07-18 10:55:13 +02:00
const contentManager =
strapi.plugins['content-manager'].services['contentmanager'];
2019-07-18 10:55:13 +02:00
const { primaryKey, associations = [] } = model;
return associations
2019-11-28 15:33:37 +01:00
.filter(association => model.attributes[association.alias].private !== true)
.reduce((resolver, association) => {
2019-12-10 16:21:21 +01:00
const target = association.model || association.collection;
const targetModel = strapi.getModel(target, association.plugin);
2019-11-28 15:33:37 +01:00
switch (association.nature) {
2019-12-10 16:21:21 +01:00
case 'oneToManyMorph':
case 'manyMorphToOne':
case 'manyMorphToMany':
case 'manyToManyMorph': {
2019-11-28 15:33:37 +01:00
resolver[association.alias] = async obj => {
2019-12-10 16:21:21 +01:00
if (obj[association.alias]) {
return obj[association.alias];
}
2019-11-28 15:33:37 +01:00
const entry = await contentManager.fetch(
{
2019-07-18 10:55:13 +02:00
id: obj[primaryKey],
2019-12-10 16:21:21 +01:00
model: model.uid,
},
2019-11-28 15:33:37 +01:00
[association.alias]
);
2019-11-28 15:33:37 +01:00
return entry[association.alias];
};
2019-11-28 15:33:37 +01:00
break;
}
default: {
resolver[association.alias] = async (obj, options) => {
// Construct parameters object to retrieve the correct related entries.
const params = {
2019-12-10 16:21:21 +01:00
model: targetModel.uid,
2019-11-28 15:33:37 +01:00
};
2019-07-18 10:55:13 +02:00
2019-12-10 16:21:21 +01:00
let queryOpts = {};
2019-11-28 15:33:37 +01:00
if (association.type === 'model') {
2019-12-10 16:21:21 +01:00
params[targetModel.primaryKey] = _.get(
2019-11-28 15:33:37 +01:00
obj,
2019-12-10 16:21:21 +01:00
[association.alias, targetModel.primaryKey],
2019-10-01 17:56:52 +02:00
obj[association.alias]
2019-07-18 10:55:13 +02:00
);
} else {
const queryParams = amountLimiting(options);
2019-11-28 15:33:37 +01:00
queryOpts = {
...queryOpts,
...convertToParams(_.omit(queryParams, 'where')), // Convert filters (sort, limit and start/skip)
...convertToQuery(queryParams.where),
2019-11-28 15:33:37 +01:00
};
if (
((association.nature === 'manyToMany' &&
association.dominant) ||
association.nature === 'manyWay') &&
_.has(obj, association.alias) // if populated
) {
_.set(
queryOpts,
2019-12-10 16:21:21 +01:00
['query', targetModel.primaryKey],
2019-11-28 15:33:37 +01:00
obj[association.alias]
? obj[association.alias]
2019-12-10 16:21:21 +01:00
.map(val => val[targetModel.primaryKey] || val)
2019-11-28 15:33:37 +01:00
.sort()
: []
);
} else {
_.set(
queryOpts,
['query', association.via],
2019-12-10 16:21:21 +01:00
obj[targetModel.primaryKey]
2019-11-28 15:33:37 +01:00
);
}
2019-07-18 10:55:13 +02:00
}
2019-11-28 15:33:37 +01:00
return association.model
? strapi.plugins.graphql.services.loaders.loaders[
2019-12-10 16:21:21 +01:00
targetModel.uid
2019-11-28 15:33:37 +01:00
].load({
params,
options: queryOpts,
single: true,
})
: strapi.plugins.graphql.services.loaders.loaders[
2019-12-10 16:21:21 +01:00
targetModel.uid
2019-11-28 15:33:37 +01:00
].load({
options: queryOpts,
association,
});
};
break;
}
2019-07-18 10:55:13 +02:00
}
2019-11-28 15:33:37 +01:00
return resolver;
}, {});
2019-07-18 10:55:13 +02:00
};
/**
* Construct the GraphQL query & definition and apply the right resolvers.
*
* @return Object
*/
const buildModels = models => {
return models.map(model => {
const { kind, modelType } = model;
if (modelType === 'component') {
return buildComponent(model);
}
switch (kind) {
case 'singleType':
return buildSingleType(model);
default:
return buildCollectionType(model);
}
});
};
const buildModelDefinition = (model, globalType = {}) => {
const { globalId, primaryKey } = model;
const schema = {
definition: '',
query: {},
mutation: {},
resolvers: {
Query: {},
Mutation: {},
[globalId]: {
id: parent => parent[primaryKey] || parent.id,
...buildAssocResolvers(model),
},
},
};
const typeDefObj = buildTypeDefObj(model);
schema.definition += generateEnumDefinitions(model.attributes, globalId);
generateDynamicZoneDefinitions(model.attributes, globalId, schema);
const description = getTypeDescription(globalType, model);
const fields = toSDL(typeDefObj, globalType, model);
const typeDef = `${description}type ${globalId} {${fields}}\n`;
schema.definition += typeDef;
return schema;
};
const buildComponent = component => {
const { globalId } = component;
const schema = buildModelDefinition(component);
schema.definition += Types.generateInputModel(component, globalId, {
allowIds: true,
});
return schema;
};
const buildSingleType = model => {
const { uid, modelName } = model;
const singularName = toSingular(modelName);
const _schema = _.cloneDeep(
_.get(strapi.plugins, 'graphql.config._schema.graphql', {})
);
const globalType = _.get(_schema, ['type', model.globalId], {});
const localSchema = buildModelDefinition(model, globalType);
// Add definition to the schema but this type won't be "queriable" or "mutable".
if (globalType === false) {
return localSchema;
}
if (isQueryEnabled(_schema, singularName)) {
_.merge(localSchema, {
query: {
// TODO: support all the unique fields
[singularName]: model.globalId,
},
resolvers: {
Query: {
[singularName]: Schema.buildQuery(singularName, {
resolver: `${uid}.find`,
..._.get(_schema, ['resolver', 'Query', singularName], {}),
}),
},
},
});
}
// Add model Input definition.
localSchema.definition += Types.generateInputModel(model, modelName);
// build every mutation
['update', 'delete'].forEach(action => {
const mutationScheam = buildMutation({ model, action }, { _schema });
mergeSchemas(localSchema, mutationScheam);
});
return localSchema;
};
const buildCollectionType = model => {
const { globalId, plugin, modelName, uid } = model;
const singularName = toSingular(modelName);
const pluralName = toPlural(modelName);
const _schema = _.cloneDeep(
_.get(strapi.plugins, 'graphql.config._schema.graphql', {})
);
2019-07-17 13:13:07 +02:00
const globalType = _.get(_schema, ['type', model.globalId], {});
const localSchema = {
definition: '',
query: {},
mutation: {},
resolvers: {
Query: {},
Mutation: {},
// define default resolver for this model
[globalId]: {
id: parent => parent[model.primaryKey] || parent.id,
...buildAssocResolvers(model),
},
},
};
2019-07-18 10:55:13 +02:00
const typeDefObj = buildTypeDefObj(model);
localSchema.definition += generateEnumDefinitions(model.attributes, globalId);
generateDynamicZoneDefinitions(model.attributes, globalId, localSchema);
const description = getTypeDescription(globalType, model);
const fields = toSDL(typeDefObj, globalType, model);
const typeDef = `${description}type ${globalId} {${fields}}\n`;
localSchema.definition += typeDef;
// Add definition to the schema but this type won't be "queriable" or "mutable".
if (globalType === false) {
return localSchema;
}
if (isQueryEnabled(_schema, singularName)) {
_.merge(localSchema, {
query: {
[`${singularName}(id: ID!)`]: model.globalId,
},
resolvers: {
Query: {
[singularName]: Schema.buildQuery(singularName, {
resolver: `${uid}.findOne`,
..._.get(_schema, ['resolver', 'Query', singularName], {}),
}),
},
},
});
}
if (isQueryEnabled(_schema, pluralName)) {
const resolverOpts = {
resolver: `${uid}.find`,
..._.get(_schema, ['resolver', 'Query', pluralName], {}),
};
const resolverFn = Schema.buildQuery(pluralName, resolverOpts);
_.merge(localSchema, {
query: {
[`${pluralName}(sort: String, limit: Int, start: Int, where: JSON)`]: `[${model.globalId}]`,
},
resolvers: {
Query: {
[pluralName]: resolverFn,
},
},
});
// TODO: Add support for Graphql Aggregation in Bookshelf ORM
if (model.orm === 'mongoose') {
// Generation the aggregation for the given model
const aggregationSchema = Aggregator.formatModelConnectionsGQL({
fields: typeDefObj,
model,
name: modelName,
resolver: resolverOpts,
plugin,
});
mergeSchemas(localSchema, aggregationSchema);
}
}
// Add model Input definition.
localSchema.definition += Types.generateInputModel(model, modelName);
// build every mutation
['create', 'update', 'delete'].forEach(action => {
const mutationScheam = buildMutation({ model, action }, { _schema });
mergeSchemas(localSchema, mutationScheam);
});
return localSchema;
};
// TODO:
// - Implement batch methods (need to update the content-manager as well).
// - Implement nested transactional methods (create/update).
const buildMutation = ({ model, action }, { _schema }) => {
const { uid } = model;
const capitalizedName = _.upperFirst(toSingular(model.modelName));
const mutationName = `${action}${capitalizedName}`;
const definition = Types.generateInputPayloadArguments({
model,
name: model.modelName,
mutationName,
action,
});
// ignore if disabled
if (!isMutationEnabled(_schema, mutationName)) {
return {
definition,
};
}
const { kind } = model;
let mutationDef = `${mutationName}(input: ${mutationName}Input)`;
if (kind === 'singleType' && action === 'delete') {
mutationDef = mutationName;
}
return {
definition,
mutation: {
[mutationDef]: `${mutationName}Payload`,
},
resolvers: {
Mutation: {
[mutationName]: Schema.buildMutation(mutationName, {
resolver: `${uid}.${action}`,
transformOutput: result => ({
[toSingular(model.modelName)]: result,
}),
..._.get(_schema, ['resolver', 'Mutation', mutationName], {}),
}),
},
},
};
};
module.exports = {
buildModels,
};