UNPKG

@directus/api

Version:

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

442 lines (440 loc) 17 kB
import { createSubscriptionGenerator } from "../subscription.js"; import { dedupeRelationalResolver } from "../utils/dedupe-resolvers.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 { getGraphQLType } from "../../../utils/get-graphql-type.js"; import { resolveQuery } from "../resolvers/query.js"; import { GraphQLJsonFilter } from "../types/json-filter.js"; import { GraphQLStringOrFloat } from "../types/string-or-float.js"; import { getTypes } from "./get-types.js"; import { SYSTEM_DENY_LIST } from "./index.js"; import "../index.js"; import { GraphQLBoolean, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLString } from "graphql"; import { GraphQLJSON } from "graphql-compose"; //#region src/services/graphql/schema/read.ts /** * Create readable types and attach resolvers for each. Also prepares full filter argument structures */ 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) } } }); 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 JsonFilterOperators = schemaComposer.createInputTC({ name: "json_filter_operators", fields: { _json: { type: GraphQLJsonFilter }, _null: { type: GraphQLBoolean }, _nnull: { 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 GraphQLJSON: filterOperatorType = JsonFilterOperators; 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) => { switch (getGraphQLType(field.type, field.special)) { 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 } }; if (Object.values(collection.fields).some((field) => { const graphqlType = getGraphQLType(field.type, field.special); if (graphqlType === GraphQLInt || graphqlType === GraphQLFloat) return true; return false; })) 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: dedupeRelationalResolver(({ info }) => resolveQuery(gql, info)) }; 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: dedupeRelationalResolver(({ info }) => resolveQuery(gql, info)) }); 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: dedupeRelationalResolver(({ info }) => resolveQuery(gql, info)) }); 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: dedupeRelationalResolver(({ info }) => resolveQuery(gql, info)) }); 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 }; } //#endregion export { getReadableTypes };