mirror of
https://github.com/directus/directus.git
synced 2026-02-01 16:24:59 -05:00
556 lines
16 KiB
TypeScript
556 lines
16 KiB
TypeScript
import Knex from 'knex';
|
|
import database from '../database';
|
|
import { AbstractServiceOptions, Accountability, Collection, Field, Relation, Query, SchemaOverview } from '../types';
|
|
import {
|
|
GraphQLString,
|
|
GraphQLSchema,
|
|
GraphQLObjectType,
|
|
GraphQLList,
|
|
GraphQLResolveInfo,
|
|
GraphQLInputObjectType,
|
|
ObjectFieldNode,
|
|
GraphQLID,
|
|
FieldNode,
|
|
GraphQLFieldConfigMap,
|
|
GraphQLInt,
|
|
IntValueNode,
|
|
StringValueNode,
|
|
BooleanValueNode,
|
|
ArgumentNode,
|
|
GraphQLBoolean,
|
|
ObjectValueNode,
|
|
GraphQLUnionType,
|
|
} from 'graphql';
|
|
import { getGraphQLType } from '../utils/get-graphql-type';
|
|
import { RelationsService } from './relations';
|
|
import { ItemsService } from './items';
|
|
import { cloneDeep } from 'lodash';
|
|
import { sanitizeQuery } from '../utils/sanitize-query';
|
|
|
|
import { ActivityService } from './activity';
|
|
import { CollectionsService } from './collections';
|
|
import { FieldsService } from './fields';
|
|
import { FilesService } from './files';
|
|
import { FoldersService } from './folders';
|
|
import { PermissionsService } from './permissions';
|
|
import { PresetsService } from './presets';
|
|
import { RevisionsService } from './revisions';
|
|
import { RolesService } from './roles';
|
|
import { SettingsService } from './settings';
|
|
import { UsersService } from './users';
|
|
import { WebhooksService } from './webhooks';
|
|
|
|
import { getRelationType } from '../utils/get-relation-type';
|
|
import { systemCollectionRows } from '../database/system-data/collections';
|
|
|
|
export class GraphQLService {
|
|
accountability: Accountability | null;
|
|
knex: Knex;
|
|
fieldsService: FieldsService;
|
|
collectionsService: CollectionsService;
|
|
relationsService: RelationsService;
|
|
schema: SchemaOverview;
|
|
|
|
constructor(options: AbstractServiceOptions) {
|
|
this.accountability = options?.accountability || null;
|
|
this.knex = options?.knex || database;
|
|
this.fieldsService = new FieldsService(options);
|
|
this.collectionsService = new CollectionsService(options);
|
|
this.relationsService = new RelationsService(options);
|
|
this.schema = options.schema;
|
|
}
|
|
|
|
args = {
|
|
sort: {
|
|
type: GraphQLString,
|
|
},
|
|
limit: {
|
|
type: GraphQLInt,
|
|
},
|
|
offset: {
|
|
type: GraphQLInt,
|
|
},
|
|
page: {
|
|
type: GraphQLInt,
|
|
},
|
|
search: {
|
|
type: GraphQLString,
|
|
},
|
|
};
|
|
|
|
async getSchema() {
|
|
const collectionsInSystem = await this.collectionsService.readByQuery();
|
|
const fieldsInSystem = await this.fieldsService.readAll();
|
|
const relationsInSystem = (await this.relationsService.readByQuery({})) as Relation[];
|
|
|
|
const schema = this.getGraphQLSchema(collectionsInSystem, fieldsInSystem, relationsInSystem);
|
|
|
|
return schema;
|
|
}
|
|
|
|
getGraphQLSchema(collections: Collection[], fields: Field[], relations: Relation[]) {
|
|
const filterTypes = this.getFilterArgs(collections, fields, relations);
|
|
const schema: any = { items: {} };
|
|
|
|
for (const collection of collections) {
|
|
const systemCollection = collection.collection.startsWith('directus_');
|
|
|
|
const schemaSection: any = {
|
|
type: new GraphQLObjectType({
|
|
name: collection.collection,
|
|
description: collection.meta?.note,
|
|
fields: () => {
|
|
const fieldsObject: GraphQLFieldConfigMap<any, any> = {};
|
|
const fieldsInCollection = fields.filter((field) => field.collection === collection.collection);
|
|
|
|
for (const field of fieldsInCollection) {
|
|
const relationForField = relations.find((relation) => {
|
|
return (
|
|
(relation.many_collection === collection.collection && relation.many_field === field.field) ||
|
|
(relation.one_collection === collection.collection && relation.one_field === field.field)
|
|
);
|
|
});
|
|
|
|
if (relationForField) {
|
|
const relationType = getRelationType({
|
|
relation: relationForField,
|
|
collection: collection.collection,
|
|
field: field.field,
|
|
});
|
|
|
|
if (relationType === 'm2o') {
|
|
const relatedIsSystem = relationForField.one_collection!.startsWith('directus_');
|
|
|
|
const relatedType = relatedIsSystem
|
|
? schema[relationForField.one_collection!.substring(9)].type
|
|
: schema.items[relationForField.one_collection!].type;
|
|
|
|
fieldsObject[field.field] = {
|
|
type: relatedType,
|
|
};
|
|
} else if (relationType === 'o2m') {
|
|
const relatedIsSystem = relationForField.many_collection.startsWith('directus_');
|
|
|
|
const relatedType = relatedIsSystem
|
|
? schema[relationForField.many_collection.substring(9)].type
|
|
: schema.items[relationForField.many_collection].type;
|
|
|
|
fieldsObject[field.field] = {
|
|
type: new GraphQLList(relatedType),
|
|
args: {
|
|
...this.args,
|
|
filter: {
|
|
type: filterTypes[relationForField.many_collection],
|
|
},
|
|
},
|
|
};
|
|
} else if (relationType === 'm2a') {
|
|
const relatedCollections = relationForField.one_allowed_collections!;
|
|
|
|
const types: any = [];
|
|
|
|
for (const relatedCollection of relatedCollections) {
|
|
const relatedType = relatedCollection.startsWith('directus_')
|
|
? schema[relatedCollection.substring(9)].type
|
|
: schema.items[relatedCollection].type;
|
|
|
|
types.push(relatedType);
|
|
}
|
|
|
|
fieldsObject[field.field] = {
|
|
type: new GraphQLUnionType({
|
|
name: field.field,
|
|
types,
|
|
resolveType(value, _, info) {
|
|
/**
|
|
* @TODO figure out a way to reach the parent level
|
|
* to be able to read one_collection_field
|
|
*/
|
|
return types[0];
|
|
},
|
|
}),
|
|
};
|
|
}
|
|
} else {
|
|
fieldsObject[field.field] = {
|
|
type: field.schema?.is_primary_key ? GraphQLID : getGraphQLType(field.type),
|
|
};
|
|
}
|
|
|
|
fieldsObject[field.field].description = field.meta?.note;
|
|
}
|
|
|
|
return fieldsObject;
|
|
},
|
|
}),
|
|
resolve: (source: any, args: any, context: any, info: GraphQLResolveInfo) => {
|
|
return this.resolve(info);
|
|
},
|
|
args: {
|
|
...this.args,
|
|
filter: {
|
|
name: `${collection.collection}_filter`,
|
|
type: filterTypes[collection.collection],
|
|
},
|
|
},
|
|
};
|
|
|
|
if (systemCollection) {
|
|
schema[collection.collection.substring(9)] = schemaSection;
|
|
} else {
|
|
schema.items[collection.collection] = schemaSection;
|
|
}
|
|
}
|
|
|
|
const schemaWithLists = cloneDeep(schema);
|
|
|
|
for (const collection of collections) {
|
|
if (collection.meta?.singleton !== true) {
|
|
const systemCollection = collection.collection.startsWith('directus_');
|
|
|
|
if (systemCollection) {
|
|
schemaWithLists[collection.collection.substring(9)].type = new GraphQLList(
|
|
schemaWithLists[collection.collection.substring(9)].type
|
|
);
|
|
} else {
|
|
schemaWithLists.items[collection.collection].type = new GraphQLList(
|
|
schemaWithLists.items[collection.collection].type
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
const queryBase: any = {
|
|
name: 'Directus',
|
|
fields: {
|
|
server: {
|
|
type: new GraphQLObjectType({
|
|
name: 'server',
|
|
fields: {
|
|
ping: {
|
|
type: GraphQLString,
|
|
resolve: () => 'pong',
|
|
},
|
|
},
|
|
}),
|
|
resolve: () => ({}),
|
|
},
|
|
},
|
|
};
|
|
|
|
if (Object.keys(schemaWithLists.items).length > 0) {
|
|
queryBase.fields.items = {
|
|
type: new GraphQLObjectType({
|
|
name: 'items',
|
|
fields: schemaWithLists.items,
|
|
}),
|
|
resolve: () => ({}),
|
|
};
|
|
}
|
|
|
|
return new GraphQLSchema({
|
|
query: new GraphQLObjectType(queryBase),
|
|
});
|
|
}
|
|
|
|
getFilterArgs(collections: Collection[], fields: Field[], relations: Relation[]) {
|
|
const filterTypes: any = {};
|
|
|
|
for (const collection of collections) {
|
|
filterTypes[collection.collection] = new GraphQLInputObjectType({
|
|
name: `${collection.collection}_filter`,
|
|
fields: () => {
|
|
const filterFields: any = {
|
|
_and: {
|
|
type: new GraphQLList(filterTypes[collection.collection]),
|
|
},
|
|
_or: {
|
|
type: new GraphQLList(filterTypes[collection.collection]),
|
|
},
|
|
};
|
|
|
|
const fieldsInCollection = fields.filter((field) => field.collection === collection.collection);
|
|
|
|
for (const field of fieldsInCollection) {
|
|
const relationForField = relations.find((relation) => {
|
|
return (
|
|
(relation.many_collection === collection.collection && relation.many_field === field.field) ||
|
|
(relation.one_collection === collection.collection && relation.one_field === field.field)
|
|
);
|
|
});
|
|
|
|
if (relationForField) {
|
|
const relationType = getRelationType({
|
|
relation: relationForField,
|
|
collection: collection.collection,
|
|
field: field.field,
|
|
});
|
|
|
|
if (relationType === 'm2o') {
|
|
const relatedType = filterTypes[relationForField.one_collection!];
|
|
|
|
filterFields[field.field] = {
|
|
type: relatedType,
|
|
};
|
|
} else if (relationType === 'o2m') {
|
|
const relatedType = filterTypes[relationForField.many_collection];
|
|
|
|
filterFields[field.field] = {
|
|
type: relatedType,
|
|
};
|
|
}
|
|
/** @TODO M2A — Handle m2a case here */
|
|
/** @TODO
|
|
* Figure out how to setup filter fields for a union type output
|
|
*/
|
|
} else {
|
|
const fieldType = field.schema?.is_primary_key ? GraphQLID : getGraphQLType(field.type);
|
|
|
|
filterFields[field.field] = {
|
|
type: new GraphQLInputObjectType({
|
|
name: `${collection.collection}_${field.field}_filter_operators`,
|
|
fields: {
|
|
/* @todo make this a little smarter by only including filters that work with current type */
|
|
_eq: {
|
|
type: fieldType,
|
|
},
|
|
_neq: {
|
|
type: fieldType,
|
|
},
|
|
_contains: {
|
|
type: fieldType,
|
|
},
|
|
_ncontains: {
|
|
type: fieldType,
|
|
},
|
|
_in: {
|
|
type: new GraphQLList(fieldType),
|
|
},
|
|
_nin: {
|
|
type: new GraphQLList(fieldType),
|
|
},
|
|
_gt: {
|
|
type: fieldType,
|
|
},
|
|
_gte: {
|
|
type: fieldType,
|
|
},
|
|
_lt: {
|
|
type: fieldType,
|
|
},
|
|
_lte: {
|
|
type: fieldType,
|
|
},
|
|
_null: {
|
|
type: GraphQLBoolean,
|
|
},
|
|
_nnull: {
|
|
type: GraphQLBoolean,
|
|
},
|
|
_empty: {
|
|
type: GraphQLBoolean,
|
|
},
|
|
_nempty: {
|
|
type: GraphQLBoolean,
|
|
},
|
|
},
|
|
}),
|
|
};
|
|
}
|
|
}
|
|
|
|
return filterFields;
|
|
},
|
|
});
|
|
}
|
|
|
|
return filterTypes;
|
|
}
|
|
|
|
async resolve(info: GraphQLResolveInfo) {
|
|
const systemField = info.path.prev?.key !== 'items';
|
|
|
|
const collection = systemField ? `directus_${info.fieldName}` : info.fieldName;
|
|
|
|
const selections = info.fieldNodes[0]?.selectionSet?.selections?.filter((node) => node.kind === 'Field') as
|
|
| FieldNode[]
|
|
| undefined;
|
|
|
|
if (!selections) return null;
|
|
|
|
return await this.getData(collection, selections, info.fieldNodes[0].arguments || [], info.variableValues);
|
|
}
|
|
|
|
async getData(
|
|
collection: string,
|
|
selections: FieldNode[],
|
|
argsArray: readonly ArgumentNode[],
|
|
variableValues: GraphQLResolveInfo['variableValues']
|
|
) {
|
|
const args: Record<string, any> = this.parseArgs(argsArray, variableValues);
|
|
|
|
const query: Query = sanitizeQuery(args, this.accountability);
|
|
|
|
const parseFields = (selections: FieldNode[], parent?: string): string[] => {
|
|
const fields: string[] = [];
|
|
|
|
for (const selection of selections) {
|
|
const current = parent ? `${parent}.${selection.name.value}` : selection.name.value;
|
|
|
|
if (selection.selectionSet === undefined) {
|
|
fields.push(current);
|
|
} else {
|
|
const children = parseFields(
|
|
selection.selectionSet.selections.filter((selection) => selection.kind === 'Field') as FieldNode[],
|
|
current
|
|
);
|
|
fields.push(...children);
|
|
}
|
|
|
|
if (selection.arguments && selection.arguments.length > 0) {
|
|
if (!query.deep) query.deep = {};
|
|
|
|
const args: Record<string, any> = this.parseArgs(selection.arguments, variableValues);
|
|
query.deep[current] = sanitizeQuery(args, this.accountability);
|
|
}
|
|
}
|
|
|
|
return fields;
|
|
};
|
|
|
|
query.fields = parseFields(selections.filter((selection) => selection.kind === 'Field') as FieldNode[]);
|
|
|
|
let service: ItemsService;
|
|
|
|
switch (collection) {
|
|
case 'directus_activity':
|
|
service = new ActivityService({
|
|
knex: this.knex,
|
|
accountability: this.accountability,
|
|
schema: this.schema,
|
|
});
|
|
// case 'directus_collections':
|
|
// service = new CollectionsService({ knex: this.knex, accountability: this.accountability });
|
|
// case 'directus_fields':
|
|
// service = new FieldsService({ knex: this.knex, accountability: this.accountability });
|
|
case 'directus_files':
|
|
service = new FilesService({
|
|
knex: this.knex,
|
|
accountability: this.accountability,
|
|
schema: this.schema,
|
|
});
|
|
case 'directus_folders':
|
|
service = new FoldersService({
|
|
knex: this.knex,
|
|
accountability: this.accountability,
|
|
schema: this.schema,
|
|
});
|
|
case 'directus_folders':
|
|
service = new FoldersService({
|
|
knex: this.knex,
|
|
accountability: this.accountability,
|
|
schema: this.schema,
|
|
});
|
|
case 'directus_permissions':
|
|
service = new PermissionsService({
|
|
knex: this.knex,
|
|
accountability: this.accountability,
|
|
schema: this.schema,
|
|
});
|
|
case 'directus_presets':
|
|
service = new PresetsService({
|
|
knex: this.knex,
|
|
accountability: this.accountability,
|
|
schema: this.schema,
|
|
});
|
|
case 'directus_relations':
|
|
service = new RelationsService({
|
|
knex: this.knex,
|
|
accountability: this.accountability,
|
|
schema: this.schema,
|
|
});
|
|
case 'directus_revisions':
|
|
service = new RevisionsService({
|
|
knex: this.knex,
|
|
accountability: this.accountability,
|
|
schema: this.schema,
|
|
});
|
|
case 'directus_roles':
|
|
service = new RolesService({
|
|
knex: this.knex,
|
|
accountability: this.accountability,
|
|
schema: this.schema,
|
|
});
|
|
case 'directus_settings':
|
|
service = new SettingsService({
|
|
knex: this.knex,
|
|
accountability: this.accountability,
|
|
schema: this.schema,
|
|
});
|
|
case 'directus_users':
|
|
service = new UsersService({
|
|
knex: this.knex,
|
|
accountability: this.accountability,
|
|
schema: this.schema,
|
|
});
|
|
case 'directus_webhooks':
|
|
service = new WebhooksService({
|
|
knex: this.knex,
|
|
accountability: this.accountability,
|
|
schema: this.schema,
|
|
});
|
|
default:
|
|
service = new ItemsService(collection, {
|
|
knex: this.knex,
|
|
accountability: this.accountability,
|
|
schema: this.schema,
|
|
});
|
|
}
|
|
|
|
const collectionInfo =
|
|
(await this.knex.select('singleton').from('directus_collections').where({ collection: collection }).first()) ||
|
|
systemCollectionRows.find((collectionMeta) => collectionMeta?.collection === collection);
|
|
|
|
const result = collectionInfo?.singleton ? await service.readSingleton(query) : await service.readByQuery(query);
|
|
|
|
return result;
|
|
}
|
|
|
|
parseArgs(
|
|
args: readonly ArgumentNode[] | readonly ObjectFieldNode[],
|
|
variableValues: GraphQLResolveInfo['variableValues']
|
|
): Record<string, any> {
|
|
if (!args || args.length === 0) return {};
|
|
|
|
const parseObjectValue = (arg: ObjectValueNode) => {
|
|
return this.parseArgs(arg.fields, variableValues);
|
|
};
|
|
|
|
const argsObject: any = {};
|
|
|
|
for (const argument of args) {
|
|
if (argument.value.kind === 'ObjectValue') {
|
|
argsObject[argument.name.value] = parseObjectValue(argument.value);
|
|
} else if (argument.value.kind === 'Variable') {
|
|
argsObject[argument.name.value] = variableValues[argument.value.name.value];
|
|
} else if (argument.value.kind === 'ListValue') {
|
|
const values: any = [];
|
|
|
|
for (const valueNode of argument.value.values) {
|
|
if (valueNode.kind === 'ObjectValue') {
|
|
values.push(this.parseArgs(valueNode.fields, variableValues));
|
|
} else {
|
|
values.push((valueNode as any).value);
|
|
}
|
|
}
|
|
|
|
argsObject[argument.name.value] = values;
|
|
} else {
|
|
argsObject[argument.name.value] = (argument.value as IntValueNode | StringValueNode | BooleanValueNode).value;
|
|
}
|
|
}
|
|
|
|
return argsObject;
|
|
}
|
|
}
|