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