@directus/api
Version:
Directus is a real-time API and App dashboard for managing SQL database content
442 lines (440 loc) • 17 kB
JavaScript
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 };