UNPKG

@directus/api

Version:

Directus is a real-time API and App dashboard for managing SQL database content

738 lines (737 loc) 25.9 kB
import { GraphQLBoolean, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLString, } from 'graphql'; import { GraphQLJSON, InputTypeComposer, ObjectTypeComposer } from 'graphql-compose'; import { getGraphQLType } from '../../../utils/get-graphql-type.js'; import { GraphQLService } from '../index.js'; import { resolveQuery } from '../resolvers/query.js'; import { createSubscriptionGenerator } from '../subscription.js'; import { GraphQLBigInt } from '../types/bigint.js'; import { GraphQLDate } from '../types/date.js'; import { GraphQLGeoJSON } from '../types/geojson.js'; import { GraphQLHash } from '../types/hash.js'; import { GraphQLStringOrFloat } from '../types/string-or-float.js'; import { SYSTEM_DENY_LIST } from './index.js'; import { getTypes } from './get-types.js'; /** * Create readable types and attach resolvers for each. Also prepares full filter argument structures */ export async function getReadableTypes(gql, schemaComposer, schema, inconsistentFields) { const { CollectionTypes: ReadCollectionTypes, VersionTypes: VersionCollectionTypes } = getTypes(schemaComposer, gql.scope, schema, inconsistentFields, 'read'); const ReadableCollectionFilterTypes = {}; const ReadableCollectionQuantifierFilterTypes = {}; const AggregatedFunctions = {}; const AggregatedFields = {}; const AggregateMethods = {}; const IDFilterOperators = schemaComposer.createInputTC({ name: 'id_filter_operators', fields: { _eq: { type: GraphQLID, }, _neq: { type: GraphQLID, }, _contains: { type: GraphQLID, }, _icontains: { type: GraphQLID, }, _ncontains: { type: GraphQLID, }, _starts_with: { type: GraphQLID, }, _nstarts_with: { type: GraphQLID, }, _istarts_with: { type: GraphQLID, }, _nistarts_with: { type: GraphQLID, }, _ends_with: { type: GraphQLID, }, _nends_with: { type: GraphQLID, }, _iends_with: { type: GraphQLID, }, _niends_with: { type: GraphQLID, }, _in: { type: new GraphQLList(GraphQLID), }, _nin: { type: new GraphQLList(GraphQLID), }, _null: { type: GraphQLBoolean, }, _nnull: { type: GraphQLBoolean, }, _empty: { type: GraphQLBoolean, }, _nempty: { type: GraphQLBoolean, }, }, }); const StringFilterOperators = schemaComposer.createInputTC({ name: 'string_filter_operators', fields: { _eq: { type: GraphQLString, }, _neq: { type: GraphQLString, }, _contains: { type: GraphQLString, }, _icontains: { type: GraphQLString, }, _ncontains: { type: GraphQLString, }, _starts_with: { type: GraphQLString, }, _nstarts_with: { type: GraphQLString, }, _istarts_with: { type: GraphQLString, }, _nistarts_with: { type: GraphQLString, }, _ends_with: { type: GraphQLString, }, _nends_with: { type: GraphQLString, }, _iends_with: { type: GraphQLString, }, _niends_with: { type: GraphQLString, }, _in: { type: new GraphQLList(GraphQLString), }, _nin: { type: new GraphQLList(GraphQLString), }, _null: { type: GraphQLBoolean, }, _nnull: { type: GraphQLBoolean, }, _empty: { type: GraphQLBoolean, }, _nempty: { type: GraphQLBoolean, }, }, }); const BooleanFilterOperators = schemaComposer.createInputTC({ name: 'boolean_filter_operators', fields: { _eq: { type: GraphQLBoolean, }, _neq: { type: GraphQLBoolean, }, _null: { type: GraphQLBoolean, }, _nnull: { type: GraphQLBoolean, }, }, }); const DateFilterOperators = schemaComposer.createInputTC({ name: 'date_filter_operators', fields: { _eq: { type: GraphQLString, }, _neq: { type: GraphQLString, }, _gt: { type: GraphQLString, }, _gte: { type: GraphQLString, }, _lt: { type: GraphQLString, }, _lte: { type: GraphQLString, }, _null: { type: GraphQLBoolean, }, _nnull: { type: GraphQLBoolean, }, _in: { type: new GraphQLList(GraphQLString), }, _nin: { type: new GraphQLList(GraphQLString), }, _between: { type: new GraphQLList(GraphQLStringOrFloat), }, _nbetween: { type: new GraphQLList(GraphQLStringOrFloat), }, }, }); // Uses StringOrFloat rather than Float to support api dynamic variables (like `$NOW`) const NumberFilterOperators = schemaComposer.createInputTC({ name: 'number_filter_operators', fields: { _eq: { type: GraphQLStringOrFloat, }, _neq: { type: GraphQLStringOrFloat, }, _in: { type: new GraphQLList(GraphQLStringOrFloat), }, _nin: { type: new GraphQLList(GraphQLStringOrFloat), }, _gt: { type: GraphQLStringOrFloat, }, _gte: { type: GraphQLStringOrFloat, }, _lt: { type: GraphQLStringOrFloat, }, _lte: { type: GraphQLStringOrFloat, }, _null: { type: GraphQLBoolean, }, _nnull: { type: GraphQLBoolean, }, _between: { type: new GraphQLList(GraphQLStringOrFloat), }, _nbetween: { type: new GraphQLList(GraphQLStringOrFloat), }, }, }); const BigIntFilterOperators = schemaComposer.createInputTC({ name: 'big_int_filter_operators', fields: { _eq: { type: GraphQLBigInt, }, _neq: { type: GraphQLBigInt, }, _in: { type: new GraphQLList(GraphQLBigInt), }, _nin: { type: new GraphQLList(GraphQLBigInt), }, _gt: { type: GraphQLBigInt, }, _gte: { type: GraphQLBigInt, }, _lt: { type: GraphQLBigInt, }, _lte: { type: GraphQLBigInt, }, _null: { type: GraphQLBoolean, }, _nnull: { type: GraphQLBoolean, }, _between: { type: new GraphQLList(GraphQLBigInt), }, _nbetween: { type: new GraphQLList(GraphQLBigInt), }, }, }); const GeometryFilterOperators = schemaComposer.createInputTC({ name: 'geometry_filter_operators', fields: { _eq: { type: GraphQLGeoJSON, }, _neq: { type: GraphQLGeoJSON, }, _intersects: { type: GraphQLGeoJSON, }, _nintersects: { type: GraphQLGeoJSON, }, _intersects_bbox: { type: GraphQLGeoJSON, }, _nintersects_bbox: { type: GraphQLGeoJSON, }, _null: { type: GraphQLBoolean, }, _nnull: { type: GraphQLBoolean, }, }, }); const HashFilterOperators = schemaComposer.createInputTC({ name: 'hash_filter_operators', fields: { _null: { type: GraphQLBoolean, }, _nnull: { type: GraphQLBoolean, }, _empty: { type: GraphQLBoolean, }, _nempty: { type: GraphQLBoolean, }, }, }); const CountFunctionFilterOperators = schemaComposer.createInputTC({ name: 'count_function_filter_operators', fields: { count: { type: NumberFilterOperators, }, }, }); const DateFunctionFilterOperators = schemaComposer.createInputTC({ name: 'date_function_filter_operators', fields: { year: { type: NumberFilterOperators, }, month: { type: NumberFilterOperators, }, week: { type: NumberFilterOperators, }, day: { type: NumberFilterOperators, }, weekday: { type: NumberFilterOperators, }, }, }); const TimeFunctionFilterOperators = schemaComposer.createInputTC({ name: 'time_function_filter_operators', fields: { hour: { type: NumberFilterOperators, }, minute: { type: NumberFilterOperators, }, second: { type: NumberFilterOperators, }, }, }); const DateTimeFunctionFilterOperators = schemaComposer.createInputTC({ name: 'datetime_function_filter_operators', fields: { ...DateFunctionFilterOperators.getFields(), ...TimeFunctionFilterOperators.getFields(), }, }); const subscriptionEventType = schemaComposer.createEnumTC({ name: 'EventEnum', values: { create: { value: 'create' }, update: { value: 'update' }, delete: { value: 'delete' }, }, }); for (const collection of Object.values(schema.read.collections)) { if (Object.keys(collection.fields).length === 0) continue; if (SYSTEM_DENY_LIST.includes(collection.collection)) continue; ReadableCollectionFilterTypes[collection.collection] = schemaComposer.createInputTC({ name: `${collection.collection}_filter`, fields: Object.values(collection.fields).reduce((acc, field) => { const graphqlType = getGraphQLType(field.type, field.special); let filterOperatorType; switch (graphqlType) { case GraphQLBoolean: filterOperatorType = BooleanFilterOperators; break; case GraphQLBigInt: filterOperatorType = BigIntFilterOperators; break; case GraphQLInt: case GraphQLFloat: filterOperatorType = NumberFilterOperators; break; case GraphQLDate: filterOperatorType = DateFilterOperators; break; case GraphQLGeoJSON: filterOperatorType = GeometryFilterOperators; break; case GraphQLHash: filterOperatorType = HashFilterOperators; break; case GraphQLID: filterOperatorType = IDFilterOperators; break; default: filterOperatorType = StringFilterOperators; } acc[field.field] = filterOperatorType; if (field.type === 'date') { acc[`${field.field}_func`] = { type: DateFunctionFilterOperators, }; } if (field.type === 'time') { acc[`${field.field}_func`] = { type: TimeFunctionFilterOperators, }; } if (field.type === 'dateTime' || field.type === 'timestamp') { acc[`${field.field}_func`] = { type: DateTimeFunctionFilterOperators, }; } if (field.type === 'json' || field.type === 'alias') { acc[`${field.field}_func`] = { type: CountFunctionFilterOperators, }; } return acc; }, {}), }); ReadableCollectionFilterTypes[collection.collection].addFields({ _and: [ReadableCollectionFilterTypes[collection.collection]], _or: [ReadableCollectionFilterTypes[collection.collection]], }); AggregatedFields[collection.collection] = schemaComposer.createObjectTC({ name: `${collection.collection}_aggregated_fields`, fields: Object.values(collection.fields).reduce((acc, field) => { const graphqlType = getGraphQLType(field.type, field.special); switch (graphqlType) { case GraphQLBigInt: case GraphQLInt: case GraphQLFloat: acc[field.field] = { type: GraphQLFloat, description: field.note, }; break; default: break; } return acc; }, {}), }); const countType = schemaComposer.createObjectTC({ name: `${collection.collection}_aggregated_count`, fields: Object.values(collection.fields).reduce((acc, field) => { acc[field.field] = { type: GraphQLInt, description: field.note, }; return acc; }, {}), }); AggregateMethods[collection.collection] = { group: { name: 'group', type: GraphQLJSON, }, countAll: { name: 'countAll', type: GraphQLInt, }, count: { name: 'count', type: countType, }, countDistinct: { name: 'countDistinct', type: countType, }, }; const hasNumericAggregates = Object.values(collection.fields).some((field) => { const graphqlType = getGraphQLType(field.type, field.special); if (graphqlType === GraphQLInt || graphqlType === GraphQLFloat) { return true; } return false; }); if (hasNumericAggregates) { Object.assign(AggregateMethods[collection.collection], { avg: { name: 'avg', type: AggregatedFields[collection.collection], }, sum: { name: 'sum', type: AggregatedFields[collection.collection], }, avgDistinct: { name: 'avgDistinct', type: AggregatedFields[collection.collection], }, sumDistinct: { name: 'sumDistinct', type: AggregatedFields[collection.collection], }, min: { name: 'min', type: AggregatedFields[collection.collection], }, max: { name: 'max', type: AggregatedFields[collection.collection], }, }); } AggregatedFunctions[collection.collection] = schemaComposer.createObjectTC({ name: `${collection.collection}_aggregated`, fields: AggregateMethods[collection.collection], }); const resolver = { name: collection.collection, type: collection.singleton ? ReadCollectionTypes[collection.collection] : new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(ReadCollectionTypes[collection.collection].getType()))), resolve: async ({ info, context }) => { const result = await resolveQuery(gql, info); context['data'] = result; return result; }, }; if (collection.singleton === false) { resolver.args = { filter: ReadableCollectionFilterTypes[collection.collection], sort: { type: new GraphQLList(GraphQLString), }, limit: { type: GraphQLInt, }, offset: { type: GraphQLInt, }, page: { type: GraphQLInt, }, search: { type: GraphQLString, }, }; } else { resolver.args = { version: GraphQLString, }; } ReadCollectionTypes[collection.collection].addResolver(resolver); ReadCollectionTypes[collection.collection].addResolver({ name: `${collection.collection}_aggregated`, type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(AggregatedFunctions[collection.collection].getType()))), args: { groupBy: new GraphQLList(GraphQLString), filter: ReadableCollectionFilterTypes[collection.collection], limit: { type: GraphQLInt, }, offset: { type: GraphQLInt, }, page: { type: GraphQLInt, }, search: { type: GraphQLString, }, sort: { type: new GraphQLList(GraphQLString), }, }, resolve: async ({ info, context }) => { const result = await resolveQuery(gql, info); context['data'] = result; return result; }, }); if (collection.singleton === false) { ReadCollectionTypes[collection.collection].addResolver({ name: `${collection.collection}_by_id`, type: ReadCollectionTypes[collection.collection], args: { id: new GraphQLNonNull(GraphQLID), version: GraphQLString, }, resolve: async ({ info, context }) => { const result = await resolveQuery(gql, info); context['data'] = result; return result; }, }); } if (gql.scope === 'items') { VersionCollectionTypes[collection.collection].addResolver({ name: `${collection.collection}_by_version`, type: VersionCollectionTypes[collection.collection], args: collection.singleton ? { version: new GraphQLNonNull(GraphQLString) } : { version: new GraphQLNonNull(GraphQLString), id: new GraphQLNonNull(GraphQLID), }, resolve: async ({ info, context }) => { const result = await resolveQuery(gql, info); context['data'] = result; return result; }, }); } const eventName = `${collection.collection}_mutated`; if (collection.collection in ReadCollectionTypes) { const subscriptionType = schemaComposer.createObjectTC({ name: eventName, fields: { key: new GraphQLNonNull(GraphQLID), event: subscriptionEventType, data: ReadCollectionTypes[collection.collection], }, }); schemaComposer.Subscription.addFields({ [eventName]: { type: subscriptionType, args: { event: subscriptionEventType, }, subscribe: createSubscriptionGenerator(gql, eventName), }, }); } } for (const collection in ReadableCollectionFilterTypes) { const quantifier_collection = ReadableCollectionFilterTypes[collection]?.clone(`${collection}_quantifier_filter`); quantifier_collection?.addFields({ _some: ReadableCollectionFilterTypes[collection], _none: ReadableCollectionFilterTypes[collection], }); ReadableCollectionQuantifierFilterTypes[collection] = quantifier_collection; } for (const relation of schema.read.relations) { if (relation.related_collection) { if (SYSTEM_DENY_LIST.includes(relation.related_collection)) continue; ReadableCollectionQuantifierFilterTypes[relation.collection]?.addFields({ [relation.field]: ReadableCollectionFilterTypes[relation.related_collection], }); ReadableCollectionFilterTypes[relation.collection]?.addFields({ [relation.field]: ReadableCollectionFilterTypes[relation.related_collection], }); ReadCollectionTypes[relation.collection]?.addFieldArgs(relation.field, { filter: ReadableCollectionFilterTypes[relation.related_collection], sort: { type: new GraphQLList(GraphQLString), }, limit: { type: GraphQLInt, }, offset: { type: GraphQLInt, }, page: { type: GraphQLInt, }, search: { type: GraphQLString, }, }); if (relation.meta?.one_field) { ReadableCollectionQuantifierFilterTypes[relation.related_collection]?.addFields({ [relation.meta.one_field]: ReadableCollectionQuantifierFilterTypes[relation.collection], }); ReadableCollectionFilterTypes[relation.related_collection]?.addFields({ [relation.meta.one_field]: ReadableCollectionQuantifierFilterTypes[relation.collection], }); ReadCollectionTypes[relation.related_collection]?.addFieldArgs(relation.meta.one_field, { filter: ReadableCollectionFilterTypes[relation.collection], sort: { type: new GraphQLList(GraphQLString), }, limit: { type: GraphQLInt, }, offset: { type: GraphQLInt, }, page: { type: GraphQLInt, }, search: { type: GraphQLString, }, }); } } else if (relation.meta?.one_allowed_collections) { ReadableCollectionQuantifierFilterTypes[relation.collection]?.removeField(relation.field); ReadableCollectionFilterTypes[relation.collection]?.removeField(relation.field); for (const collection of relation.meta.one_allowed_collections) { ReadableCollectionQuantifierFilterTypes[relation.collection]?.addFields({ [`${relation.field}__${collection}`]: ReadableCollectionFilterTypes[collection], }); ReadableCollectionFilterTypes[relation.collection]?.addFields({ [`${relation.field}__${collection}`]: ReadableCollectionFilterTypes[collection], }); } } } return { ReadCollectionTypes, VersionCollectionTypes, ReadableCollectionFilterTypes }; }