1
import { Action, FUNCTIONS } from '@directus/constants';
2
import { useEnv } from '@directus/env';
3
import { ErrorCode, ForbiddenError, InvalidPayloadError, isDirectusError, type DirectusError } from '@directus/errors';
4
import { isSystemCollection } from '@directus/system-data';
5
import type { Accountability, Aggregate, Filter, Item, PrimaryKey, Query, SchemaOverview } from '@directus/types';
6
import { parseFilterFunctionPath, toBoolean } from '@directus/utils';
7
import argon2 from 'argon2';
12
FormattedExecutionResult,
13
FragmentDefinitionNode,
34
NoSchemaIntrospectionCustomRule,
40
InputTypeComposerFieldConfigMapDefinition,
41
ObjectTypeComposerFieldConfigAsObjectDefinition,
42
ObjectTypeComposerFieldConfigDefinition,
43
ObjectTypeComposerFieldConfigMapDefinition,
45
} from 'graphql-compose';
46
import { GraphQLJSON, InputTypeComposer, ObjectTypeComposer, SchemaComposer, toInputObjectType } from 'graphql-compose';
47
import type { Knex } from 'knex';
48
import { flatten, get, mapKeys, merge, omit, pick, set, transform, uniq } from 'lodash-es';
49
import { clearSystemCache, getCache } from '../../cache.js';
51
DEFAULT_AUTH_PROVIDER,
53
REFRESH_COOKIE_OPTIONS,
54
SESSION_COOKIE_OPTIONS,
55
} from '../../constants.js';
56
import getDatabase from '../../database/index.js';
57
import { rateLimiter } from '../../middleware/rate-limiter-registration.js';
58
import type { AbstractServiceOptions, AuthenticationMode, GraphQLParams } from '../../types/index.js';
59
import { generateHash } from '../../utils/generate-hash.js';
60
import { getGraphQLType } from '../../utils/get-graphql-type.js';
61
import { getIPFromReq } from '../../utils/get-ip-from-req.js';
62
import { getSecret } from '../../utils/get-secret.js';
63
import { getService } from '../../utils/get-service.js';
64
import isDirectusJWT from '../../utils/is-directus-jwt.js';
65
import { verifyAccessJWT } from '../../utils/jwt.js';
66
import { mergeVersionsRaw, mergeVersionsRecursive } from '../../utils/merge-version-data.js';
67
import { reduceSchema } from '../../utils/reduce-schema.js';
68
import { sanitizeQuery } from '../../utils/sanitize-query.js';
69
import { validateQuery } from '../../utils/validate-query.js';
70
import { ActivityService } from '../activity.js';
71
import { AuthenticationService } from '../authentication.js';
72
import { CollectionsService } from '../collections.js';
73
import { ExtensionsService } from '../extensions.js';
74
import { FieldsService } from '../fields.js';
75
import { FilesService } from '../files.js';
76
import { RelationsService } from '../relations.js';
77
import { RevisionsService } from '../revisions.js';
78
import { ServerService } from '../server.js';
79
import { SpecificationService } from '../specifications.js';
80
import { TFAService } from '../tfa.js';
81
import { UsersService } from '../users.js';
82
import { UtilsService } from '../utils.js';
83
import { VersionsService } from '../versions.js';
84
import { GraphQLExecutionError, GraphQLValidationError } from './errors/index.js';
85
import { cache } from './schema-cache.js';
86
import { createSubscriptionGenerator } from './subscription.js';
87
import { GraphQLBigInt } from './types/bigint.js';
88
import { GraphQLDate } from './types/date.js';
89
import { GraphQLGeoJSON } from './types/geojson.js';
90
import { GraphQLHash } from './types/hash.js';
91
import { GraphQLStringOrFloat } from './types/string-or-float.js';
92
import { GraphQLVoid } from './types/void.js';
93
import { addPathToValidationError } from './utils/add-path-to-validation-error.js';
94
import processError from './utils/process-error.js';
95
import { sanitizeGraphqlSchema } from './utils/sanitize-gql-schema.js';
99
const validationRules = Array.from(specifiedRules);
101
if (env['GRAPHQL_INTROSPECTION'] === false) {
102
validationRules.push(NoSchemaIntrospectionCustomRule);
106
* These should be ignored in the context of GraphQL, and/or are replaced by a custom resolver (for non-standard structures)
108
const SYSTEM_DENY_LIST = [
109
'directus_collections',
111
'directus_relations',
112
'directus_migrations',
114
'directus_extensions',
117
const READ_ONLY = ['directus_activity', 'directus_revisions'];
119
export class GraphQLService {
120
accountability: Accountability | null;
122
schema: SchemaOverview;
123
scope: 'items' | 'system';
125
constructor(options: AbstractServiceOptions & { scope: 'items' | 'system' }) {
126
this.accountability = options?.accountability || null;
127
this.knex = options?.knex || getDatabase();
128
this.schema = options.schema;
129
this.scope = options.scope;
133
* Execute a GraphQL structure
140
}: GraphQLParams): Promise<FormattedExecutionResult> {
141
const schema = this.getSchema();
143
const validationErrors = validate(schema, document, validationRules).map((validationError) =>
144
addPathToValidationError(validationError),
147
if (validationErrors.length > 0) {
148
throw new GraphQLValidationError({ errors: validationErrors });
151
let result: ExecutionResult;
154
result = await execute({
158
variableValues: variables,
162
throw new GraphQLExecutionError({ errors: [err.message] });
165
const formattedResult: FormattedExecutionResult = {};
167
if (result['data']) formattedResult.data = result['data'];
169
if (result['errors']) {
170
formattedResult.errors = result['errors'].map((error) => processError(this.accountability, error));
173
if (result['extensions']) formattedResult.extensions = result['extensions'];
175
return formattedResult;
179
* Generate the GraphQL schema. Pulls from the schema information generated by the get-schema util.
181
getSchema(): GraphQLSchema;
182
getSchema(type: 'schema'): GraphQLSchema;
183
getSchema(type: 'sdl'): GraphQLSchema | string;
184
getSchema(type: 'schema' | 'sdl' = 'schema'): GraphQLSchema | string {
185
const key = `${this.scope}_${type}_${this.accountability?.role}_${this.accountability?.user}`;
187
const cachedSchema = cache.get(key);
189
if (cachedSchema) return cachedSchema;
191
// eslint-disable-next-line @typescript-eslint/no-this-alias
194
const schemaComposer = new SchemaComposer<GraphQLParams['contextValue']>();
196
const sanitizedSchema = sanitizeGraphqlSchema(this.schema);
200
this.accountability?.admin === true
202
: reduceSchema(sanitizedSchema, this.accountability?.permissions || null, ['read']),
204
this.accountability?.admin === true
206
: reduceSchema(sanitizedSchema, this.accountability?.permissions || null, ['create']),
208
this.accountability?.admin === true
210
: reduceSchema(sanitizedSchema, this.accountability?.permissions || null, ['update']),
212
this.accountability?.admin === true
214
: reduceSchema(sanitizedSchema, this.accountability?.permissions || null, ['delete']),
217
const subscriptionEventType = schemaComposer.createEnumTC({
220
create: { value: 'create' },
221
update: { value: 'update' },
222
delete: { value: 'delete' },
226
const { ReadCollectionTypes, VersionCollectionTypes } = getReadableTypes();
227
const { CreateCollectionTypes, UpdateCollectionTypes, DeleteCollectionTypes } = getWritableTypes();
229
const scopeFilter = (collection: SchemaOverview['collections'][string]) => {
230
if (this.scope === 'items' && isSystemCollection(collection.collection)) return false;
232
if (this.scope === 'system') {
233
if (isSystemCollection(collection.collection) === false) return false;
234
if (SYSTEM_DENY_LIST.includes(collection.collection)) return false;
240
if (this.scope === 'system') {
241
this.injectSystemResolvers(
244
CreateCollectionTypes,
246
UpdateCollectionTypes,
247
DeleteCollectionTypes,
253
const readableCollections = Object.values(schema.read.collections)
254
.filter((collection) => collection.collection in ReadCollectionTypes)
255
.filter(scopeFilter);
257
if (readableCollections.length > 0) {
258
schemaComposer.Query.addFields(
259
readableCollections.reduce(
260
(acc, collection) => {
261
const collectionName = this.scope === 'items' ? collection.collection : collection.collection.substring(9);
262
acc[collectionName] = ReadCollectionTypes[collection.collection]!.getResolver(collection.collection);
264
if (this.schema.collections[collection.collection]!.singleton === false) {
265
acc[`${collectionName}_by_id`] = ReadCollectionTypes[collection.collection]!.getResolver(
266
`${collection.collection}_by_id`,
269
acc[`${collectionName}_aggregated`] = ReadCollectionTypes[collection.collection]!.getResolver(
270
`${collection.collection}_aggregated`,
274
if (this.scope === 'items') {
275
acc[`${collectionName}_by_version`] = VersionCollectionTypes[collection.collection]!.getResolver(
276
`${collection.collection}_by_version`,
282
{} as ObjectTypeComposerFieldConfigAsObjectDefinition<any, any>,
286
schemaComposer.Query.addFields({
289
description: "There's no data to query.",
294
if (Object.keys(schema.create.collections).length > 0) {
295
schemaComposer.Mutation.addFields(
296
Object.values(schema.create.collections)
297
.filter((collection) => collection.collection in CreateCollectionTypes && collection.singleton === false)
299
.filter((collection) => READ_ONLY.includes(collection.collection) === false)
301
(acc, collection) => {
302
const collectionName =
303
this.scope === 'items' ? collection.collection : collection.collection.substring(9);
305
acc[`create_${collectionName}_items`] = CreateCollectionTypes[collection.collection]!.getResolver(
306
`create_${collection.collection}_items`,
309
acc[`create_${collectionName}_item`] = CreateCollectionTypes[collection.collection]!.getResolver(
310
`create_${collection.collection}_item`,
315
{} as ObjectTypeComposerFieldConfigAsObjectDefinition<any, any>,
320
if (Object.keys(schema.update.collections).length > 0) {
321
schemaComposer.Mutation.addFields(
322
Object.values(schema.update.collections)
323
.filter((collection) => collection.collection in UpdateCollectionTypes)
325
.filter((collection) => READ_ONLY.includes(collection.collection) === false)
327
(acc, collection) => {
328
const collectionName =
329
this.scope === 'items' ? collection.collection : collection.collection.substring(9);
331
if (collection.singleton) {
332
acc[`update_${collectionName}`] = UpdateCollectionTypes[collection.collection]!.getResolver(
333
`update_${collection.collection}`,
336
acc[`update_${collectionName}_items`] = UpdateCollectionTypes[collection.collection]!.getResolver(
337
`update_${collection.collection}_items`,
340
acc[`update_${collectionName}_batch`] = UpdateCollectionTypes[collection.collection]!.getResolver(
341
`update_${collection.collection}_batch`,
344
acc[`update_${collectionName}_item`] = UpdateCollectionTypes[collection.collection]!.getResolver(
345
`update_${collection.collection}_item`,
351
{} as ObjectTypeComposerFieldConfigAsObjectDefinition<any, any>,
356
if (Object.keys(schema.delete.collections).length > 0) {
357
schemaComposer.Mutation.addFields(
358
Object.values(schema.delete.collections)
359
.filter((collection) => collection.singleton === false)
361
.filter((collection) => READ_ONLY.includes(collection.collection) === false)
363
(acc, collection) => {
364
const collectionName =
365
this.scope === 'items' ? collection.collection : collection.collection.substring(9);
367
acc[`delete_${collectionName}_items`] = DeleteCollectionTypes['many']!.getResolver(
368
`delete_${collection.collection}_items`,
371
acc[`delete_${collectionName}_item`] = DeleteCollectionTypes['one']!.getResolver(
372
`delete_${collection.collection}_item`,
377
{} as ObjectTypeComposerFieldConfigAsObjectDefinition<any, any>,
382
if (type === 'sdl') {
383
const sdl = schemaComposer.toSDL();
388
const gqlSchema = schemaComposer.buildSchema();
389
cache.set(key, gqlSchema);
393
* Construct an object of types for every collection, using the permitted fields per action type
396
function getTypes(action: 'read' | 'create' | 'update' | 'delete') {
397
const CollectionTypes: Record<string, ObjectTypeComposer> = {};
398
const VersionTypes: Record<string, ObjectTypeComposer> = {};
400
const CountFunctions = schemaComposer.createObjectTC({
401
name: 'count_functions',
409
const DateFunctions = schemaComposer.createObjectTC({
410
name: 'date_functions',
430
const TimeFunctions = schemaComposer.createObjectTC({
431
name: 'time_functions',
445
const DateTimeFunctions = schemaComposer.createObjectTC({
446
name: 'datetime_functions',
448
...DateFunctions.getFields(),
449
...TimeFunctions.getFields(),
453
for (const collection of Object.values(schema[action].collections)) {
454
if (Object.keys(collection.fields).length === 0) continue;
455
if (SYSTEM_DENY_LIST.includes(collection.collection)) continue;
457
CollectionTypes[collection.collection] = schemaComposer.createObjectTC({
458
name: action === 'read' ? collection.collection : `${action}_${collection.collection}`,
459
fields: Object.values(collection.fields).reduce(
461
let type: GraphQLScalarType | GraphQLNonNull<GraphQLNullableType> = getGraphQLType(
466
// GraphQL doesn't differentiate between not-null and has-to-be-submitted. We
467
// can't non-null in update, as that would require every not-nullable field to be
468
// submitted on updates
470
field.nullable === false &&
471
!field.defaultValue &&
472
!GENERATE_SPECIAL.some((flag) => field.special.includes(flag)) &&
475
type = new GraphQLNonNull(type);
478
if (collection.primary === field.field) {
479
// permissions IDs need to be nullable https://github.com/directus/directus/issues/20509
480
if (collection.collection === 'directus_permissions') {
482
} else if (!field.defaultValue && !field.special.includes('uuid') && action === 'create') {
483
type = new GraphQLNonNull(GraphQLID);
484
} else if (['create', 'update'].includes(action)) {
487
type = new GraphQLNonNull(GraphQLID);
493
description: field.note,
494
resolve: (obj: Record<string, any>) => {
495
return obj[field.field];
497
} as ObjectTypeComposerFieldConfigDefinition<any, any>;
499
if (action === 'read') {
500
if (field.type === 'date') {
501
acc[`${field.field}_func`] = {
503
resolve: (obj: Record<string, any>) => {
504
const funcFields = Object.keys(DateFunctions.getFields()).map((key) => `${field.field}_${key}`);
505
return mapKeys(pick(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
510
if (field.type === 'time') {
511
acc[`${field.field}_func`] = {
513
resolve: (obj: Record<string, any>) => {
514
const funcFields = Object.keys(TimeFunctions.getFields()).map((key) => `${field.field}_${key}`);
515
return mapKeys(pick(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
520
if (field.type === 'dateTime' || field.type === 'timestamp') {
521
acc[`${field.field}_func`] = {
522
type: DateTimeFunctions,
523
resolve: (obj: Record<string, any>) => {
524
const funcFields = Object.keys(DateTimeFunctions.getFields()).map(
525
(key) => `${field.field}_${key}`,
528
return mapKeys(pick(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
533
if (field.type === 'json' || field.type === 'alias') {
534
acc[`${field.field}_func`] = {
535
type: CountFunctions,
536
resolve: (obj: Record<string, any>) => {
537
const funcFields = Object.keys(CountFunctions.getFields()).map((key) => `${field.field}_${key}`);
538
return mapKeys(pick(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
546
{} as ObjectTypeComposerFieldConfigAsObjectDefinition<any, any>,
550
if (self.scope === 'items') {
551
VersionTypes[collection.collection] = CollectionTypes[collection.collection]!.clone(
552
`version_${collection.collection}`,
557
for (const relation of schema[action].relations) {
558
if (relation.related_collection) {
559
if (SYSTEM_DENY_LIST.includes(relation.related_collection)) continue;
561
CollectionTypes[relation.collection]?.addFields({
563
type: CollectionTypes[relation.related_collection]!,
564
resolve: (obj: Record<string, any>, _, __, info) => {
565
return obj[info?.path?.key ?? relation.field];
570
VersionTypes[relation.collection]?.addFields({
573
resolve: (obj: Record<string, any>, _, __, info) => {
574
return obj[info?.path?.key ?? relation.field];
579
if (relation.meta?.one_field) {
580
CollectionTypes[relation.related_collection]?.addFields({
581
[relation.meta.one_field]: {
582
type: [CollectionTypes[relation.collection]!],
583
resolve: (obj: Record<string, any>, _, __, info) => {
584
return obj[info?.path?.key ?? relation.meta!.one_field];
589
if (self.scope === 'items') {
590
VersionTypes[relation.related_collection]?.addFields({
591
[relation.meta.one_field]: {
593
resolve: (obj: Record<string, any>, _, __, info) => {
594
return obj[info?.path?.key ?? relation.meta!.one_field];
600
} else if (relation.meta?.one_allowed_collections && action === 'read') {
601
// NOTE: There are no union input types in GraphQL, so this only applies to Read actions
602
CollectionTypes[relation.collection]?.addFields({
604
type: new GraphQLUnionType({
605
name: `${relation.collection}_${relation.field}_union`,
606
types: relation.meta.one_allowed_collections.map((collection) =>
607
CollectionTypes[collection]!.getType(),
609
resolveType(_value, context, info) {
610
let path: (string | number)[] = [];
611
let currentPath = info.path;
613
while (currentPath.prev) {
614
path.push(currentPath.key);
615
currentPath = currentPath.prev;
618
path = path.reverse().slice(0, -1);
620
let parent = context['data']!;
622
for (const pathPart of path) {
623
parent = parent[pathPart];
626
const collection = parent[relation.meta!.one_collection_field!]!;
627
return CollectionTypes[collection]!.getType().name;
630
resolve: (obj: Record<string, any>, _, __, info) => {
631
return obj[info?.path?.key ?? relation.field];
638
return { CollectionTypes, VersionTypes };
642
* Create readable types and attach resolvers for each. Also prepares full filter argument structures
644
function getReadableTypes() {
645
const { CollectionTypes: ReadCollectionTypes, VersionTypes: VersionCollectionTypes } = getTypes('read');
647
const ReadableCollectionFilterTypes: Record<string, InputTypeComposer> = {};
649
const AggregatedFunctions: Record<string, ObjectTypeComposer<any, any>> = {};
650
const AggregatedFields: Record<string, ObjectTypeComposer<any, any>> = {};
651
const AggregateMethods: Record<string, ObjectTypeComposerFieldConfigMapDefinition<any, any>> = {};
653
const StringFilterOperators = schemaComposer.createInputTC({
654
name: 'string_filter_operators',
696
type: new GraphQLList(GraphQLString),
699
type: new GraphQLList(GraphQLString),
702
type: GraphQLBoolean,
705
type: GraphQLBoolean,
708
type: GraphQLBoolean,
711
type: GraphQLBoolean,
716
const BooleanFilterOperators = schemaComposer.createInputTC({
717
name: 'boolean_filter_operators',
720
type: GraphQLBoolean,
723
type: GraphQLBoolean,
726
type: GraphQLBoolean,
729
type: GraphQLBoolean,
734
const DateFilterOperators = schemaComposer.createInputTC({
735
name: 'date_filter_operators',
756
type: GraphQLBoolean,
759
type: GraphQLBoolean,
762
type: new GraphQLList(GraphQLString),
765
type: new GraphQLList(GraphQLString),
768
type: new GraphQLList(GraphQLStringOrFloat),
771
type: new GraphQLList(GraphQLStringOrFloat),
776
// Uses StringOrFloat rather than Float to support api dynamic variables (like `$NOW`)
777
const NumberFilterOperators = schemaComposer.createInputTC({
778
name: 'number_filter_operators',
781
type: GraphQLStringOrFloat,
784
type: GraphQLStringOrFloat,
787
type: new GraphQLList(GraphQLStringOrFloat),
790
type: new GraphQLList(GraphQLStringOrFloat),
793
type: GraphQLStringOrFloat,
796
type: GraphQLStringOrFloat,
799
type: GraphQLStringOrFloat,
802
type: GraphQLStringOrFloat,
805
type: GraphQLBoolean,
808
type: GraphQLBoolean,
811
type: new GraphQLList(GraphQLStringOrFloat),
814
type: new GraphQLList(GraphQLStringOrFloat),
819
const BigIntFilterOperators = schemaComposer.createInputTC({
820
name: 'big_int_filter_operators',
829
type: new GraphQLList(GraphQLBigInt),
832
type: new GraphQLList(GraphQLBigInt),
847
type: GraphQLBoolean,
850
type: GraphQLBoolean,
853
type: new GraphQLList(GraphQLBigInt),
856
type: new GraphQLList(GraphQLBigInt),
861
const GeometryFilterOperators = schemaComposer.createInputTC({
862
name: 'geometry_filter_operators',
865
type: GraphQLGeoJSON,
868
type: GraphQLGeoJSON,
871
type: GraphQLGeoJSON,
874
type: GraphQLGeoJSON,
877
type: GraphQLGeoJSON,
880
type: GraphQLGeoJSON,
883
type: GraphQLBoolean,
886
type: GraphQLBoolean,
891
const HashFilterOperators = schemaComposer.createInputTC({
892
name: 'hash_filter_operators',
895
type: GraphQLBoolean,
898
type: GraphQLBoolean,
901
type: GraphQLBoolean,
904
type: GraphQLBoolean,
909
const CountFunctionFilterOperators = schemaComposer.createInputTC({
910
name: 'count_function_filter_operators',
913
type: NumberFilterOperators,
918
const DateFunctionFilterOperators = schemaComposer.createInputTC({
919
name: 'date_function_filter_operators',
922
type: NumberFilterOperators,
925
type: NumberFilterOperators,
928
type: NumberFilterOperators,
931
type: NumberFilterOperators,
934
type: NumberFilterOperators,
939
const TimeFunctionFilterOperators = schemaComposer.createInputTC({
940
name: 'time_function_filter_operators',
943
type: NumberFilterOperators,
946
type: NumberFilterOperators,
949
type: NumberFilterOperators,
954
const DateTimeFunctionFilterOperators = schemaComposer.createInputTC({
955
name: 'datetime_function_filter_operators',
957
...DateFunctionFilterOperators.getFields(),
958
...TimeFunctionFilterOperators.getFields(),
962
for (const collection of Object.values(schema.read.collections)) {
963
if (Object.keys(collection.fields).length === 0) continue;
964
if (SYSTEM_DENY_LIST.includes(collection.collection)) continue;
966
ReadableCollectionFilterTypes[collection.collection] = schemaComposer.createInputTC({
967
name: `${collection.collection}_filter`,
968
fields: Object.values(collection.fields).reduce((acc, field) => {
969
const graphqlType = getGraphQLType(field.type, field.special);
971
let filterOperatorType: InputTypeComposer;
973
switch (graphqlType) {
975
filterOperatorType = BooleanFilterOperators;
978
filterOperatorType = BigIntFilterOperators;
982
filterOperatorType = NumberFilterOperators;
985
filterOperatorType = DateFilterOperators;
988
filterOperatorType = GeometryFilterOperators;
991
filterOperatorType = HashFilterOperators;
994
filterOperatorType = StringFilterOperators;
997
acc[field.field] = filterOperatorType;
999
if (field.type === 'date') {
1000
acc[`${field.field}_func`] = {
1001
type: DateFunctionFilterOperators,
1005
if (field.type === 'time') {
1006
acc[`${field.field}_func`] = {
1007
type: TimeFunctionFilterOperators,
1011
if (field.type === 'dateTime' || field.type === 'timestamp') {
1012
acc[`${field.field}_func`] = {
1013
type: DateTimeFunctionFilterOperators,
1017
if (field.type === 'json' || field.type === 'alias') {
1018
acc[`${field.field}_func`] = {
1019
type: CountFunctionFilterOperators,
1024
}, {} as InputTypeComposerFieldConfigMapDefinition),
1027
ReadableCollectionFilterTypes[collection.collection]!.addFields({
1028
_and: [ReadableCollectionFilterTypes[collection.collection]!],
1029
_or: [ReadableCollectionFilterTypes[collection.collection]!],
1032
AggregatedFields[collection.collection] = schemaComposer.createObjectTC({
1033
name: `${collection.collection}_aggregated_fields`,
1034
fields: Object.values(collection.fields).reduce(
1036
const graphqlType = getGraphQLType(field.type, field.special);
1038
switch (graphqlType) {
1042
acc[field.field] = {
1044
description: field.note,
1054
{} as ObjectTypeComposerFieldConfigAsObjectDefinition<any, any>,
1058
const countType = schemaComposer.createObjectTC({
1059
name: `${collection.collection}_aggregated_count`,
1060
fields: Object.values(collection.fields).reduce(
1062
acc[field.field] = {
1064
description: field.note,
1069
{} as ObjectTypeComposerFieldConfigAsObjectDefinition<any, any>,
1073
AggregateMethods[collection.collection] = {
1087
name: 'countDistinct',
1092
const hasNumericAggregates = Object.values(collection.fields).some((field) => {
1093
const graphqlType = getGraphQLType(field.type, field.special);
1095
if (graphqlType === GraphQLInt || graphqlType === GraphQLFloat) {
1102
if (hasNumericAggregates) {
1103
Object.assign(AggregateMethods[collection.collection]!, {
1106
type: AggregatedFields[collection.collection],
1110
type: AggregatedFields[collection.collection],
1113
name: 'avgDistinct',
1114
type: AggregatedFields[collection.collection],
1117
name: 'sumDistinct',
1118
type: AggregatedFields[collection.collection],
1122
type: AggregatedFields[collection.collection],
1126
type: AggregatedFields[collection.collection],
1131
AggregatedFunctions[collection.collection] = schemaComposer.createObjectTC({
1132
name: `${collection.collection}_aggregated`,
1133
fields: AggregateMethods[collection.collection]!,
1136
const resolver: ResolverDefinition<any, any> = {
1137
name: collection.collection,
1138
type: collection.singleton
1139
? ReadCollectionTypes[collection.collection]!
1140
: new GraphQLNonNull(
1141
new GraphQLList(new GraphQLNonNull(ReadCollectionTypes[collection.collection]!.getType())),
1143
resolve: async ({ info, context }: { info: GraphQLResolveInfo; context: Record<string, any> }) => {
1144
const result = await self.resolveQuery(info);
1145
context['data'] = result;
1150
if (collection.singleton === false) {
1152
filter: ReadableCollectionFilterTypes[collection.collection]!,
1154
type: new GraphQLList(GraphQLString),
1166
type: GraphQLString,
1171
version: GraphQLString,
1175
ReadCollectionTypes[collection.collection]!.addResolver(resolver);
1177
ReadCollectionTypes[collection.collection]!.addResolver({
1178
name: `${collection.collection}_aggregated`,
1179
type: new GraphQLNonNull(
1180
new GraphQLList(new GraphQLNonNull(AggregatedFunctions[collection.collection]!.getType())),
1183
groupBy: new GraphQLList(GraphQLString),
1184
filter: ReadableCollectionFilterTypes[collection.collection]!,
1195
type: GraphQLString,
1198
type: new GraphQLList(GraphQLString),
1201
resolve: async ({ info, context }: { info: GraphQLResolveInfo; context: Record<string, any> }) => {
1202
const result = await self.resolveQuery(info);
1203
context['data'] = result;
1209
if (collection.singleton === false) {
1210
ReadCollectionTypes[collection.collection]!.addResolver({
1211
name: `${collection.collection}_by_id`,
1212
type: ReadCollectionTypes[collection.collection]!,
1214
id: new GraphQLNonNull(GraphQLID),
1215
version: GraphQLString,
1217
resolve: async ({ info, context }: { info: GraphQLResolveInfo; context: Record<string, any> }) => {
1218
const result = await self.resolveQuery(info);
1219
context['data'] = result;
1225
if (self.scope === 'items') {
1226
VersionCollectionTypes[collection.collection]!.addResolver({
1227
name: `${collection.collection}_by_version`,
1228
type: VersionCollectionTypes[collection.collection]!,
1229
args: collection.singleton
1230
? { version: new GraphQLNonNull(GraphQLString) }
1232
version: new GraphQLNonNull(GraphQLString),
1233
id: new GraphQLNonNull(GraphQLID),
1235
resolve: async ({ info, context }: { info: GraphQLResolveInfo; context: Record<string, any> }) => {
1236
const result = await self.resolveQuery(info);
1237
context['data'] = result;
1243
const eventName = `${collection.collection}_mutated`;
1245
if (collection.collection in ReadCollectionTypes) {
1246
const subscriptionType = schemaComposer.createObjectTC({
1249
key: new GraphQLNonNull(GraphQLID),
1250
event: subscriptionEventType,
1251
data: ReadCollectionTypes[collection.collection]!,
1255
schemaComposer.Subscription.addFields({
1257
type: subscriptionType,
1259
event: subscriptionEventType,
1261
subscribe: createSubscriptionGenerator(self, eventName),
1267
for (const relation of schema.read.relations) {
1268
if (relation.related_collection) {
1269
if (SYSTEM_DENY_LIST.includes(relation.related_collection)) continue;
1271
ReadableCollectionFilterTypes[relation.collection]?.addFields({
1272
[relation.field]: ReadableCollectionFilterTypes[relation.related_collection]!,
1275
ReadCollectionTypes[relation.collection]?.addFieldArgs(relation.field, {
1276
filter: ReadableCollectionFilterTypes[relation.related_collection]!,
1278
type: new GraphQLList(GraphQLString),
1290
type: GraphQLString,
1294
if (relation.meta?.one_field) {
1295
ReadableCollectionFilterTypes[relation.related_collection]?.addFields({
1296
[relation.meta.one_field]: ReadableCollectionFilterTypes[relation.collection]!,
1299
ReadCollectionTypes[relation.related_collection]?.addFieldArgs(relation.meta.one_field, {
1300
filter: ReadableCollectionFilterTypes[relation.collection]!,
1302
type: new GraphQLList(GraphQLString),
1314
type: GraphQLString,
1318
} else if (relation.meta?.one_allowed_collections) {
1319
ReadableCollectionFilterTypes[relation.collection]?.removeField('item');
1321
for (const collection of relation.meta.one_allowed_collections) {
1322
ReadableCollectionFilterTypes[relation.collection]?.addFields({
1323
[`item__${collection}`]: ReadableCollectionFilterTypes[collection]!,
1329
return { ReadCollectionTypes, VersionCollectionTypes, ReadableCollectionFilterTypes };
1332
function getWritableTypes() {
1333
const { CollectionTypes: CreateCollectionTypes } = getTypes('create');
1334
const { CollectionTypes: UpdateCollectionTypes } = getTypes('update');
1335
const DeleteCollectionTypes: Record<string, ObjectTypeComposer<any, any>> = {};
1337
for (const collection of Object.values(schema.create.collections)) {
1338
if (Object.keys(collection.fields).length === 0) continue;
1339
if (SYSTEM_DENY_LIST.includes(collection.collection)) continue;
1340
if (collection.collection in CreateCollectionTypes === false) continue;
1342
const collectionIsReadable = collection.collection in ReadCollectionTypes;
1344
const creatableFields = CreateCollectionTypes[collection.collection]?.getFields() || {};
1346
if (Object.keys(creatableFields).length > 0) {
1347
const resolverDefinition: ResolverDefinition<any, any> = {
1348
name: `create_${collection.collection}_items`,
1349
type: collectionIsReadable
1350
? new GraphQLNonNull(
1351
new GraphQLList(new GraphQLNonNull(ReadCollectionTypes[collection.collection]!.getType())),
1354
resolve: async ({ args, info }: { args: Record<string, any>; info: GraphQLResolveInfo }) =>
1355
await self.resolveMutation(args, info),
1358
if (collectionIsReadable) {
1359
resolverDefinition.args = ReadCollectionTypes[collection.collection]!.getResolver(
1360
collection.collection,
1364
CreateCollectionTypes[collection.collection]!.addResolver(resolverDefinition);
1366
CreateCollectionTypes[collection.collection]!.addResolver({
1367
name: `create_${collection.collection}_item`,
1368
type: collectionIsReadable ? ReadCollectionTypes[collection.collection]! : GraphQLBoolean,
1369
resolve: async ({ args, info }: { args: Record<string, any>; info: GraphQLResolveInfo }) =>
1370
await self.resolveMutation(args, info),
1373
CreateCollectionTypes[collection.collection]!.getResolver(`create_${collection.collection}_items`).addArgs({
1374
...CreateCollectionTypes[collection.collection]!.getResolver(
1375
`create_${collection.collection}_items`,
1378
toInputObjectType(CreateCollectionTypes[collection.collection]!).setTypeName(
1379
`create_${collection.collection}_input`,
1384
CreateCollectionTypes[collection.collection]!.getResolver(`create_${collection.collection}_item`).addArgs({
1385
...CreateCollectionTypes[collection.collection]!.getResolver(
1386
`create_${collection.collection}_item`,
1388
data: toInputObjectType(CreateCollectionTypes[collection.collection]!).setTypeName(
1389
`create_${collection.collection}_input`,
1395
for (const collection of Object.values(schema.update.collections)) {
1396
if (Object.keys(collection.fields).length === 0) continue;
1397
if (SYSTEM_DENY_LIST.includes(collection.collection)) continue;
1398
if (collection.collection in UpdateCollectionTypes === false) continue;
1400
const collectionIsReadable = collection.collection in ReadCollectionTypes;
1402
const updatableFields = UpdateCollectionTypes[collection.collection]?.getFields() || {};
1404
if (Object.keys(updatableFields).length > 0) {
1405
if (collection.singleton) {
1406
UpdateCollectionTypes[collection.collection]!.addResolver({
1407
name: `update_${collection.collection}`,
1408
type: collectionIsReadable ? ReadCollectionTypes[collection.collection]! : GraphQLBoolean,
1410
data: toInputObjectType(UpdateCollectionTypes[collection.collection]!).setTypeName(
1411
`update_${collection.collection}_input`,
1414
resolve: async ({ args, info }: { args: Record<string, any>; info: GraphQLResolveInfo }) =>
1415
await self.resolveMutation(args, info),
1418
UpdateCollectionTypes[collection.collection]!.addResolver({
1419
name: `update_${collection.collection}_batch`,
1420
type: collectionIsReadable
1421
? new GraphQLNonNull(
1422
new GraphQLList(new GraphQLNonNull(ReadCollectionTypes[collection.collection]!.getType())),
1426
...(collectionIsReadable
1427
? ReadCollectionTypes[collection.collection]!.getResolver(collection.collection).getArgs()
1430
toInputObjectType(UpdateCollectionTypes[collection.collection]!).setTypeName(
1431
`update_${collection.collection}_input`,
1435
resolve: async ({ args, info }: { args: Record<string, any>; info: GraphQLResolveInfo }) =>
1436
await self.resolveMutation(args, info),
1439
UpdateCollectionTypes[collection.collection]!.addResolver({
1440
name: `update_${collection.collection}_items`,
1441
type: collectionIsReadable
1442
? new GraphQLNonNull(
1443
new GraphQLList(new GraphQLNonNull(ReadCollectionTypes[collection.collection]!.getType())),
1447
...(collectionIsReadable
1448
? ReadCollectionTypes[collection.collection]!.getResolver(collection.collection).getArgs()
1450
ids: new GraphQLNonNull(new GraphQLList(GraphQLID)),
1451
data: toInputObjectType(UpdateCollectionTypes[collection.collection]!).setTypeName(
1452
`update_${collection.collection}_input`,
1455
resolve: async ({ args, info }: { args: Record<string, any>; info: GraphQLResolveInfo }) =>
1456
await self.resolveMutation(args, info),
1459
UpdateCollectionTypes[collection.collection]!.addResolver({
1460
name: `update_${collection.collection}_item`,
1461
type: collectionIsReadable ? ReadCollectionTypes[collection.collection]! : GraphQLBoolean,
1463
id: new GraphQLNonNull(GraphQLID),
1464
data: toInputObjectType(UpdateCollectionTypes[collection.collection]!).setTypeName(
1465
`update_${collection.collection}_input`,
1468
resolve: async ({ args, info }: { args: Record<string, any>; info: GraphQLResolveInfo }) =>
1469
await self.resolveMutation(args, info),
1475
DeleteCollectionTypes['many'] = schemaComposer.createObjectTC({
1476
name: `delete_many`,
1478
ids: new GraphQLNonNull(new GraphQLList(GraphQLID)),
1482
DeleteCollectionTypes['one'] = schemaComposer.createObjectTC({
1485
id: new GraphQLNonNull(GraphQLID),
1489
for (const collection of Object.values(schema.delete.collections)) {
1490
DeleteCollectionTypes['many']!.addResolver({
1491
name: `delete_${collection.collection}_items`,
1492
type: DeleteCollectionTypes['many'],
1494
ids: new GraphQLNonNull(new GraphQLList(GraphQLID)),
1496
resolve: async ({ args, info }: { args: Record<string, any>; info: GraphQLResolveInfo }) =>
1497
await self.resolveMutation(args, info),
1500
DeleteCollectionTypes['one'].addResolver({
1501
name: `delete_${collection.collection}_item`,
1502
type: DeleteCollectionTypes['one'],
1504
id: new GraphQLNonNull(GraphQLID),
1506
resolve: async ({ args, info }: { args: Record<string, any>; info: GraphQLResolveInfo }) =>
1507
await self.resolveMutation(args, info),
1511
return { CreateCollectionTypes, UpdateCollectionTypes, DeleteCollectionTypes };
1516
* Generic resolver that's used for every "regular" items/system query. Converts the incoming GraphQL AST / fragments into
1517
* Directus' query structure which is then executed by the services.
1519
async resolveQuery(info: GraphQLResolveInfo): Promise<Partial<Item> | null> {
1520
let collection = info.fieldName;
1521
if (this.scope === 'system') collection = `directus_${collection}`;
1522
const selections = this.replaceFragmentsInSelections(info.fieldNodes[0]?.selectionSet?.selections, info.fragments);
1524
if (!selections) return null;
1525
const args: Record<string, any> = this.parseArgs(info.fieldNodes[0]!.arguments || [], info.variableValues);
1528
let versionRaw = false;
1530
const isAggregate = collection.endsWith('_aggregated') && collection in this.schema.collections === false;
1533
query = this.getAggregateQuery(args, selections);
1534
collection = collection.slice(0, -11);
1536
query = this.getQuery(args, selections, info.variableValues);
1538
if (collection.endsWith('_by_id') && collection in this.schema.collections === false) {
1539
collection = collection.slice(0, -6);
1542
if (collection.endsWith('_by_version') && collection in this.schema.collections === false) {
1543
collection = collection.slice(0, -11);
1553
[this.schema.collections[collection]!.primary]: {
1563
// Transform count(a.b.c) into a.b.count(c)
1564
if (query.fields?.length) {
1565
for (let fieldIndex = 0; fieldIndex < query.fields.length; fieldIndex++) {
1566
query.fields[fieldIndex] = parseFilterFunctionPath(query.fields[fieldIndex]!);
1570
const result = await this.read(collection, query);
1572
if (args['version']) {
1573
const versionsService = new VersionsService({ accountability: this.accountability, schema: this.schema });
1575
const saves = await versionsService.getVersionSaves(args['version'], collection, args['id']);
1578
if (this.schema.collections[collection]!.singleton) {
1580
? mergeVersionsRaw(result, saves)
1581
: mergeVersionsRecursive(result, saves, collection, this.schema);
1583
if (result?.[0] === undefined) return null;
1586
? mergeVersionsRaw(result[0], saves)
1587
: mergeVersionsRecursive(result[0], saves, collection, this.schema);
1593
return result?.[0] || null;
1597
// for every entry in result add a group field based on query.group;
1598
const aggregateKeys = Object.keys(query.aggregate ?? {});
1600
result['map']((field: Item) => {
1601
field['group'] = omit(field, aggregateKeys);
1608
async resolveMutation(
1609
args: Record<string, any>,
1610
info: GraphQLResolveInfo,
1611
): Promise<Partial<Item> | boolean | undefined> {
1612
const action = info.fieldName.split('_')[0] as 'create' | 'update' | 'delete';
1613
let collection = info.fieldName.substring(action.length + 1);
1614
if (this.scope === 'system') collection = `directus_${collection}`;
1616
const selections = this.replaceFragmentsInSelections(info.fieldNodes[0]?.selectionSet?.selections, info.fragments);
1617
const query = this.getQuery(args, selections || [], info.variableValues);
1620
collection.endsWith('_batch') === false &&
1621
collection.endsWith('_items') === false &&
1622
collection.endsWith('_item') === false &&
1623
collection in this.schema.collections;
1625
const single = collection.endsWith('_items') === false && collection.endsWith('_batch') === false;
1626
const batchUpdate = action === 'update' && collection.endsWith('_batch');
1628
if (collection.endsWith('_batch')) collection = collection.slice(0, -6);
1629
if (collection.endsWith('_items')) collection = collection.slice(0, -6);
1630
if (collection.endsWith('_item')) collection = collection.slice(0, -5);
1632
if (singleton && action === 'update') {
1633
return await this.upsertSingleton(collection, args['data'], query);
1636
const service = getService(collection, {
1638
accountability: this.accountability,
1639
schema: this.schema,
1642
const hasQuery = (query.fields || []).length > 0;
1646
if (action === 'create') {
1647
const key = await service.createOne(args['data']);
1648
return hasQuery ? await service.readOne(key, query) : true;
1651
if (action === 'update') {
1652
const key = await service.updateOne(args['id'], args['data']);
1653
return hasQuery ? await service.readOne(key, query) : true;
1656
if (action === 'delete') {
1657
await service.deleteOne(args['id']);
1658
return { id: args['id'] };
1663
if (action === 'create') {
1664
const keys = await service.createMany(args['data']);
1665
return hasQuery ? await service.readMany(keys, query) : true;
1668
if (action === 'update') {
1669
const keys: PrimaryKey[] = [];
1672
keys.push(...(await service.updateBatch(args['data'])));
1674
keys.push(...(await service.updateMany(args['ids'], args['data'])));
1677
return hasQuery ? await service.readMany(keys, query) : true;
1680
if (action === 'delete') {
1681
const keys = await service.deleteMany(args['ids']);
1682
return { ids: keys };
1687
} catch (err: any) {
1688
return this.formatError(err);
1693
* Execute the read action on the correct service. Checks for singleton as well.
1695
async read(collection: string, query: Query): Promise<Partial<Item>> {
1696
const service = getService(collection, {
1698
accountability: this.accountability,
1699
schema: this.schema,
1702
const result = this.schema.collections[collection]!.singleton
1703
? await service.readSingleton(query, { stripNonRequested: false })
1704
: await service.readByQuery(query, { stripNonRequested: false });
1710
* Upsert and read singleton item
1712
async upsertSingleton(
1714
body: Record<string, any> | Record<string, any>[],
1716
): Promise<Partial<Item> | boolean> {
1717
const service = getService(collection, {
1719
accountability: this.accountability,
1720
schema: this.schema,
1724
await service.upsertSingleton(body);
1726
if ((query.fields || []).length > 0) {
1727
const result = await service.readSingleton(query);
1732
} catch (err: any) {
1733
throw this.formatError(err);
1738
* GraphQL's regular resolver `args` variable only contains the "top-level" arguments. Seeing that we convert the
1739
* whole nested tree into one big query using Directus' own query resolver, we want to have a nested structure of
1740
* arguments for the whole resolving tree, which can later be transformed into Directus' AST using `deep`.
1741
* In order to do that, we'll parse over all ArgumentNodes and ObjectFieldNodes to manually recreate an object structure
1744
parseArgs(args: readonly ArgumentNode[], variableValues: GraphQLResolveInfo['variableValues']): Record<string, any> {
1745
if (!args || args['length'] === 0) return {};
1747
const parse = (node: ValueNode): any => {
1748
switch (node.kind) {
1750
return variableValues[node.name.value];
1752
return node.values.map(parse);
1754
return Object.fromEntries(node.fields.map((node) => [node.name.value, parse(node.value)]));
1758
return String(node.value);
1761
return Number(node.value);
1762
case 'BooleanValue':
1763
return Boolean(node.value);
1766
return 'value' in node ? node.value : null;
1770
const argsObject = Object.fromEntries(args['map']((arg) => [arg.name.value, parse(arg.value)]));
1776
* Get a Directus Query object from the parsed arguments (rawQuery) and GraphQL AST selectionSet. Converts SelectionSet into
1777
* Directus' `fields` query for use in the resolver. Also applies variables where appropriate.
1781
selections: readonly SelectionNode[],
1782
variableValues: GraphQLResolveInfo['variableValues'],
1784
const query: Query = sanitizeQuery(rawQuery, this.accountability);
1786
const parseAliases = (selections: readonly SelectionNode[]) => {
1787
const aliases: Record<string, string> = {};
1789
for (const selection of selections) {
1790
if (selection.kind !== 'Field') continue;
1792
if (selection.alias?.value) {
1793
aliases[selection.alias.value] = selection.name.value;
1800
const parseFields = (selections: readonly SelectionNode[], parent?: string): string[] => {
1801
const fields: string[] = [];
1803
for (let selection of selections) {
1804
if ((selection.kind === 'Field' || selection.kind === 'InlineFragment') !== true) continue;
1806
selection = selection as FieldNode | InlineFragmentNode;
1808
let current: string;
1809
let currentAlias: string | null = null;
1811
// Union type (Many-to-Any)
1812
if (selection.kind === 'InlineFragment') {
1813
if (selection.typeCondition!.name.value.startsWith('__')) continue;
1815
current = `${parent}:${selection.typeCondition!.name.value}`;
1817
// Any other field type
1819
// filter out graphql pointers, like __typename
1820
if (selection.name.value.startsWith('__')) continue;
1822
current = selection.name.value;
1824
if (selection.alias) {
1825
currentAlias = selection.alias.value;
1829
current = `${parent}.${current}`;
1832
currentAlias = `${parent}.${currentAlias}`;
1834
// add nested aliases into deep query
1835
if (selection.selectionSet) {
1836
if (!query.deep) query.deep = {};
1841
merge({}, get(query.deep, parent), { _alias: { [selection.alias!.value]: selection.name.value } }),
1848
if (selection.selectionSet) {
1849
let children: string[];
1851
if (current.endsWith('_func')) {
1854
const rootField = current.slice(0, -5);
1856
for (const subSelection of selection.selectionSet.selections) {
1857
if (subSelection.kind !== 'Field') continue;
1858
if (subSelection.name!.value.startsWith('__')) continue;
1859
children.push(`${subSelection.name!.value}(${rootField})`);
1862
children = parseFields(selection.selectionSet.selections, currentAlias ?? current);
1865
fields.push(...children);
1867
fields.push(current);
1870
if (selection.kind === 'Field' && selection.arguments && selection.arguments.length > 0) {
1871
if (selection.arguments && selection.arguments.length > 0) {
1872
if (!query.deep) query.deep = {};
1874
const args: Record<string, any> = this.parseArgs(selection.arguments, variableValues);
1878
currentAlias ?? current,
1881
get(query.deep, currentAlias ?? current),
1882
mapKeys(sanitizeQuery(args, this.accountability), (_value, key) => `_${key}`),
1889
return uniq(fields);
1892
query.alias = parseAliases(selections);
1893
query.fields = parseFields(selections);
1894
if (query.filter) query.filter = this.replaceFuncs(query.filter);
1895
query.deep = this.replaceFuncs(query.deep as any) as any;
1897
validateQuery(query);
1903
* Resolve the aggregation query based on the requested aggregated fields
1905
getAggregateQuery(rawQuery: Query, selections: readonly SelectionNode[]): Query {
1906
const query: Query = sanitizeQuery(rawQuery, this.accountability);
1908
query.aggregate = {};
1910
for (let aggregationGroup of selections) {
1911
if ((aggregationGroup.kind === 'Field') !== true) continue;
1913
aggregationGroup = aggregationGroup as FieldNode;
1915
// filter out graphql pointers, like __typename
1916
if (aggregationGroup.name.value.startsWith('__')) continue;
1918
const aggregateProperty = aggregationGroup.name.value as keyof Aggregate;
1920
query.aggregate[aggregateProperty] =
1921
aggregationGroup.selectionSet?.selections
1922
// filter out graphql pointers, like __typename
1923
.filter((selectionNode) => !(selectionNode as FieldNode)?.name.value.startsWith('__'))
1924
.map((selectionNode) => {
1925
selectionNode = selectionNode as FieldNode;
1926
return selectionNode.name.value;
1931
query.filter = this.replaceFuncs(query.filter);
1934
validateQuery(query);
1940
* Replace functions from GraphQL format to Directus-Filter format
1942
replaceFuncs(filter: Filter): Filter {
1943
return replaceFuncDeep(filter);
1945
function replaceFuncDeep(filter: Record<string, any>) {
1946
return transform(filter, (result: Record<string, any>, value, key) => {
1947
const isFunctionKey =
1948
typeof key === 'string' && key.endsWith('_func') && FUNCTIONS.includes(Object.keys(value)[0]! as any);
1950
if (isFunctionKey) {
1951
const functionName = Object.keys(value)[0]!;
1952
const fieldName = key.slice(0, -5);
1954
result[`${functionName}(${fieldName})`] = Object.values(value)[0]!;
1956
result[key] = value?.constructor === Object || value?.constructor === Array ? replaceFuncDeep(value) : value;
1963
* Convert Directus-Exception into a GraphQL format, so it can be returned by GraphQL properly.
1965
formatError(error: DirectusError | DirectusError[]): GraphQLError {
1966
if (Array.isArray(error)) {
1967
set(error[0]!, 'extensions.code', error[0]!.code);
1968
return new GraphQLError(error[0]!.message, undefined, undefined, undefined, undefined, error[0]);
1971
set(error, 'extensions.code', error.code);
1972
return new GraphQLError(error.message, undefined, undefined, undefined, undefined, error);
1976
* Replace all fragments in a selectionset for the actual selection set as defined in the fragment
1977
* Effectively merges the selections with the fragments used in those selections
1979
replaceFragmentsInSelections(
1980
selections: readonly SelectionNode[] | undefined,
1981
fragments: Record<string, FragmentDefinitionNode>,
1982
): readonly SelectionNode[] | null {
1983
if (!selections) return null;
1985
const result = flatten(
1986
selections.map((selection) => {
1987
// Fragments can contains fragments themselves. This allows for nested fragments
1988
if (selection.kind === 'FragmentSpread') {
1989
return this.replaceFragmentsInSelections(fragments[selection.name.value]!.selectionSet.selections, fragments);
1992
// Nested relational fields can also contain fragments
1993
if ((selection.kind === 'Field' || selection.kind === 'InlineFragment') && selection.selectionSet) {
1994
selection.selectionSet.selections = this.replaceFragmentsInSelections(
1995
selection.selectionSet.selections,
1997
) as readonly SelectionNode[];
2002
).filter((s) => s) as SelectionNode[];
2007
injectSystemResolvers(
2008
schemaComposer: SchemaComposer<GraphQLParams['contextValue']>,
2010
CreateCollectionTypes,
2011
ReadCollectionTypes,
2012
UpdateCollectionTypes,
2013
DeleteCollectionTypes,
2015
CreateCollectionTypes: Record<string, ObjectTypeComposer<any, any>>;
2016
ReadCollectionTypes: Record<string, ObjectTypeComposer<any, any>>;
2017
UpdateCollectionTypes: Record<string, ObjectTypeComposer<any, any>>;
2018
DeleteCollectionTypes: Record<string, ObjectTypeComposer<any, any>>;
2021
create: SchemaOverview;
2022
read: SchemaOverview;
2023
update: SchemaOverview;
2024
delete: SchemaOverview;
2026
): SchemaComposer<any> {
2027
const AuthTokens = schemaComposer.createObjectTC({
2028
name: 'auth_tokens',
2030
access_token: GraphQLString,
2031
expires: GraphQLBigInt,
2032
refresh_token: GraphQLString,
2036
const AuthMode = new GraphQLEnumType({
2039
json: { value: 'json' },
2040
cookie: { value: 'cookie' },
2041
session: { value: 'session' },
2045
const ServerInfo = schemaComposer.createObjectTC({
2046
name: 'server_info',
2049
type: new GraphQLObjectType({
2050
name: 'server_info_project',
2052
project_name: { type: GraphQLString },
2053
project_descriptor: { type: GraphQLString },
2054
project_logo: { type: GraphQLString },
2055
project_color: { type: GraphQLString },
2056
default_language: { type: GraphQLString },
2057
public_foreground: { type: GraphQLString },
2058
public_background: { type: GraphQLString },
2059
public_note: { type: GraphQLString },
2060
custom_css: { type: GraphQLString },
2061
public_registration: { type: GraphQLBoolean },
2062
public_registration_verify_email: { type: GraphQLBoolean },
2069
if (this.accountability?.user) {
2070
ServerInfo.addFields({
2071
rateLimit: env['RATE_LIMITER_ENABLED']
2073
type: new GraphQLObjectType({
2074
name: 'server_info_rate_limit',
2076
points: { type: GraphQLInt },
2077
duration: { type: GraphQLInt },
2082
rateLimitGlobal: env['RATE_LIMITER_GLOBAL_ENABLED']
2084
type: new GraphQLObjectType({
2085
name: 'server_info_rate_limit_global',
2087
points: { type: GraphQLInt },
2088
duration: { type: GraphQLInt },
2093
websocket: toBoolean(env['WEBSOCKETS_ENABLED'])
2095
type: new GraphQLObjectType({
2096
name: 'server_info_websocket',
2099
type: toBoolean(env['WEBSOCKETS_REST_ENABLED'])
2100
? new GraphQLObjectType({
2101
name: 'server_info_websocket_rest',
2104
type: new GraphQLEnumType({
2105
name: 'server_info_websocket_rest_authentication',
2107
public: { value: 'public' },
2108
handshake: { value: 'handshake' },
2109
strict: { value: 'strict' },
2113
path: { type: GraphQLString },
2119
type: toBoolean(env['WEBSOCKETS_GRAPHQL_ENABLED'])
2120
? new GraphQLObjectType({
2121
name: 'server_info_websocket_graphql',
2124
type: new GraphQLEnumType({
2125
name: 'server_info_websocket_graphql_authentication',
2127
public: { value: 'public' },
2128
handshake: { value: 'handshake' },
2129
strict: { value: 'strict' },
2133
path: { type: GraphQLString },
2139
type: toBoolean(env['WEBSOCKETS_HEARTBEAT_ENABLED']) ? GraphQLInt : GraphQLBoolean,
2146
type: new GraphQLObjectType({
2147
name: 'server_info_query_limit',
2149
default: { type: GraphQLInt },
2150
max: { type: GraphQLInt },
2157
/** Globally available query */
2158
schemaComposer.Query.addFields({
2161
resolve: async () => {
2162
const service = new SpecificationService({ schema: this.schema, accountability: this.accountability });
2163
return await service.oas.generate();
2166
server_specs_graphql: {
2167
type: GraphQLString,
2169
scope: new GraphQLEnumType({
2170
name: 'graphql_sdl_scope',
2172
items: { value: 'items' },
2173
system: { value: 'system' },
2177
resolve: async (_, args) => {
2178
const service = new GraphQLService({
2179
schema: this.schema,
2180
accountability: this.accountability,
2181
scope: args['scope'] ?? 'items',
2184
return service.getSchema('sdl');
2188
type: GraphQLString,
2189
resolve: () => 'pong',
2193
resolve: async () => {
2194
const service = new ServerService({
2195
accountability: this.accountability,
2196
schema: this.schema,
2199
return await service.serverInfo();
2204
resolve: async () => {
2205
const service = new ServerService({
2206
accountability: this.accountability,
2207
schema: this.schema,
2210
return await service.health();
2215
const Collection = schemaComposer.createObjectTC({
2216
name: 'directus_collections',
2219
const Field = schemaComposer.createObjectTC({
2220
name: 'directus_fields',
2223
const Relation = schemaComposer.createObjectTC({
2224
name: 'directus_relations',
2227
const Extension = schemaComposer.createObjectTC({
2228
name: 'directus_extensions',
2232
* Globally available mutations
2234
schemaComposer.Mutation.addFields({
2238
email: new GraphQLNonNull(GraphQLString),
2239
password: new GraphQLNonNull(GraphQLString),
2243
resolve: async (_, args, { req, res }) => {
2244
const accountability: Accountability = { role: null };
2246
if (req?.ip) accountability.ip = req.ip;
2248
const userAgent = req?.get('user-agent');
2249
if (userAgent) accountability.userAgent = userAgent;
2251
const origin = req?.get('origin');
2252
if (origin) accountability.origin = origin;
2254
const authenticationService = new AuthenticationService({
2255
accountability: accountability,
2256
schema: this.schema,
2259
const mode: AuthenticationMode = args['mode'] ?? 'json';
2261
const { accessToken, refreshToken, expires } = await authenticationService.login(
2262
DEFAULT_AUTH_PROVIDER,
2265
session: mode === 'session',
2270
const payload = { expires } as { expires: number; access_token?: string; refresh_token?: string };
2272
if (mode === 'json') {
2273
payload.refresh_token = refreshToken;
2274
payload.access_token = accessToken;
2277
if (mode === 'cookie') {
2278
res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'] as string, refreshToken, REFRESH_COOKIE_OPTIONS);
2279
payload.access_token = accessToken;
2282
if (mode === 'session') {
2283
res?.cookie(env['SESSION_COOKIE_NAME'] as string, accessToken, SESSION_COOKIE_OPTIONS);
2292
refresh_token: GraphQLString,
2295
resolve: async (_, args, { req, res }) => {
2296
const accountability: Accountability = { role: null };
2298
if (req?.ip) accountability.ip = req.ip;
2300
const userAgent = req?.get('user-agent');
2301
if (userAgent) accountability.userAgent = userAgent;
2303
const origin = req?.get('origin');
2304
if (origin) accountability.origin = origin;
2306
const authenticationService = new AuthenticationService({
2307
accountability: accountability,
2308
schema: this.schema,
2311
const mode: AuthenticationMode = args['mode'] ?? 'json';
2312
let currentRefreshToken: string | undefined;
2314
if (mode === 'json') {
2315
currentRefreshToken = args['refresh_token'];
2316
} else if (mode === 'cookie') {
2317
currentRefreshToken = req?.cookies[env['REFRESH_TOKEN_COOKIE_NAME'] as string];
2318
} else if (mode === 'session') {
2319
const token = req?.cookies[env['SESSION_COOKIE_NAME'] as string];
2321
if (isDirectusJWT(token)) {
2322
const payload = verifyAccessJWT(token, getSecret());
2323
currentRefreshToken = payload.session;
2327
if (!currentRefreshToken) {
2328
throw new InvalidPayloadError({
2329
reason: `The refresh token is required in either the payload or cookie`,
2333
const { accessToken, refreshToken, expires } = await authenticationService.refresh(currentRefreshToken, {
2334
session: mode === 'session',
2337
const payload = { expires } as { expires: number; access_token?: string; refresh_token?: string };
2339
if (mode === 'json') {
2340
payload.refresh_token = refreshToken;
2341
payload.access_token = accessToken;
2344
if (mode === 'cookie') {
2345
res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'] as string, refreshToken, REFRESH_COOKIE_OPTIONS);
2346
payload.access_token = accessToken;
2349
if (mode === 'session') {
2350
res?.cookie(env['SESSION_COOKIE_NAME'] as string, accessToken, SESSION_COOKIE_OPTIONS);
2357
type: GraphQLBoolean,
2359
refresh_token: GraphQLString,
2362
resolve: async (_, args, { req, res }) => {
2363
const accountability: Accountability = { role: null };
2365
if (req?.ip) accountability.ip = req.ip;
2367
const userAgent = req?.get('user-agent');
2368
if (userAgent) accountability.userAgent = userAgent;
2370
const origin = req?.get('origin');
2371
if (origin) accountability.origin = origin;
2373
const authenticationService = new AuthenticationService({
2374
accountability: accountability,
2375
schema: this.schema,
2378
const mode: AuthenticationMode = args['mode'] ?? 'json';
2379
let currentRefreshToken: string | undefined;
2381
if (mode === 'json') {
2382
currentRefreshToken = args['refresh_token'];
2383
} else if (mode === 'cookie') {
2384
currentRefreshToken = req?.cookies[env['REFRESH_TOKEN_COOKIE_NAME'] as string];
2385
} else if (mode === 'session') {
2386
const token = req?.cookies[env['SESSION_COOKIE_NAME'] as string];
2388
if (isDirectusJWT(token)) {
2389
const payload = verifyAccessJWT(token, getSecret());
2390
currentRefreshToken = payload.session;
2394
if (!currentRefreshToken) {
2395
throw new InvalidPayloadError({
2396
reason: `The refresh token is required in either the payload or cookie`,
2400
await authenticationService.logout(currentRefreshToken);
2402
if (req?.cookies[env['REFRESH_TOKEN_COOKIE_NAME'] as string]) {
2403
res?.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'] as string, REFRESH_COOKIE_OPTIONS);
2406
if (req?.cookies[env['SESSION_COOKIE_NAME'] as string]) {
2407
res?.clearCookie(env['SESSION_COOKIE_NAME'] as string, SESSION_COOKIE_OPTIONS);
2413
auth_password_request: {
2414
type: GraphQLBoolean,
2416
email: new GraphQLNonNull(GraphQLString),
2417
reset_url: GraphQLString,
2419
resolve: async (_, args, { req }) => {
2420
const accountability: Accountability = { role: null };
2422
if (req?.ip) accountability.ip = req.ip;
2424
const userAgent = req?.get('user-agent');
2425
if (userAgent) accountability.userAgent = userAgent;
2427
const origin = req?.get('origin');
2428
if (origin) accountability.origin = origin;
2429
const service = new UsersService({ accountability, schema: this.schema });
2432
await service.requestPasswordReset(args['email'], args['reset_url'] || null);
2433
} catch (err: any) {
2434
if (isDirectusError(err, ErrorCode.InvalidPayload)) {
2442
auth_password_reset: {
2443
type: GraphQLBoolean,
2445
token: new GraphQLNonNull(GraphQLString),
2446
password: new GraphQLNonNull(GraphQLString),
2448
resolve: async (_, args, { req }) => {
2449
const accountability: Accountability = { role: null };
2451
if (req?.ip) accountability.ip = req.ip;
2453
const userAgent = req?.get('user-agent');
2454
if (userAgent) accountability.userAgent = userAgent;
2456
const origin = req?.get('origin');
2457
if (origin) accountability.origin = origin;
2459
const service = new UsersService({ accountability, schema: this.schema });
2460
await service.resetPassword(args['token'], args['password']);
2464
users_me_tfa_generate: {
2465
type: new GraphQLObjectType({
2466
name: 'users_me_tfa_generate_data',
2468
secret: { type: GraphQLString },
2469
otpauth_url: { type: GraphQLString },
2473
password: new GraphQLNonNull(GraphQLString),
2475
resolve: async (_, args) => {
2476
if (!this.accountability?.user) return null;
2478
const service = new TFAService({
2479
accountability: this.accountability,
2480
schema: this.schema,
2483
const authService = new AuthenticationService({
2484
accountability: this.accountability,
2485
schema: this.schema,
2488
await authService.verifyPassword(this.accountability.user, args['password']);
2489
const { url, secret } = await service.generateTFA(this.accountability.user);
2490
return { secret, otpauth_url: url };
2493
users_me_tfa_enable: {
2494
type: GraphQLBoolean,
2496
otp: new GraphQLNonNull(GraphQLString),
2497
secret: new GraphQLNonNull(GraphQLString),
2499
resolve: async (_, args) => {
2500
if (!this.accountability?.user) return null;
2502
const service = new TFAService({
2503
accountability: this.accountability,
2504
schema: this.schema,
2507
await service.enableTFA(this.accountability.user, args['otp'], args['secret']);
2511
users_me_tfa_disable: {
2512
type: GraphQLBoolean,
2514
otp: new GraphQLNonNull(GraphQLString),
2516
resolve: async (_, args) => {
2517
if (!this.accountability?.user) return null;
2519
const service = new TFAService({
2520
accountability: this.accountability,
2521
schema: this.schema,
2524
const otpValid = await service.verifyOTP(this.accountability.user, args['otp']);
2526
if (otpValid === false) {
2527
throw new InvalidPayloadError({ reason: `"otp" is invalid` });
2530
await service.disableTFA(this.accountability.user);
2534
utils_random_string: {
2535
type: GraphQLString,
2539
resolve: async (_, args) => {
2540
const { nanoid } = await import('nanoid');
2542
if (args['length'] !== undefined && (args['length'] < 1 || args['length'] > 500)) {
2543
throw new InvalidPayloadError({ reason: `"length" must be between 1 and 500` });
2546
return nanoid(args['length'] ? args['length'] : 32);
2549
utils_hash_generate: {
2550
type: GraphQLString,
2552
string: new GraphQLNonNull(GraphQLString),
2554
resolve: async (_, args) => {
2555
return await generateHash(args['string']);
2558
utils_hash_verify: {
2559
type: GraphQLBoolean,
2561
string: new GraphQLNonNull(GraphQLString),
2562
hash: new GraphQLNonNull(GraphQLString),
2564
resolve: async (_, args) => {
2565
return await argon2.verify(args['hash'], args['string']);
2569
type: GraphQLBoolean,
2571
collection: new GraphQLNonNull(GraphQLString),
2572
item: new GraphQLNonNull(GraphQLID),
2573
to: new GraphQLNonNull(GraphQLID),
2575
resolve: async (_, args) => {
2576
const service = new UtilsService({
2577
accountability: this.accountability,
2578
schema: this.schema,
2581
const { item, to } = args;
2582
await service.sort(args['collection'], { item, to });
2587
type: GraphQLBoolean,
2589
revision: new GraphQLNonNull(GraphQLID),
2591
resolve: async (_, args) => {
2592
const service = new RevisionsService({
2593
accountability: this.accountability,
2594
schema: this.schema,
2597
await service.revert(args['revision']);
2601
utils_cache_clear: {
2603
resolve: async () => {
2604
if (this.accountability?.admin !== true) {
2605
throw new ForbiddenError();
2608
const { cache } = getCache();
2610
await cache?.clear();
2611
await clearSystemCache();
2616
users_invite_accept: {
2617
type: GraphQLBoolean,
2619
token: new GraphQLNonNull(GraphQLString),
2620
password: new GraphQLNonNull(GraphQLString),
2622
resolve: async (_, args) => {
2623
const service = new UsersService({
2624
accountability: this.accountability,
2625
schema: this.schema,
2628
await service.acceptInvite(args['token'], args['password']);
2633
type: GraphQLBoolean,
2635
email: new GraphQLNonNull(GraphQLString),
2636
password: new GraphQLNonNull(GraphQLString),
2637
verification_url: GraphQLString,
2638
first_name: GraphQLString,
2639
last_name: GraphQLString,
2641
resolve: async (_, args, { req }) => {
2642
const service = new UsersService({ accountability: null, schema: this.schema });
2644
const ip = req ? getIPFromReq(req) : null;
2647
await rateLimiter.consume(ip);
2650
await service.registerUser({
2652
password: args.password,
2653
verification_url: args.verification_url,
2654
first_name: args.first_name,
2655
last_name: args.last_name,
2661
users_register_verify: {
2662
type: GraphQLBoolean,
2664
token: new GraphQLNonNull(GraphQLString),
2666
resolve: async (_, args) => {
2667
const service = new UsersService({ accountability: null, schema: this.schema });
2668
await service.verifyRegistration(args.token);
2674
if ('directus_collections' in schema.read.collections) {
2675
Collection.addFields({
2676
collection: GraphQLString,
2677
meta: schemaComposer.createObjectTC({
2678
name: 'directus_collections_meta',
2679
fields: Object.values(schema.read.collections['directus_collections']!.fields).reduce(
2681
acc[field.field] = {
2682
type: field.nullable
2683
? getGraphQLType(field.type, field.special)
2684
: new GraphQLNonNull(getGraphQLType(field.type, field.special)),
2685
description: field.note,
2686
} as ObjectTypeComposerFieldConfigDefinition<any, any, any>;
2690
{} as ObjectTypeComposerFieldConfigAsObjectDefinition<any, any>,
2693
schema: schemaComposer.createObjectTC({
2694
name: 'directus_collections_schema',
2696
name: GraphQLString,
2697
comment: GraphQLString,
2702
schemaComposer.Query.addFields({
2704
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Collection.getType()))),
2705
resolve: async () => {
2706
const collectionsService = new CollectionsService({
2707
accountability: this.accountability,
2708
schema: this.schema,
2711
return await collectionsService.readByQuery();
2715
collections_by_name: {
2718
name: new GraphQLNonNull(GraphQLString),
2720
resolve: async (_, args) => {
2721
const collectionsService = new CollectionsService({
2722
accountability: this.accountability,
2723
schema: this.schema,
2726
return await collectionsService.readOne(args['name']);
2732
if ('directus_fields' in schema.read.collections) {
2734
collection: GraphQLString,
2735
field: GraphQLString,
2736
type: GraphQLString,
2737
meta: schemaComposer.createObjectTC({
2738
name: 'directus_fields_meta',
2739
fields: Object.values(schema.read.collections['directus_fields']!.fields).reduce(
2741
acc[field.field] = {
2742
type: field.nullable
2743
? getGraphQLType(field.type, field.special)
2744
: new GraphQLNonNull(getGraphQLType(field.type, field.special)),
2745
description: field.note,
2746
} as ObjectTypeComposerFieldConfigDefinition<any, any, any>;
2750
{} as ObjectTypeComposerFieldConfigAsObjectDefinition<any, any>,
2753
schema: schemaComposer.createObjectTC({
2754
name: 'directus_fields_schema',
2756
name: GraphQLString,
2757
table: GraphQLString,
2758
data_type: GraphQLString,
2759
default_value: GraphQLString,
2760
max_length: GraphQLInt,
2761
numeric_precision: GraphQLInt,
2762
numeric_scale: GraphQLInt,
2763
is_nullable: GraphQLBoolean,
2764
is_unique: GraphQLBoolean,
2765
is_primary_key: GraphQLBoolean,
2766
has_auto_increment: GraphQLBoolean,
2767
foreign_key_column: GraphQLString,
2768
foreign_key_table: GraphQLString,
2769
comment: GraphQLString,
2774
schemaComposer.Query.addFields({
2776
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Field.getType()))),
2777
resolve: async () => {
2778
const service = new FieldsService({
2779
accountability: this.accountability,
2780
schema: this.schema,
2783
return await service.readAll();
2786
fields_in_collection: {
2787
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Field.getType()))),
2789
collection: new GraphQLNonNull(GraphQLString),
2791
resolve: async (_, args) => {
2792
const service = new FieldsService({
2793
accountability: this.accountability,
2794
schema: this.schema,
2797
return await service.readAll(args['collection']);
2803
collection: new GraphQLNonNull(GraphQLString),
2804
field: new GraphQLNonNull(GraphQLString),
2806
resolve: async (_, args) => {
2807
const service = new FieldsService({
2808
accountability: this.accountability,
2809
schema: this.schema,
2812
return await service.readOne(args['collection'], args['field']);
2818
if ('directus_relations' in schema.read.collections) {
2819
Relation.addFields({
2820
collection: GraphQLString,
2821
field: GraphQLString,
2822
related_collection: GraphQLString,
2823
schema: schemaComposer.createObjectTC({
2824
name: 'directus_relations_schema',
2826
table: new GraphQLNonNull(GraphQLString),
2827
column: new GraphQLNonNull(GraphQLString),
2828
foreign_key_table: new GraphQLNonNull(GraphQLString),
2829
foreign_key_column: new GraphQLNonNull(GraphQLString),
2830
constraint_name: GraphQLString,
2831
on_update: new GraphQLNonNull(GraphQLString),
2832
on_delete: new GraphQLNonNull(GraphQLString),
2835
meta: schemaComposer.createObjectTC({
2836
name: 'directus_relations_meta',
2837
fields: Object.values(schema.read.collections['directus_relations']!.fields).reduce(
2839
acc[field.field] = {
2840
type: getGraphQLType(field.type, field.special),
2841
description: field.note,
2842
} as ObjectTypeComposerFieldConfigDefinition<any, any, any>;
2846
{} as ObjectTypeComposerFieldConfigAsObjectDefinition<any, any>,
2851
schemaComposer.Query.addFields({
2853
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Relation.getType()))),
2854
resolve: async () => {
2855
const service = new RelationsService({
2856
accountability: this.accountability,
2857
schema: this.schema,
2860
return await service.readAll();
2863
relations_in_collection: {
2864
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Relation.getType()))),
2866
collection: new GraphQLNonNull(GraphQLString),
2868
resolve: async (_, args) => {
2869
const service = new RelationsService({
2870
accountability: this.accountability,
2871
schema: this.schema,
2874
return await service.readAll(args['collection']);
2877
relations_by_name: {
2880
collection: new GraphQLNonNull(GraphQLString),
2881
field: new GraphQLNonNull(GraphQLString),
2883
resolve: async (_, args) => {
2884
const service = new RelationsService({
2885
accountability: this.accountability,
2886
schema: this.schema,
2889
return await service.readOne(args['collection'], args['field']);
2895
if (this.accountability?.admin === true) {
2896
schemaComposer.Mutation.addFields({
2897
create_collections_item: {
2900
data: toInputObjectType(Collection.clone('create_directus_collections'), {
2904
toInputObjectType(Field.clone('create_directus_collections_fields'), { postfix: '_input' }).NonNull,
2908
resolve: async (_, args) => {
2909
const collectionsService = new CollectionsService({
2910
accountability: this.accountability,
2911
schema: this.schema,
2914
const collectionKey = await collectionsService.createOne(args['data']);
2915
return await collectionsService.readOne(collectionKey);
2918
update_collections_item: {
2921
collection: new GraphQLNonNull(GraphQLString),
2922
data: toInputObjectType(Collection.clone('update_directus_collections'), {
2924
}).removeField(['collection', 'schema']).NonNull,
2926
resolve: async (_, args) => {
2927
const collectionsService = new CollectionsService({
2928
accountability: this.accountability,
2929
schema: this.schema,
2932
const collectionKey = await collectionsService.updateOne(args['collection'], args['data']);
2933
return await collectionsService.readOne(collectionKey);
2936
delete_collections_item: {
2937
type: schemaComposer.createObjectTC({
2938
name: 'delete_collection',
2940
collection: GraphQLString,
2944
collection: new GraphQLNonNull(GraphQLString),
2946
resolve: async (_, args) => {
2947
const collectionsService = new CollectionsService({
2948
accountability: this.accountability,
2949
schema: this.schema,
2952
await collectionsService.deleteOne(args['collection']);
2953
return { collection: args['collection'] };
2958
schemaComposer.Mutation.addFields({
2959
create_fields_item: {
2962
collection: new GraphQLNonNull(GraphQLString),
2963
data: toInputObjectType(Field.clone('create_directus_fields'), { postfix: '_input' }).NonNull,
2965
resolve: async (_, args) => {
2966
const service = new FieldsService({
2967
accountability: this.accountability,
2968
schema: this.schema,
2971
await service.createField(args['collection'], args['data']);
2972
return await service.readOne(args['collection'], args['data'].field);
2975
update_fields_item: {
2978
collection: new GraphQLNonNull(GraphQLString),
2979
field: new GraphQLNonNull(GraphQLString),
2980
data: toInputObjectType(Field.clone('update_directus_fields'), { postfix: '_input' }).NonNull,
2982
resolve: async (_, args) => {
2983
const service = new FieldsService({
2984
accountability: this.accountability,
2985
schema: this.schema,
2988
await service.updateField(args['collection'], {
2990
field: args['field'],
2993
return await service.readOne(args['collection'], args['data'].field);
2996
delete_fields_item: {
2997
type: schemaComposer.createObjectTC({
2998
name: 'delete_field',
3000
collection: GraphQLString,
3001
field: GraphQLString,
3005
collection: new GraphQLNonNull(GraphQLString),
3006
field: new GraphQLNonNull(GraphQLString),
3008
resolve: async (_, args) => {
3009
const service = new FieldsService({
3010
accountability: this.accountability,
3011
schema: this.schema,
3014
await service.deleteField(args['collection'], args['field']);
3015
const { collection, field } = args;
3016
return { collection, field };
3021
schemaComposer.Mutation.addFields({
3022
create_relations_item: {
3025
data: toInputObjectType(Relation.clone('create_directus_relations'), { postfix: '_input' }).NonNull,
3027
resolve: async (_, args) => {
3028
const relationsService = new RelationsService({
3029
accountability: this.accountability,
3030
schema: this.schema,
3033
await relationsService.createOne(args['data']);
3034
return await relationsService.readOne(args['data'].collection, args['data'].field);
3037
update_relations_item: {
3040
collection: new GraphQLNonNull(GraphQLString),
3041
field: new GraphQLNonNull(GraphQLString),
3042
data: toInputObjectType(Relation.clone('update_directus_relations'), { postfix: '_input' }).NonNull,
3044
resolve: async (_, args) => {
3045
const relationsService = new RelationsService({
3046
accountability: this.accountability,
3047
schema: this.schema,
3050
await relationsService.updateOne(args['collection'], args['field'], args['data']);
3051
return await relationsService.readOne(args['data'].collection, args['data'].field);
3054
delete_relations_item: {
3055
type: schemaComposer.createObjectTC({
3056
name: 'delete_relation',
3058
collection: GraphQLString,
3059
field: GraphQLString,
3063
collection: new GraphQLNonNull(GraphQLString),
3064
field: new GraphQLNonNull(GraphQLString),
3066
resolve: async (_, args) => {
3067
const relationsService = new RelationsService({
3068
accountability: this.accountability,
3069
schema: this.schema,
3072
await relationsService.deleteOne(args['collection'], args['field']);
3073
return { collection: args['collection'], field: args['field'] };
3078
Extension.addFields({
3079
bundle: GraphQLString,
3080
name: new GraphQLNonNull(GraphQLString),
3081
schema: schemaComposer.createObjectTC({
3082
name: 'directus_extensions_schema',
3084
type: GraphQLString,
3085
local: GraphQLBoolean,
3088
meta: schemaComposer.createObjectTC({
3089
name: 'directus_extensions_meta',
3091
enabled: GraphQLBoolean,
3096
schemaComposer.Query.addFields({
3098
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Extension.getType()))),
3099
resolve: async () => {
3100
const service = new ExtensionsService({
3101
accountability: this.accountability,
3102
schema: this.schema,
3105
return await service.readAll();
3110
schemaComposer.Mutation.addFields({
3111
update_extensions_item: {
3115
data: toInputObjectType(
3116
schemaComposer.createObjectTC({
3117
name: 'update_directus_extensions_input',
3119
meta: schemaComposer.createObjectTC({
3120
name: 'update_directus_extensions_input_meta',
3122
enabled: GraphQLBoolean,
3129
resolve: async (_, args) => {
3130
const extensionsService = new ExtensionsService({
3131
accountability: this.accountability,
3132
schema: this.schema,
3135
await extensionsService.updateOne(args['id'], args['data']);
3136
return await extensionsService.readOne(args['id']);
3142
if ('directus_users' in schema.read.collections) {
3143
schemaComposer.Query.addFields({
3145
type: ReadCollectionTypes['directus_users']!,
3146
resolve: async (_, args, __, info) => {
3147
if (!this.accountability?.user) return null;
3148
const service = new UsersService({ schema: this.schema, accountability: this.accountability });
3150
const selections = this.replaceFragmentsInSelections(
3151
info.fieldNodes[0]?.selectionSet?.selections,
3155
const query = this.getQuery(args, selections || [], info.variableValues);
3157
return await service.readOne(this.accountability.user, query);
3163
if ('directus_users' in schema.update.collections && this.accountability?.user) {
3164
schemaComposer.Mutation.addFields({
3166
type: ReadCollectionTypes['directus_users']!,
3168
data: toInputObjectType(UpdateCollectionTypes['directus_users']!),
3170
resolve: async (_, args, __, info) => {
3171
if (!this.accountability?.user) return null;
3173
const service = new UsersService({
3174
schema: this.schema,
3175
accountability: this.accountability,
3178
await service.updateOne(this.accountability.user, args['data']);
3180
if ('directus_users' in ReadCollectionTypes) {
3181
const selections = this.replaceFragmentsInSelections(
3182
info.fieldNodes[0]?.selectionSet?.selections,
3186
const query = this.getQuery(args, selections || [], info.variableValues);
3188
return await service.readOne(this.accountability.user, query);
3197
if ('directus_activity' in schema.create.collections) {
3198
schemaComposer.Mutation.addFields({
3200
type: ReadCollectionTypes['directus_activity'] ?? GraphQLBoolean,
3202
collection: new GraphQLNonNull(GraphQLString),
3203
item: new GraphQLNonNull(GraphQLID),
3204
comment: new GraphQLNonNull(GraphQLString),
3206
resolve: async (_, args, __, info) => {
3207
const service = new ActivityService({
3208
accountability: this.accountability,
3209
schema: this.schema,
3212
const primaryKey = await service.createOne({
3214
action: Action.COMMENT,
3215
user: this.accountability?.user,
3216
ip: this.accountability?.ip,
3217
user_agent: this.accountability?.userAgent,
3218
origin: this.accountability?.origin,
3221
if ('directus_activity' in ReadCollectionTypes) {
3222
const selections = this.replaceFragmentsInSelections(
3223
info.fieldNodes[0]?.selectionSet?.selections,
3227
const query = this.getQuery(args, selections || [], info.variableValues);
3229
return await service.readOne(primaryKey, query);
3238
if ('directus_activity' in schema.update.collections) {
3239
schemaComposer.Mutation.addFields({
3241
type: ReadCollectionTypes['directus_activity'] ?? GraphQLBoolean,
3243
id: new GraphQLNonNull(GraphQLID),
3244
comment: new GraphQLNonNull(GraphQLString),
3246
resolve: async (_, args, __, info) => {
3247
const service = new ActivityService({
3248
accountability: this.accountability,
3249
schema: this.schema,
3252
const primaryKey = await service.updateOne(args['id'], { comment: args['comment'] });
3254
if ('directus_activity' in ReadCollectionTypes) {
3255
const selections = this.replaceFragmentsInSelections(
3256
info.fieldNodes[0]?.selectionSet?.selections,
3260
const query = this.getQuery(args, selections || [], info.variableValues);
3262
return await service.readOne(primaryKey, query);
3271
if ('directus_activity' in schema.delete.collections) {
3272
schemaComposer.Mutation.addFields({
3274
type: DeleteCollectionTypes['one']!,
3276
id: new GraphQLNonNull(GraphQLID),
3278
resolve: async (_, args) => {
3279
const service = new ActivityService({
3280
accountability: this.accountability,
3281
schema: this.schema,
3284
await service.deleteOne(args['id']);
3285
return { id: args['id'] };
3291
if ('directus_files' in schema.create.collections) {
3292
schemaComposer.Mutation.addFields({
3294
type: ReadCollectionTypes['directus_files'] ?? GraphQLBoolean,
3296
url: new GraphQLNonNull(GraphQLString),
3297
data: toInputObjectType(CreateCollectionTypes['directus_files']!).setTypeName(
3298
'create_directus_files_input',
3301
resolve: async (_, args, __, info) => {
3302
const service = new FilesService({
3303
accountability: this.accountability,
3304
schema: this.schema,
3307
const primaryKey = await service.importOne(args['url'], args['data']);
3309
if ('directus_files' in ReadCollectionTypes) {
3310
const selections = this.replaceFragmentsInSelections(
3311
info.fieldNodes[0]?.selectionSet?.selections,
3315
const query = this.getQuery(args, selections || [], info.variableValues);
3316
return await service.readOne(primaryKey, query);
3325
if ('directus_users' in schema.create.collections) {
3326
schemaComposer.Mutation.addFields({
3328
type: GraphQLBoolean,
3330
email: new GraphQLNonNull(GraphQLString),
3331
role: new GraphQLNonNull(GraphQLString),
3332
invite_url: GraphQLString,
3334
resolve: async (_, args) => {
3335
const service = new UsersService({
3336
accountability: this.accountability,
3337
schema: this.schema,
3340
await service.inviteUser(args['email'], args['role'], args['invite_url'] || null);
3347
return schemaComposer;