directus

Форк
0
3349 строк · 96.9 Кб
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';
8
import type {
9
	ArgumentNode,
10
	ExecutionResult,
11
	FieldNode,
12
	FormattedExecutionResult,
13
	FragmentDefinitionNode,
14
	GraphQLNullableType,
15
	GraphQLResolveInfo,
16
	InlineFragmentNode,
17
	SelectionNode,
18
	ValueNode,
19
} from 'graphql';
20
import {
21
	GraphQLBoolean,
22
	GraphQLEnumType,
23
	GraphQLError,
24
	GraphQLFloat,
25
	GraphQLID,
26
	GraphQLInt,
27
	GraphQLList,
28
	GraphQLNonNull,
29
	GraphQLObjectType,
30
	GraphQLScalarType,
31
	GraphQLSchema,
32
	GraphQLString,
33
	GraphQLUnionType,
34
	NoSchemaIntrospectionCustomRule,
35
	execute,
36
	specifiedRules,
37
	validate,
38
} from 'graphql';
39
import type {
40
	InputTypeComposerFieldConfigMapDefinition,
41
	ObjectTypeComposerFieldConfigAsObjectDefinition,
42
	ObjectTypeComposerFieldConfigDefinition,
43
	ObjectTypeComposerFieldConfigMapDefinition,
44
	ResolverDefinition,
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';
50
import {
51
	DEFAULT_AUTH_PROVIDER,
52
	GENERATE_SPECIAL,
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';
96

97
const env = useEnv();
98

99
const validationRules = Array.from(specifiedRules);
100

101
if (env['GRAPHQL_INTROSPECTION'] === false) {
102
	validationRules.push(NoSchemaIntrospectionCustomRule);
103
}
104

105
/**
106
 * These should be ignored in the context of GraphQL, and/or are replaced by a custom resolver (for non-standard structures)
107
 */
108
const SYSTEM_DENY_LIST = [
109
	'directus_collections',
110
	'directus_fields',
111
	'directus_relations',
112
	'directus_migrations',
113
	'directus_sessions',
114
	'directus_extensions',
115
];
116

117
const READ_ONLY = ['directus_activity', 'directus_revisions'];
118

119
export class GraphQLService {
120
	accountability: Accountability | null;
121
	knex: Knex;
122
	schema: SchemaOverview;
123
	scope: 'items' | 'system';
124

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;
130
	}
131

132
	/**
133
	 * Execute a GraphQL structure
134
	 */
135
	async execute({
136
		document,
137
		variables,
138
		operationName,
139
		contextValue,
140
	}: GraphQLParams): Promise<FormattedExecutionResult> {
141
		const schema = this.getSchema();
142

143
		const validationErrors = validate(schema, document, validationRules).map((validationError) =>
144
			addPathToValidationError(validationError),
145
		);
146

147
		if (validationErrors.length > 0) {
148
			throw new GraphQLValidationError({ errors: validationErrors });
149
		}
150

151
		let result: ExecutionResult;
152

153
		try {
154
			result = await execute({
155
				schema,
156
				document,
157
				contextValue,
158
				variableValues: variables,
159
				operationName,
160
			});
161
		} catch (err: any) {
162
			throw new GraphQLExecutionError({ errors: [err.message] });
163
		}
164

165
		const formattedResult: FormattedExecutionResult = {};
166

167
		if (result['data']) formattedResult.data = result['data'];
168

169
		if (result['errors']) {
170
			formattedResult.errors = result['errors'].map((error) => processError(this.accountability, error));
171
		}
172

173
		if (result['extensions']) formattedResult.extensions = result['extensions'];
174

175
		return formattedResult;
176
	}
177

178
	/**
179
	 * Generate the GraphQL schema. Pulls from the schema information generated by the get-schema util.
180
	 */
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}`;
186

187
		const cachedSchema = cache.get(key);
188

189
		if (cachedSchema) return cachedSchema;
190

191
		// eslint-disable-next-line @typescript-eslint/no-this-alias
192
		const self = this;
193

194
		const schemaComposer = new SchemaComposer<GraphQLParams['contextValue']>();
195

196
		const sanitizedSchema = sanitizeGraphqlSchema(this.schema);
197

198
		const schema = {
199
			read:
200
				this.accountability?.admin === true
201
					? sanitizedSchema
202
					: reduceSchema(sanitizedSchema, this.accountability?.permissions || null, ['read']),
203
			create:
204
				this.accountability?.admin === true
205
					? sanitizedSchema
206
					: reduceSchema(sanitizedSchema, this.accountability?.permissions || null, ['create']),
207
			update:
208
				this.accountability?.admin === true
209
					? sanitizedSchema
210
					: reduceSchema(sanitizedSchema, this.accountability?.permissions || null, ['update']),
211
			delete:
212
				this.accountability?.admin === true
213
					? sanitizedSchema
214
					: reduceSchema(sanitizedSchema, this.accountability?.permissions || null, ['delete']),
215
		};
216

217
		const subscriptionEventType = schemaComposer.createEnumTC({
218
			name: 'EventEnum',
219
			values: {
220
				create: { value: 'create' },
221
				update: { value: 'update' },
222
				delete: { value: 'delete' },
223
			},
224
		});
225

226
		const { ReadCollectionTypes, VersionCollectionTypes } = getReadableTypes();
227
		const { CreateCollectionTypes, UpdateCollectionTypes, DeleteCollectionTypes } = getWritableTypes();
228

229
		const scopeFilter = (collection: SchemaOverview['collections'][string]) => {
230
			if (this.scope === 'items' && isSystemCollection(collection.collection)) return false;
231

232
			if (this.scope === 'system') {
233
				if (isSystemCollection(collection.collection) === false) return false;
234
				if (SYSTEM_DENY_LIST.includes(collection.collection)) return false;
235
			}
236

237
			return true;
238
		};
239

240
		if (this.scope === 'system') {
241
			this.injectSystemResolvers(
242
				schemaComposer,
243
				{
244
					CreateCollectionTypes,
245
					ReadCollectionTypes,
246
					UpdateCollectionTypes,
247
					DeleteCollectionTypes,
248
				},
249
				schema,
250
			);
251
		}
252

253
		const readableCollections = Object.values(schema.read.collections)
254
			.filter((collection) => collection.collection in ReadCollectionTypes)
255
			.filter(scopeFilter);
256

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);
263

264
						if (this.schema.collections[collection.collection]!.singleton === false) {
265
							acc[`${collectionName}_by_id`] = ReadCollectionTypes[collection.collection]!.getResolver(
266
								`${collection.collection}_by_id`,
267
							);
268

269
							acc[`${collectionName}_aggregated`] = ReadCollectionTypes[collection.collection]!.getResolver(
270
								`${collection.collection}_aggregated`,
271
							);
272
						}
273

274
						if (this.scope === 'items') {
275
							acc[`${collectionName}_by_version`] = VersionCollectionTypes[collection.collection]!.getResolver(
276
								`${collection.collection}_by_version`,
277
							);
278
						}
279

280
						return acc;
281
					},
282
					{} as ObjectTypeComposerFieldConfigAsObjectDefinition<any, any>,
283
				),
284
			);
285
		} else {
286
			schemaComposer.Query.addFields({
287
				_empty: {
288
					type: GraphQLVoid,
289
					description: "There's no data to query.",
290
				},
291
			});
292
		}
293

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)
298
					.filter(scopeFilter)
299
					.filter((collection) => READ_ONLY.includes(collection.collection) === false)
300
					.reduce(
301
						(acc, collection) => {
302
							const collectionName =
303
								this.scope === 'items' ? collection.collection : collection.collection.substring(9);
304

305
							acc[`create_${collectionName}_items`] = CreateCollectionTypes[collection.collection]!.getResolver(
306
								`create_${collection.collection}_items`,
307
							);
308

309
							acc[`create_${collectionName}_item`] = CreateCollectionTypes[collection.collection]!.getResolver(
310
								`create_${collection.collection}_item`,
311
							);
312

313
							return acc;
314
						},
315
						{} as ObjectTypeComposerFieldConfigAsObjectDefinition<any, any>,
316
					),
317
			);
318
		}
319

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)
324
					.filter(scopeFilter)
325
					.filter((collection) => READ_ONLY.includes(collection.collection) === false)
326
					.reduce(
327
						(acc, collection) => {
328
							const collectionName =
329
								this.scope === 'items' ? collection.collection : collection.collection.substring(9);
330

331
							if (collection.singleton) {
332
								acc[`update_${collectionName}`] = UpdateCollectionTypes[collection.collection]!.getResolver(
333
									`update_${collection.collection}`,
334
								);
335
							} else {
336
								acc[`update_${collectionName}_items`] = UpdateCollectionTypes[collection.collection]!.getResolver(
337
									`update_${collection.collection}_items`,
338
								);
339

340
								acc[`update_${collectionName}_batch`] = UpdateCollectionTypes[collection.collection]!.getResolver(
341
									`update_${collection.collection}_batch`,
342
								);
343

344
								acc[`update_${collectionName}_item`] = UpdateCollectionTypes[collection.collection]!.getResolver(
345
									`update_${collection.collection}_item`,
346
								);
347
							}
348

349
							return acc;
350
						},
351
						{} as ObjectTypeComposerFieldConfigAsObjectDefinition<any, any>,
352
					),
353
			);
354
		}
355

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)
360
					.filter(scopeFilter)
361
					.filter((collection) => READ_ONLY.includes(collection.collection) === false)
362
					.reduce(
363
						(acc, collection) => {
364
							const collectionName =
365
								this.scope === 'items' ? collection.collection : collection.collection.substring(9);
366

367
							acc[`delete_${collectionName}_items`] = DeleteCollectionTypes['many']!.getResolver(
368
								`delete_${collection.collection}_items`,
369
							);
370

371
							acc[`delete_${collectionName}_item`] = DeleteCollectionTypes['one']!.getResolver(
372
								`delete_${collection.collection}_item`,
373
							);
374

375
							return acc;
376
						},
377
						{} as ObjectTypeComposerFieldConfigAsObjectDefinition<any, any>,
378
					),
379
			);
380
		}
381

382
		if (type === 'sdl') {
383
			const sdl = schemaComposer.toSDL();
384
			cache.set(key, sdl);
385
			return sdl;
386
		}
387

388
		const gqlSchema = schemaComposer.buildSchema();
389
		cache.set(key, gqlSchema);
390
		return gqlSchema;
391

392
		/**
393
		 * Construct an object of types for every collection, using the permitted fields per action type
394
		 * as it's fields.
395
		 */
396
		function getTypes(action: 'read' | 'create' | 'update' | 'delete') {
397
			const CollectionTypes: Record<string, ObjectTypeComposer> = {};
398
			const VersionTypes: Record<string, ObjectTypeComposer> = {};
399

400
			const CountFunctions = schemaComposer.createObjectTC({
401
				name: 'count_functions',
402
				fields: {
403
					count: {
404
						type: GraphQLInt,
405
					},
406
				},
407
			});
408

409
			const DateFunctions = schemaComposer.createObjectTC({
410
				name: 'date_functions',
411
				fields: {
412
					year: {
413
						type: GraphQLInt,
414
					},
415
					month: {
416
						type: GraphQLInt,
417
					},
418
					week: {
419
						type: GraphQLInt,
420
					},
421
					day: {
422
						type: GraphQLInt,
423
					},
424
					weekday: {
425
						type: GraphQLInt,
426
					},
427
				},
428
			});
429

430
			const TimeFunctions = schemaComposer.createObjectTC({
431
				name: 'time_functions',
432
				fields: {
433
					hour: {
434
						type: GraphQLInt,
435
					},
436
					minute: {
437
						type: GraphQLInt,
438
					},
439
					second: {
440
						type: GraphQLInt,
441
					},
442
				},
443
			});
444

445
			const DateTimeFunctions = schemaComposer.createObjectTC({
446
				name: 'datetime_functions',
447
				fields: {
448
					...DateFunctions.getFields(),
449
					...TimeFunctions.getFields(),
450
				},
451
			});
452

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;
456

457
				CollectionTypes[collection.collection] = schemaComposer.createObjectTC({
458
					name: action === 'read' ? collection.collection : `${action}_${collection.collection}`,
459
					fields: Object.values(collection.fields).reduce(
460
						(acc, field) => {
461
							let type: GraphQLScalarType | GraphQLNonNull<GraphQLNullableType> = getGraphQLType(
462
								field.type,
463
								field.special,
464
							);
465

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
469
							if (
470
								field.nullable === false &&
471
								!field.defaultValue &&
472
								!GENERATE_SPECIAL.some((flag) => field.special.includes(flag)) &&
473
								action !== 'update'
474
							) {
475
								type = new GraphQLNonNull(type);
476
							}
477

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') {
481
									type = GraphQLID;
482
								} else if (!field.defaultValue && !field.special.includes('uuid') && action === 'create') {
483
									type = new GraphQLNonNull(GraphQLID);
484
								} else if (['create', 'update'].includes(action)) {
485
									type = GraphQLID;
486
								} else {
487
									type = new GraphQLNonNull(GraphQLID);
488
								}
489
							}
490

491
							acc[field.field] = {
492
								type,
493
								description: field.note,
494
								resolve: (obj: Record<string, any>) => {
495
									return obj[field.field];
496
								},
497
							} as ObjectTypeComposerFieldConfigDefinition<any, any>;
498

499
							if (action === 'read') {
500
								if (field.type === 'date') {
501
									acc[`${field.field}_func`] = {
502
										type: DateFunctions,
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));
506
										},
507
									};
508
								}
509

510
								if (field.type === 'time') {
511
									acc[`${field.field}_func`] = {
512
										type: TimeFunctions,
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));
516
										},
517
									};
518
								}
519

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}`,
526
											);
527

528
											return mapKeys(pick(obj, funcFields), (_value, key) => key.substring(field.field.length + 1));
529
										},
530
									};
531
								}
532

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));
539
										},
540
									};
541
								}
542
							}
543

544
							return acc;
545
						},
546
						{} as ObjectTypeComposerFieldConfigAsObjectDefinition<any, any>,
547
					),
548
				});
549

550
				if (self.scope === 'items') {
551
					VersionTypes[collection.collection] = CollectionTypes[collection.collection]!.clone(
552
						`version_${collection.collection}`,
553
					);
554
				}
555
			}
556

557
			for (const relation of schema[action].relations) {
558
				if (relation.related_collection) {
559
					if (SYSTEM_DENY_LIST.includes(relation.related_collection)) continue;
560

561
					CollectionTypes[relation.collection]?.addFields({
562
						[relation.field]: {
563
							type: CollectionTypes[relation.related_collection]!,
564
							resolve: (obj: Record<string, any>, _, __, info) => {
565
								return obj[info?.path?.key ?? relation.field];
566
							},
567
						},
568
					});
569

570
					VersionTypes[relation.collection]?.addFields({
571
						[relation.field]: {
572
							type: GraphQLJSON,
573
							resolve: (obj: Record<string, any>, _, __, info) => {
574
								return obj[info?.path?.key ?? relation.field];
575
							},
576
						},
577
					});
578

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];
585
								},
586
							},
587
						});
588

589
						if (self.scope === 'items') {
590
							VersionTypes[relation.related_collection]?.addFields({
591
								[relation.meta.one_field]: {
592
									type: GraphQLJSON,
593
									resolve: (obj: Record<string, any>, _, __, info) => {
594
										return obj[info?.path?.key ?? relation.meta!.one_field];
595
									},
596
								},
597
							});
598
						}
599
					}
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({
603
						[relation.field]: {
604
							type: new GraphQLUnionType({
605
								name: `${relation.collection}_${relation.field}_union`,
606
								types: relation.meta.one_allowed_collections.map((collection) =>
607
									CollectionTypes[collection]!.getType(),
608
								),
609
								resolveType(_value, context, info) {
610
									let path: (string | number)[] = [];
611
									let currentPath = info.path;
612

613
									while (currentPath.prev) {
614
										path.push(currentPath.key);
615
										currentPath = currentPath.prev;
616
									}
617

618
									path = path.reverse().slice(0, -1);
619

620
									let parent = context['data']!;
621

622
									for (const pathPart of path) {
623
										parent = parent[pathPart];
624
									}
625

626
									const collection = parent[relation.meta!.one_collection_field!]!;
627
									return CollectionTypes[collection]!.getType().name;
628
								},
629
							}),
630
							resolve: (obj: Record<string, any>, _, __, info) => {
631
								return obj[info?.path?.key ?? relation.field];
632
							},
633
						},
634
					});
635
				}
636
			}
637

638
			return { CollectionTypes, VersionTypes };
639
		}
640

641
		/**
642
		 * Create readable types and attach resolvers for each. Also prepares full filter argument structures
643
		 */
644
		function getReadableTypes() {
645
			const { CollectionTypes: ReadCollectionTypes, VersionTypes: VersionCollectionTypes } = getTypes('read');
646

647
			const ReadableCollectionFilterTypes: Record<string, InputTypeComposer> = {};
648

649
			const AggregatedFunctions: Record<string, ObjectTypeComposer<any, any>> = {};
650
			const AggregatedFields: Record<string, ObjectTypeComposer<any, any>> = {};
651
			const AggregateMethods: Record<string, ObjectTypeComposerFieldConfigMapDefinition<any, any>> = {};
652

653
			const StringFilterOperators = schemaComposer.createInputTC({
654
				name: 'string_filter_operators',
655
				fields: {
656
					_eq: {
657
						type: GraphQLString,
658
					},
659
					_neq: {
660
						type: GraphQLString,
661
					},
662
					_contains: {
663
						type: GraphQLString,
664
					},
665
					_icontains: {
666
						type: GraphQLString,
667
					},
668
					_ncontains: {
669
						type: GraphQLString,
670
					},
671
					_starts_with: {
672
						type: GraphQLString,
673
					},
674
					_nstarts_with: {
675
						type: GraphQLString,
676
					},
677
					_istarts_with: {
678
						type: GraphQLString,
679
					},
680
					_nistarts_with: {
681
						type: GraphQLString,
682
					},
683
					_ends_with: {
684
						type: GraphQLString,
685
					},
686
					_nends_with: {
687
						type: GraphQLString,
688
					},
689
					_iends_with: {
690
						type: GraphQLString,
691
					},
692
					_niends_with: {
693
						type: GraphQLString,
694
					},
695
					_in: {
696
						type: new GraphQLList(GraphQLString),
697
					},
698
					_nin: {
699
						type: new GraphQLList(GraphQLString),
700
					},
701
					_null: {
702
						type: GraphQLBoolean,
703
					},
704
					_nnull: {
705
						type: GraphQLBoolean,
706
					},
707
					_empty: {
708
						type: GraphQLBoolean,
709
					},
710
					_nempty: {
711
						type: GraphQLBoolean,
712
					},
713
				},
714
			});
715

716
			const BooleanFilterOperators = schemaComposer.createInputTC({
717
				name: 'boolean_filter_operators',
718
				fields: {
719
					_eq: {
720
						type: GraphQLBoolean,
721
					},
722
					_neq: {
723
						type: GraphQLBoolean,
724
					},
725
					_null: {
726
						type: GraphQLBoolean,
727
					},
728
					_nnull: {
729
						type: GraphQLBoolean,
730
					},
731
				},
732
			});
733

734
			const DateFilterOperators = schemaComposer.createInputTC({
735
				name: 'date_filter_operators',
736
				fields: {
737
					_eq: {
738
						type: GraphQLString,
739
					},
740
					_neq: {
741
						type: GraphQLString,
742
					},
743
					_gt: {
744
						type: GraphQLString,
745
					},
746
					_gte: {
747
						type: GraphQLString,
748
					},
749
					_lt: {
750
						type: GraphQLString,
751
					},
752
					_lte: {
753
						type: GraphQLString,
754
					},
755
					_null: {
756
						type: GraphQLBoolean,
757
					},
758
					_nnull: {
759
						type: GraphQLBoolean,
760
					},
761
					_in: {
762
						type: new GraphQLList(GraphQLString),
763
					},
764
					_nin: {
765
						type: new GraphQLList(GraphQLString),
766
					},
767
					_between: {
768
						type: new GraphQLList(GraphQLStringOrFloat),
769
					},
770
					_nbetween: {
771
						type: new GraphQLList(GraphQLStringOrFloat),
772
					},
773
				},
774
			});
775

776
			// Uses StringOrFloat rather than Float to support api dynamic variables (like `$NOW`)
777
			const NumberFilterOperators = schemaComposer.createInputTC({
778
				name: 'number_filter_operators',
779
				fields: {
780
					_eq: {
781
						type: GraphQLStringOrFloat,
782
					},
783
					_neq: {
784
						type: GraphQLStringOrFloat,
785
					},
786
					_in: {
787
						type: new GraphQLList(GraphQLStringOrFloat),
788
					},
789
					_nin: {
790
						type: new GraphQLList(GraphQLStringOrFloat),
791
					},
792
					_gt: {
793
						type: GraphQLStringOrFloat,
794
					},
795
					_gte: {
796
						type: GraphQLStringOrFloat,
797
					},
798
					_lt: {
799
						type: GraphQLStringOrFloat,
800
					},
801
					_lte: {
802
						type: GraphQLStringOrFloat,
803
					},
804
					_null: {
805
						type: GraphQLBoolean,
806
					},
807
					_nnull: {
808
						type: GraphQLBoolean,
809
					},
810
					_between: {
811
						type: new GraphQLList(GraphQLStringOrFloat),
812
					},
813
					_nbetween: {
814
						type: new GraphQLList(GraphQLStringOrFloat),
815
					},
816
				},
817
			});
818

819
			const BigIntFilterOperators = schemaComposer.createInputTC({
820
				name: 'big_int_filter_operators',
821
				fields: {
822
					_eq: {
823
						type: GraphQLBigInt,
824
					},
825
					_neq: {
826
						type: GraphQLBigInt,
827
					},
828
					_in: {
829
						type: new GraphQLList(GraphQLBigInt),
830
					},
831
					_nin: {
832
						type: new GraphQLList(GraphQLBigInt),
833
					},
834
					_gt: {
835
						type: GraphQLBigInt,
836
					},
837
					_gte: {
838
						type: GraphQLBigInt,
839
					},
840
					_lt: {
841
						type: GraphQLBigInt,
842
					},
843
					_lte: {
844
						type: GraphQLBigInt,
845
					},
846
					_null: {
847
						type: GraphQLBoolean,
848
					},
849
					_nnull: {
850
						type: GraphQLBoolean,
851
					},
852
					_between: {
853
						type: new GraphQLList(GraphQLBigInt),
854
					},
855
					_nbetween: {
856
						type: new GraphQLList(GraphQLBigInt),
857
					},
858
				},
859
			});
860

861
			const GeometryFilterOperators = schemaComposer.createInputTC({
862
				name: 'geometry_filter_operators',
863
				fields: {
864
					_eq: {
865
						type: GraphQLGeoJSON,
866
					},
867
					_neq: {
868
						type: GraphQLGeoJSON,
869
					},
870
					_intersects: {
871
						type: GraphQLGeoJSON,
872
					},
873
					_nintersects: {
874
						type: GraphQLGeoJSON,
875
					},
876
					_intersects_bbox: {
877
						type: GraphQLGeoJSON,
878
					},
879
					_nintersects_bbox: {
880
						type: GraphQLGeoJSON,
881
					},
882
					_null: {
883
						type: GraphQLBoolean,
884
					},
885
					_nnull: {
886
						type: GraphQLBoolean,
887
					},
888
				},
889
			});
890

891
			const HashFilterOperators = schemaComposer.createInputTC({
892
				name: 'hash_filter_operators',
893
				fields: {
894
					_null: {
895
						type: GraphQLBoolean,
896
					},
897
					_nnull: {
898
						type: GraphQLBoolean,
899
					},
900
					_empty: {
901
						type: GraphQLBoolean,
902
					},
903
					_nempty: {
904
						type: GraphQLBoolean,
905
					},
906
				},
907
			});
908

909
			const CountFunctionFilterOperators = schemaComposer.createInputTC({
910
				name: 'count_function_filter_operators',
911
				fields: {
912
					count: {
913
						type: NumberFilterOperators,
914
					},
915
				},
916
			});
917

918
			const DateFunctionFilterOperators = schemaComposer.createInputTC({
919
				name: 'date_function_filter_operators',
920
				fields: {
921
					year: {
922
						type: NumberFilterOperators,
923
					},
924
					month: {
925
						type: NumberFilterOperators,
926
					},
927
					week: {
928
						type: NumberFilterOperators,
929
					},
930
					day: {
931
						type: NumberFilterOperators,
932
					},
933
					weekday: {
934
						type: NumberFilterOperators,
935
					},
936
				},
937
			});
938

939
			const TimeFunctionFilterOperators = schemaComposer.createInputTC({
940
				name: 'time_function_filter_operators',
941
				fields: {
942
					hour: {
943
						type: NumberFilterOperators,
944
					},
945
					minute: {
946
						type: NumberFilterOperators,
947
					},
948
					second: {
949
						type: NumberFilterOperators,
950
					},
951
				},
952
			});
953

954
			const DateTimeFunctionFilterOperators = schemaComposer.createInputTC({
955
				name: 'datetime_function_filter_operators',
956
				fields: {
957
					...DateFunctionFilterOperators.getFields(),
958
					...TimeFunctionFilterOperators.getFields(),
959
				},
960
			});
961

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;
965

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);
970

971
						let filterOperatorType: InputTypeComposer;
972

973
						switch (graphqlType) {
974
							case GraphQLBoolean:
975
								filterOperatorType = BooleanFilterOperators;
976
								break;
977
							case GraphQLBigInt:
978
								filterOperatorType = BigIntFilterOperators;
979
								break;
980
							case GraphQLInt:
981
							case GraphQLFloat:
982
								filterOperatorType = NumberFilterOperators;
983
								break;
984
							case GraphQLDate:
985
								filterOperatorType = DateFilterOperators;
986
								break;
987
							case GraphQLGeoJSON:
988
								filterOperatorType = GeometryFilterOperators;
989
								break;
990
							case GraphQLHash:
991
								filterOperatorType = HashFilterOperators;
992
								break;
993
							default:
994
								filterOperatorType = StringFilterOperators;
995
						}
996

997
						acc[field.field] = filterOperatorType;
998

999
						if (field.type === 'date') {
1000
							acc[`${field.field}_func`] = {
1001
								type: DateFunctionFilterOperators,
1002
							};
1003
						}
1004

1005
						if (field.type === 'time') {
1006
							acc[`${field.field}_func`] = {
1007
								type: TimeFunctionFilterOperators,
1008
							};
1009
						}
1010

1011
						if (field.type === 'dateTime' || field.type === 'timestamp') {
1012
							acc[`${field.field}_func`] = {
1013
								type: DateTimeFunctionFilterOperators,
1014
							};
1015
						}
1016

1017
						if (field.type === 'json' || field.type === 'alias') {
1018
							acc[`${field.field}_func`] = {
1019
								type: CountFunctionFilterOperators,
1020
							};
1021
						}
1022

1023
						return acc;
1024
					}, {} as InputTypeComposerFieldConfigMapDefinition),
1025
				});
1026

1027
				ReadableCollectionFilterTypes[collection.collection]!.addFields({
1028
					_and: [ReadableCollectionFilterTypes[collection.collection]!],
1029
					_or: [ReadableCollectionFilterTypes[collection.collection]!],
1030
				});
1031

1032
				AggregatedFields[collection.collection] = schemaComposer.createObjectTC({
1033
					name: `${collection.collection}_aggregated_fields`,
1034
					fields: Object.values(collection.fields).reduce(
1035
						(acc, field) => {
1036
							const graphqlType = getGraphQLType(field.type, field.special);
1037

1038
							switch (graphqlType) {
1039
								case GraphQLBigInt:
1040
								case GraphQLInt:
1041
								case GraphQLFloat:
1042
									acc[field.field] = {
1043
										type: GraphQLFloat,
1044
										description: field.note,
1045
									};
1046

1047
									break;
1048
								default:
1049
									break;
1050
							}
1051

1052
							return acc;
1053
						},
1054
						{} as ObjectTypeComposerFieldConfigAsObjectDefinition<any, any>,
1055
					),
1056
				});
1057

1058
				const countType = schemaComposer.createObjectTC({
1059
					name: `${collection.collection}_aggregated_count`,
1060
					fields: Object.values(collection.fields).reduce(
1061
						(acc, field) => {
1062
							acc[field.field] = {
1063
								type: GraphQLInt,
1064
								description: field.note,
1065
							};
1066

1067
							return acc;
1068
						},
1069
						{} as ObjectTypeComposerFieldConfigAsObjectDefinition<any, any>,
1070
					),
1071
				});
1072

1073
				AggregateMethods[collection.collection] = {
1074
					group: {
1075
						name: 'group',
1076
						type: GraphQLJSON,
1077
					},
1078
					countAll: {
1079
						name: 'countAll',
1080
						type: GraphQLInt,
1081
					},
1082
					count: {
1083
						name: 'count',
1084
						type: countType,
1085
					},
1086
					countDistinct: {
1087
						name: 'countDistinct',
1088
						type: countType,
1089
					},
1090
				};
1091

1092
				const hasNumericAggregates = Object.values(collection.fields).some((field) => {
1093
					const graphqlType = getGraphQLType(field.type, field.special);
1094

1095
					if (graphqlType === GraphQLInt || graphqlType === GraphQLFloat) {
1096
						return true;
1097
					}
1098

1099
					return false;
1100
				});
1101

1102
				if (hasNumericAggregates) {
1103
					Object.assign(AggregateMethods[collection.collection]!, {
1104
						avg: {
1105
							name: 'avg',
1106
							type: AggregatedFields[collection.collection],
1107
						},
1108
						sum: {
1109
							name: 'sum',
1110
							type: AggregatedFields[collection.collection],
1111
						},
1112
						avgDistinct: {
1113
							name: 'avgDistinct',
1114
							type: AggregatedFields[collection.collection],
1115
						},
1116
						sumDistinct: {
1117
							name: 'sumDistinct',
1118
							type: AggregatedFields[collection.collection],
1119
						},
1120
						min: {
1121
							name: 'min',
1122
							type: AggregatedFields[collection.collection],
1123
						},
1124
						max: {
1125
							name: 'max',
1126
							type: AggregatedFields[collection.collection],
1127
						},
1128
					});
1129
				}
1130

1131
				AggregatedFunctions[collection.collection] = schemaComposer.createObjectTC({
1132
					name: `${collection.collection}_aggregated`,
1133
					fields: AggregateMethods[collection.collection]!,
1134
				});
1135

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())),
1142
						  ),
1143
					resolve: async ({ info, context }: { info: GraphQLResolveInfo; context: Record<string, any> }) => {
1144
						const result = await self.resolveQuery(info);
1145
						context['data'] = result;
1146
						return result;
1147
					},
1148
				};
1149

1150
				if (collection.singleton === false) {
1151
					resolver.args = {
1152
						filter: ReadableCollectionFilterTypes[collection.collection]!,
1153
						sort: {
1154
							type: new GraphQLList(GraphQLString),
1155
						},
1156
						limit: {
1157
							type: GraphQLInt,
1158
						},
1159
						offset: {
1160
							type: GraphQLInt,
1161
						},
1162
						page: {
1163
							type: GraphQLInt,
1164
						},
1165
						search: {
1166
							type: GraphQLString,
1167
						},
1168
					};
1169
				} else {
1170
					resolver.args = {
1171
						version: GraphQLString,
1172
					};
1173
				}
1174

1175
				ReadCollectionTypes[collection.collection]!.addResolver(resolver);
1176

1177
				ReadCollectionTypes[collection.collection]!.addResolver({
1178
					name: `${collection.collection}_aggregated`,
1179
					type: new GraphQLNonNull(
1180
						new GraphQLList(new GraphQLNonNull(AggregatedFunctions[collection.collection]!.getType())),
1181
					),
1182
					args: {
1183
						groupBy: new GraphQLList(GraphQLString),
1184
						filter: ReadableCollectionFilterTypes[collection.collection]!,
1185
						limit: {
1186
							type: GraphQLInt,
1187
						},
1188
						offset: {
1189
							type: GraphQLInt,
1190
						},
1191
						page: {
1192
							type: GraphQLInt,
1193
						},
1194
						search: {
1195
							type: GraphQLString,
1196
						},
1197
						sort: {
1198
							type: new GraphQLList(GraphQLString),
1199
						},
1200
					},
1201
					resolve: async ({ info, context }: { info: GraphQLResolveInfo; context: Record<string, any> }) => {
1202
						const result = await self.resolveQuery(info);
1203
						context['data'] = result;
1204

1205
						return result;
1206
					},
1207
				});
1208

1209
				if (collection.singleton === false) {
1210
					ReadCollectionTypes[collection.collection]!.addResolver({
1211
						name: `${collection.collection}_by_id`,
1212
						type: ReadCollectionTypes[collection.collection]!,
1213
						args: {
1214
							id: new GraphQLNonNull(GraphQLID),
1215
							version: GraphQLString,
1216
						},
1217
						resolve: async ({ info, context }: { info: GraphQLResolveInfo; context: Record<string, any> }) => {
1218
							const result = await self.resolveQuery(info);
1219
							context['data'] = result;
1220
							return result;
1221
						},
1222
					});
1223
				}
1224

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) }
1231
							: {
1232
									version: new GraphQLNonNull(GraphQLString),
1233
									id: new GraphQLNonNull(GraphQLID),
1234
							  },
1235
						resolve: async ({ info, context }: { info: GraphQLResolveInfo; context: Record<string, any> }) => {
1236
							const result = await self.resolveQuery(info);
1237
							context['data'] = result;
1238
							return result;
1239
						},
1240
					});
1241
				}
1242

1243
				const eventName = `${collection.collection}_mutated`;
1244

1245
				if (collection.collection in ReadCollectionTypes) {
1246
					const subscriptionType = schemaComposer.createObjectTC({
1247
						name: eventName,
1248
						fields: {
1249
							key: new GraphQLNonNull(GraphQLID),
1250
							event: subscriptionEventType,
1251
							data: ReadCollectionTypes[collection.collection]!,
1252
						},
1253
					});
1254

1255
					schemaComposer.Subscription.addFields({
1256
						[eventName]: {
1257
							type: subscriptionType,
1258
							args: {
1259
								event: subscriptionEventType,
1260
							},
1261
							subscribe: createSubscriptionGenerator(self, eventName),
1262
						},
1263
					});
1264
				}
1265
			}
1266

1267
			for (const relation of schema.read.relations) {
1268
				if (relation.related_collection) {
1269
					if (SYSTEM_DENY_LIST.includes(relation.related_collection)) continue;
1270

1271
					ReadableCollectionFilterTypes[relation.collection]?.addFields({
1272
						[relation.field]: ReadableCollectionFilterTypes[relation.related_collection]!,
1273
					});
1274

1275
					ReadCollectionTypes[relation.collection]?.addFieldArgs(relation.field, {
1276
						filter: ReadableCollectionFilterTypes[relation.related_collection]!,
1277
						sort: {
1278
							type: new GraphQLList(GraphQLString),
1279
						},
1280
						limit: {
1281
							type: GraphQLInt,
1282
						},
1283
						offset: {
1284
							type: GraphQLInt,
1285
						},
1286
						page: {
1287
							type: GraphQLInt,
1288
						},
1289
						search: {
1290
							type: GraphQLString,
1291
						},
1292
					});
1293

1294
					if (relation.meta?.one_field) {
1295
						ReadableCollectionFilterTypes[relation.related_collection]?.addFields({
1296
							[relation.meta.one_field]: ReadableCollectionFilterTypes[relation.collection]!,
1297
						});
1298

1299
						ReadCollectionTypes[relation.related_collection]?.addFieldArgs(relation.meta.one_field, {
1300
							filter: ReadableCollectionFilterTypes[relation.collection]!,
1301
							sort: {
1302
								type: new GraphQLList(GraphQLString),
1303
							},
1304
							limit: {
1305
								type: GraphQLInt,
1306
							},
1307
							offset: {
1308
								type: GraphQLInt,
1309
							},
1310
							page: {
1311
								type: GraphQLInt,
1312
							},
1313
							search: {
1314
								type: GraphQLString,
1315
							},
1316
						});
1317
					}
1318
				} else if (relation.meta?.one_allowed_collections) {
1319
					ReadableCollectionFilterTypes[relation.collection]?.removeField('item');
1320

1321
					for (const collection of relation.meta.one_allowed_collections) {
1322
						ReadableCollectionFilterTypes[relation.collection]?.addFields({
1323
							[`item__${collection}`]: ReadableCollectionFilterTypes[collection]!,
1324
						});
1325
					}
1326
				}
1327
			}
1328

1329
			return { ReadCollectionTypes, VersionCollectionTypes, ReadableCollectionFilterTypes };
1330
		}
1331

1332
		function getWritableTypes() {
1333
			const { CollectionTypes: CreateCollectionTypes } = getTypes('create');
1334
			const { CollectionTypes: UpdateCollectionTypes } = getTypes('update');
1335
			const DeleteCollectionTypes: Record<string, ObjectTypeComposer<any, any>> = {};
1336

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;
1341

1342
				const collectionIsReadable = collection.collection in ReadCollectionTypes;
1343

1344
				const creatableFields = CreateCollectionTypes[collection.collection]?.getFields() || {};
1345

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())),
1352
							  )
1353
							: GraphQLBoolean,
1354
						resolve: async ({ args, info }: { args: Record<string, any>; info: GraphQLResolveInfo }) =>
1355
							await self.resolveMutation(args, info),
1356
					};
1357

1358
					if (collectionIsReadable) {
1359
						resolverDefinition.args = ReadCollectionTypes[collection.collection]!.getResolver(
1360
							collection.collection,
1361
						).getArgs();
1362
					}
1363

1364
					CreateCollectionTypes[collection.collection]!.addResolver(resolverDefinition);
1365

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),
1371
					});
1372

1373
					CreateCollectionTypes[collection.collection]!.getResolver(`create_${collection.collection}_items`).addArgs({
1374
						...CreateCollectionTypes[collection.collection]!.getResolver(
1375
							`create_${collection.collection}_items`,
1376
						).getArgs(),
1377
						data: [
1378
							toInputObjectType(CreateCollectionTypes[collection.collection]!).setTypeName(
1379
								`create_${collection.collection}_input`,
1380
							).NonNull,
1381
						],
1382
					});
1383

1384
					CreateCollectionTypes[collection.collection]!.getResolver(`create_${collection.collection}_item`).addArgs({
1385
						...CreateCollectionTypes[collection.collection]!.getResolver(
1386
							`create_${collection.collection}_item`,
1387
						).getArgs(),
1388
						data: toInputObjectType(CreateCollectionTypes[collection.collection]!).setTypeName(
1389
							`create_${collection.collection}_input`,
1390
						).NonNull,
1391
					});
1392
				}
1393
			}
1394

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;
1399

1400
				const collectionIsReadable = collection.collection in ReadCollectionTypes;
1401

1402
				const updatableFields = UpdateCollectionTypes[collection.collection]?.getFields() || {};
1403

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,
1409
							args: {
1410
								data: toInputObjectType(UpdateCollectionTypes[collection.collection]!).setTypeName(
1411
									`update_${collection.collection}_input`,
1412
								).NonNull,
1413
							},
1414
							resolve: async ({ args, info }: { args: Record<string, any>; info: GraphQLResolveInfo }) =>
1415
								await self.resolveMutation(args, info),
1416
						});
1417
					} else {
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())),
1423
								  )
1424
								: GraphQLBoolean,
1425
							args: {
1426
								...(collectionIsReadable
1427
									? ReadCollectionTypes[collection.collection]!.getResolver(collection.collection).getArgs()
1428
									: {}),
1429
								data: [
1430
									toInputObjectType(UpdateCollectionTypes[collection.collection]!).setTypeName(
1431
										`update_${collection.collection}_input`,
1432
									).NonNull,
1433
								],
1434
							},
1435
							resolve: async ({ args, info }: { args: Record<string, any>; info: GraphQLResolveInfo }) =>
1436
								await self.resolveMutation(args, info),
1437
						});
1438

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())),
1444
								  )
1445
								: GraphQLBoolean,
1446
							args: {
1447
								...(collectionIsReadable
1448
									? ReadCollectionTypes[collection.collection]!.getResolver(collection.collection).getArgs()
1449
									: {}),
1450
								ids: new GraphQLNonNull(new GraphQLList(GraphQLID)),
1451
								data: toInputObjectType(UpdateCollectionTypes[collection.collection]!).setTypeName(
1452
									`update_${collection.collection}_input`,
1453
								).NonNull,
1454
							},
1455
							resolve: async ({ args, info }: { args: Record<string, any>; info: GraphQLResolveInfo }) =>
1456
								await self.resolveMutation(args, info),
1457
						});
1458

1459
						UpdateCollectionTypes[collection.collection]!.addResolver({
1460
							name: `update_${collection.collection}_item`,
1461
							type: collectionIsReadable ? ReadCollectionTypes[collection.collection]! : GraphQLBoolean,
1462
							args: {
1463
								id: new GraphQLNonNull(GraphQLID),
1464
								data: toInputObjectType(UpdateCollectionTypes[collection.collection]!).setTypeName(
1465
									`update_${collection.collection}_input`,
1466
								).NonNull,
1467
							},
1468
							resolve: async ({ args, info }: { args: Record<string, any>; info: GraphQLResolveInfo }) =>
1469
								await self.resolveMutation(args, info),
1470
						});
1471
					}
1472
				}
1473
			}
1474

1475
			DeleteCollectionTypes['many'] = schemaComposer.createObjectTC({
1476
				name: `delete_many`,
1477
				fields: {
1478
					ids: new GraphQLNonNull(new GraphQLList(GraphQLID)),
1479
				},
1480
			});
1481

1482
			DeleteCollectionTypes['one'] = schemaComposer.createObjectTC({
1483
				name: `delete_one`,
1484
				fields: {
1485
					id: new GraphQLNonNull(GraphQLID),
1486
				},
1487
			});
1488

1489
			for (const collection of Object.values(schema.delete.collections)) {
1490
				DeleteCollectionTypes['many']!.addResolver({
1491
					name: `delete_${collection.collection}_items`,
1492
					type: DeleteCollectionTypes['many'],
1493
					args: {
1494
						ids: new GraphQLNonNull(new GraphQLList(GraphQLID)),
1495
					},
1496
					resolve: async ({ args, info }: { args: Record<string, any>; info: GraphQLResolveInfo }) =>
1497
						await self.resolveMutation(args, info),
1498
				});
1499

1500
				DeleteCollectionTypes['one'].addResolver({
1501
					name: `delete_${collection.collection}_item`,
1502
					type: DeleteCollectionTypes['one'],
1503
					args: {
1504
						id: new GraphQLNonNull(GraphQLID),
1505
					},
1506
					resolve: async ({ args, info }: { args: Record<string, any>; info: GraphQLResolveInfo }) =>
1507
						await self.resolveMutation(args, info),
1508
				});
1509
			}
1510

1511
			return { CreateCollectionTypes, UpdateCollectionTypes, DeleteCollectionTypes };
1512
		}
1513
	}
1514

1515
	/**
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.
1518
	 */
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);
1523

1524
		if (!selections) return null;
1525
		const args: Record<string, any> = this.parseArgs(info.fieldNodes[0]!.arguments || [], info.variableValues);
1526

1527
		let query: Query;
1528
		let versionRaw = false;
1529

1530
		const isAggregate = collection.endsWith('_aggregated') && collection in this.schema.collections === false;
1531

1532
		if (isAggregate) {
1533
			query = this.getAggregateQuery(args, selections);
1534
			collection = collection.slice(0, -11);
1535
		} else {
1536
			query = this.getQuery(args, selections, info.variableValues);
1537

1538
			if (collection.endsWith('_by_id') && collection in this.schema.collections === false) {
1539
				collection = collection.slice(0, -6);
1540
			}
1541

1542
			if (collection.endsWith('_by_version') && collection in this.schema.collections === false) {
1543
				collection = collection.slice(0, -11);
1544
				versionRaw = true;
1545
			}
1546
		}
1547

1548
		if (args['id']) {
1549
			query.filter = {
1550
				_and: [
1551
					query.filter || {},
1552
					{
1553
						[this.schema.collections[collection]!.primary]: {
1554
							_eq: args['id'],
1555
						},
1556
					},
1557
				],
1558
			};
1559

1560
			query.limit = 1;
1561
		}
1562

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]!);
1567
			}
1568
		}
1569

1570
		const result = await this.read(collection, query);
1571

1572
		if (args['version']) {
1573
			const versionsService = new VersionsService({ accountability: this.accountability, schema: this.schema });
1574

1575
			const saves = await versionsService.getVersionSaves(args['version'], collection, args['id']);
1576

1577
			if (saves) {
1578
				if (this.schema.collections[collection]!.singleton) {
1579
					return versionRaw
1580
						? mergeVersionsRaw(result, saves)
1581
						: mergeVersionsRecursive(result, saves, collection, this.schema);
1582
				} else {
1583
					if (result?.[0] === undefined) return null;
1584

1585
					return versionRaw
1586
						? mergeVersionsRaw(result[0], saves)
1587
						: mergeVersionsRecursive(result[0], saves, collection, this.schema);
1588
				}
1589
			}
1590
		}
1591

1592
		if (args['id']) {
1593
			return result?.[0] || null;
1594
		}
1595

1596
		if (query.group) {
1597
			// for every entry in result add a group field based on query.group;
1598
			const aggregateKeys = Object.keys(query.aggregate ?? {});
1599

1600
			result['map']((field: Item) => {
1601
				field['group'] = omit(field, aggregateKeys);
1602
			});
1603
		}
1604

1605
		return result;
1606
	}
1607

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}`;
1615

1616
		const selections = this.replaceFragmentsInSelections(info.fieldNodes[0]?.selectionSet?.selections, info.fragments);
1617
		const query = this.getQuery(args, selections || [], info.variableValues);
1618

1619
		const singleton =
1620
			collection.endsWith('_batch') === false &&
1621
			collection.endsWith('_items') === false &&
1622
			collection.endsWith('_item') === false &&
1623
			collection in this.schema.collections;
1624

1625
		const single = collection.endsWith('_items') === false && collection.endsWith('_batch') === false;
1626
		const batchUpdate = action === 'update' && collection.endsWith('_batch');
1627

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);
1631

1632
		if (singleton && action === 'update') {
1633
			return await this.upsertSingleton(collection, args['data'], query);
1634
		}
1635

1636
		const service = getService(collection, {
1637
			knex: this.knex,
1638
			accountability: this.accountability,
1639
			schema: this.schema,
1640
		});
1641

1642
		const hasQuery = (query.fields || []).length > 0;
1643

1644
		try {
1645
			if (single) {
1646
				if (action === 'create') {
1647
					const key = await service.createOne(args['data']);
1648
					return hasQuery ? await service.readOne(key, query) : true;
1649
				}
1650

1651
				if (action === 'update') {
1652
					const key = await service.updateOne(args['id'], args['data']);
1653
					return hasQuery ? await service.readOne(key, query) : true;
1654
				}
1655

1656
				if (action === 'delete') {
1657
					await service.deleteOne(args['id']);
1658
					return { id: args['id'] };
1659
				}
1660

1661
				return undefined;
1662
			} else {
1663
				if (action === 'create') {
1664
					const keys = await service.createMany(args['data']);
1665
					return hasQuery ? await service.readMany(keys, query) : true;
1666
				}
1667

1668
				if (action === 'update') {
1669
					const keys: PrimaryKey[] = [];
1670

1671
					if (batchUpdate) {
1672
						keys.push(...(await service.updateBatch(args['data'])));
1673
					} else {
1674
						keys.push(...(await service.updateMany(args['ids'], args['data'])));
1675
					}
1676

1677
					return hasQuery ? await service.readMany(keys, query) : true;
1678
				}
1679

1680
				if (action === 'delete') {
1681
					const keys = await service.deleteMany(args['ids']);
1682
					return { ids: keys };
1683
				}
1684

1685
				return undefined;
1686
			}
1687
		} catch (err: any) {
1688
			return this.formatError(err);
1689
		}
1690
	}
1691

1692
	/**
1693
	 * Execute the read action on the correct service. Checks for singleton as well.
1694
	 */
1695
	async read(collection: string, query: Query): Promise<Partial<Item>> {
1696
		const service = getService(collection, {
1697
			knex: this.knex,
1698
			accountability: this.accountability,
1699
			schema: this.schema,
1700
		});
1701

1702
		const result = this.schema.collections[collection]!.singleton
1703
			? await service.readSingleton(query, { stripNonRequested: false })
1704
			: await service.readByQuery(query, { stripNonRequested: false });
1705

1706
		return result;
1707
	}
1708

1709
	/**
1710
	 * Upsert and read singleton item
1711
	 */
1712
	async upsertSingleton(
1713
		collection: string,
1714
		body: Record<string, any> | Record<string, any>[],
1715
		query: Query,
1716
	): Promise<Partial<Item> | boolean> {
1717
		const service = getService(collection, {
1718
			knex: this.knex,
1719
			accountability: this.accountability,
1720
			schema: this.schema,
1721
		});
1722

1723
		try {
1724
			await service.upsertSingleton(body);
1725

1726
			if ((query.fields || []).length > 0) {
1727
				const result = await service.readSingleton(query);
1728
				return result;
1729
			}
1730

1731
			return true;
1732
		} catch (err: any) {
1733
			throw this.formatError(err);
1734
		}
1735
	}
1736

1737
	/**
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
1742
	 * of arguments
1743
	 */
1744
	parseArgs(args: readonly ArgumentNode[], variableValues: GraphQLResolveInfo['variableValues']): Record<string, any> {
1745
		if (!args || args['length'] === 0) return {};
1746

1747
		const parse = (node: ValueNode): any => {
1748
			switch (node.kind) {
1749
				case 'Variable':
1750
					return variableValues[node.name.value];
1751
				case 'ListValue':
1752
					return node.values.map(parse);
1753
				case 'ObjectValue':
1754
					return Object.fromEntries(node.fields.map((node) => [node.name.value, parse(node.value)]));
1755
				case 'NullValue':
1756
					return null;
1757
				case 'StringValue':
1758
					return String(node.value);
1759
				case 'IntValue':
1760
				case 'FloatValue':
1761
					return Number(node.value);
1762
				case 'BooleanValue':
1763
					return Boolean(node.value);
1764
				case 'EnumValue':
1765
				default:
1766
					return 'value' in node ? node.value : null;
1767
			}
1768
		};
1769

1770
		const argsObject = Object.fromEntries(args['map']((arg) => [arg.name.value, parse(arg.value)]));
1771

1772
		return argsObject;
1773
	}
1774

1775
	/**
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.
1778
	 */
1779
	getQuery(
1780
		rawQuery: Query,
1781
		selections: readonly SelectionNode[],
1782
		variableValues: GraphQLResolveInfo['variableValues'],
1783
	): Query {
1784
		const query: Query = sanitizeQuery(rawQuery, this.accountability);
1785

1786
		const parseAliases = (selections: readonly SelectionNode[]) => {
1787
			const aliases: Record<string, string> = {};
1788

1789
			for (const selection of selections) {
1790
				if (selection.kind !== 'Field') continue;
1791

1792
				if (selection.alias?.value) {
1793
					aliases[selection.alias.value] = selection.name.value;
1794
				}
1795
			}
1796

1797
			return aliases;
1798
		};
1799

1800
		const parseFields = (selections: readonly SelectionNode[], parent?: string): string[] => {
1801
			const fields: string[] = [];
1802

1803
			for (let selection of selections) {
1804
				if ((selection.kind === 'Field' || selection.kind === 'InlineFragment') !== true) continue;
1805

1806
				selection = selection as FieldNode | InlineFragmentNode;
1807

1808
				let current: string;
1809
				let currentAlias: string | null = null;
1810

1811
				// Union type (Many-to-Any)
1812
				if (selection.kind === 'InlineFragment') {
1813
					if (selection.typeCondition!.name.value.startsWith('__')) continue;
1814

1815
					current = `${parent}:${selection.typeCondition!.name.value}`;
1816
				}
1817
				// Any other field type
1818
				else {
1819
					// filter out graphql pointers, like __typename
1820
					if (selection.name.value.startsWith('__')) continue;
1821

1822
					current = selection.name.value;
1823

1824
					if (selection.alias) {
1825
						currentAlias = selection.alias.value;
1826
					}
1827

1828
					if (parent) {
1829
						current = `${parent}.${current}`;
1830

1831
						if (currentAlias) {
1832
							currentAlias = `${parent}.${currentAlias}`;
1833

1834
							// add nested aliases into deep query
1835
							if (selection.selectionSet) {
1836
								if (!query.deep) query.deep = {};
1837

1838
								set(
1839
									query.deep,
1840
									parent,
1841
									merge({}, get(query.deep, parent), { _alias: { [selection.alias!.value]: selection.name.value } }),
1842
								);
1843
							}
1844
						}
1845
					}
1846
				}
1847

1848
				if (selection.selectionSet) {
1849
					let children: string[];
1850

1851
					if (current.endsWith('_func')) {
1852
						children = [];
1853

1854
						const rootField = current.slice(0, -5);
1855

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})`);
1860
						}
1861
					} else {
1862
						children = parseFields(selection.selectionSet.selections, currentAlias ?? current);
1863
					}
1864

1865
					fields.push(...children);
1866
				} else {
1867
					fields.push(current);
1868
				}
1869

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 = {};
1873

1874
						const args: Record<string, any> = this.parseArgs(selection.arguments, variableValues);
1875

1876
						set(
1877
							query.deep,
1878
							currentAlias ?? current,
1879
							merge(
1880
								{},
1881
								get(query.deep, currentAlias ?? current),
1882
								mapKeys(sanitizeQuery(args, this.accountability), (_value, key) => `_${key}`),
1883
							),
1884
						);
1885
					}
1886
				}
1887
			}
1888

1889
			return uniq(fields);
1890
		};
1891

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;
1896

1897
		validateQuery(query);
1898

1899
		return query;
1900
	}
1901

1902
	/**
1903
	 * Resolve the aggregation query based on the requested aggregated fields
1904
	 */
1905
	getAggregateQuery(rawQuery: Query, selections: readonly SelectionNode[]): Query {
1906
		const query: Query = sanitizeQuery(rawQuery, this.accountability);
1907

1908
		query.aggregate = {};
1909

1910
		for (let aggregationGroup of selections) {
1911
			if ((aggregationGroup.kind === 'Field') !== true) continue;
1912

1913
			aggregationGroup = aggregationGroup as FieldNode;
1914

1915
			// filter out graphql pointers, like __typename
1916
			if (aggregationGroup.name.value.startsWith('__')) continue;
1917

1918
			const aggregateProperty = aggregationGroup.name.value as keyof Aggregate;
1919

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;
1927
					}) ?? [];
1928
		}
1929

1930
		if (query.filter) {
1931
			query.filter = this.replaceFuncs(query.filter);
1932
		}
1933

1934
		validateQuery(query);
1935

1936
		return query;
1937
	}
1938

1939
	/**
1940
	 * Replace functions from GraphQL format to Directus-Filter format
1941
	 */
1942
	replaceFuncs(filter: Filter): Filter {
1943
		return replaceFuncDeep(filter);
1944

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);
1949

1950
				if (isFunctionKey) {
1951
					const functionName = Object.keys(value)[0]!;
1952
					const fieldName = key.slice(0, -5);
1953

1954
					result[`${functionName}(${fieldName})`] = Object.values(value)[0]!;
1955
				} else {
1956
					result[key] = value?.constructor === Object || value?.constructor === Array ? replaceFuncDeep(value) : value;
1957
				}
1958
			});
1959
		}
1960
	}
1961

1962
	/**
1963
	 * Convert Directus-Exception into a GraphQL format, so it can be returned by GraphQL properly.
1964
	 */
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]);
1969
		}
1970

1971
		set(error, 'extensions.code', error.code);
1972
		return new GraphQLError(error.message, undefined, undefined, undefined, undefined, error);
1973
	}
1974

1975
	/**
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
1978
	 */
1979
	replaceFragmentsInSelections(
1980
		selections: readonly SelectionNode[] | undefined,
1981
		fragments: Record<string, FragmentDefinitionNode>,
1982
	): readonly SelectionNode[] | null {
1983
		if (!selections) return null;
1984

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);
1990
				}
1991

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,
1996
						fragments,
1997
					) as readonly SelectionNode[];
1998
				}
1999

2000
				return selection;
2001
			}),
2002
		).filter((s) => s) as SelectionNode[];
2003

2004
		return result;
2005
	}
2006

2007
	injectSystemResolvers(
2008
		schemaComposer: SchemaComposer<GraphQLParams['contextValue']>,
2009
		{
2010
			CreateCollectionTypes,
2011
			ReadCollectionTypes,
2012
			UpdateCollectionTypes,
2013
			DeleteCollectionTypes,
2014
		}: {
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>>;
2019
		},
2020
		schema: {
2021
			create: SchemaOverview;
2022
			read: SchemaOverview;
2023
			update: SchemaOverview;
2024
			delete: SchemaOverview;
2025
		},
2026
	): SchemaComposer<any> {
2027
		const AuthTokens = schemaComposer.createObjectTC({
2028
			name: 'auth_tokens',
2029
			fields: {
2030
				access_token: GraphQLString,
2031
				expires: GraphQLBigInt,
2032
				refresh_token: GraphQLString,
2033
			},
2034
		});
2035

2036
		const AuthMode = new GraphQLEnumType({
2037
			name: 'auth_mode',
2038
			values: {
2039
				json: { value: 'json' },
2040
				cookie: { value: 'cookie' },
2041
				session: { value: 'session' },
2042
			},
2043
		});
2044

2045
		const ServerInfo = schemaComposer.createObjectTC({
2046
			name: 'server_info',
2047
			fields: {
2048
				project: {
2049
					type: new GraphQLObjectType({
2050
						name: 'server_info_project',
2051
						fields: {
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 },
2063
						},
2064
					}),
2065
				},
2066
			},
2067
		});
2068

2069
		if (this.accountability?.user) {
2070
			ServerInfo.addFields({
2071
				rateLimit: env['RATE_LIMITER_ENABLED']
2072
					? {
2073
							type: new GraphQLObjectType({
2074
								name: 'server_info_rate_limit',
2075
								fields: {
2076
									points: { type: GraphQLInt },
2077
									duration: { type: GraphQLInt },
2078
								},
2079
							}),
2080
					  }
2081
					: GraphQLBoolean,
2082
				rateLimitGlobal: env['RATE_LIMITER_GLOBAL_ENABLED']
2083
					? {
2084
							type: new GraphQLObjectType({
2085
								name: 'server_info_rate_limit_global',
2086
								fields: {
2087
									points: { type: GraphQLInt },
2088
									duration: { type: GraphQLInt },
2089
								},
2090
							}),
2091
					  }
2092
					: GraphQLBoolean,
2093
				websocket: toBoolean(env['WEBSOCKETS_ENABLED'])
2094
					? {
2095
							type: new GraphQLObjectType({
2096
								name: 'server_info_websocket',
2097
								fields: {
2098
									rest: {
2099
										type: toBoolean(env['WEBSOCKETS_REST_ENABLED'])
2100
											? new GraphQLObjectType({
2101
													name: 'server_info_websocket_rest',
2102
													fields: {
2103
														authentication: {
2104
															type: new GraphQLEnumType({
2105
																name: 'server_info_websocket_rest_authentication',
2106
																values: {
2107
																	public: { value: 'public' },
2108
																	handshake: { value: 'handshake' },
2109
																	strict: { value: 'strict' },
2110
																},
2111
															}),
2112
														},
2113
														path: { type: GraphQLString },
2114
													},
2115
											  })
2116
											: GraphQLBoolean,
2117
									},
2118
									graphql: {
2119
										type: toBoolean(env['WEBSOCKETS_GRAPHQL_ENABLED'])
2120
											? new GraphQLObjectType({
2121
													name: 'server_info_websocket_graphql',
2122
													fields: {
2123
														authentication: {
2124
															type: new GraphQLEnumType({
2125
																name: 'server_info_websocket_graphql_authentication',
2126
																values: {
2127
																	public: { value: 'public' },
2128
																	handshake: { value: 'handshake' },
2129
																	strict: { value: 'strict' },
2130
																},
2131
															}),
2132
														},
2133
														path: { type: GraphQLString },
2134
													},
2135
											  })
2136
											: GraphQLBoolean,
2137
									},
2138
									heartbeat: {
2139
										type: toBoolean(env['WEBSOCKETS_HEARTBEAT_ENABLED']) ? GraphQLInt : GraphQLBoolean,
2140
									},
2141
								},
2142
							}),
2143
					  }
2144
					: GraphQLBoolean,
2145
				queryLimit: {
2146
					type: new GraphQLObjectType({
2147
						name: 'server_info_query_limit',
2148
						fields: {
2149
							default: { type: GraphQLInt },
2150
							max: { type: GraphQLInt },
2151
						},
2152
					}),
2153
				},
2154
			});
2155
		}
2156

2157
		/** Globally available query */
2158
		schemaComposer.Query.addFields({
2159
			server_specs_oas: {
2160
				type: GraphQLJSON,
2161
				resolve: async () => {
2162
					const service = new SpecificationService({ schema: this.schema, accountability: this.accountability });
2163
					return await service.oas.generate();
2164
				},
2165
			},
2166
			server_specs_graphql: {
2167
				type: GraphQLString,
2168
				args: {
2169
					scope: new GraphQLEnumType({
2170
						name: 'graphql_sdl_scope',
2171
						values: {
2172
							items: { value: 'items' },
2173
							system: { value: 'system' },
2174
						},
2175
					}),
2176
				},
2177
				resolve: async (_, args) => {
2178
					const service = new GraphQLService({
2179
						schema: this.schema,
2180
						accountability: this.accountability,
2181
						scope: args['scope'] ?? 'items',
2182
					});
2183

2184
					return service.getSchema('sdl');
2185
				},
2186
			},
2187
			server_ping: {
2188
				type: GraphQLString,
2189
				resolve: () => 'pong',
2190
			},
2191
			server_info: {
2192
				type: ServerInfo,
2193
				resolve: async () => {
2194
					const service = new ServerService({
2195
						accountability: this.accountability,
2196
						schema: this.schema,
2197
					});
2198

2199
					return await service.serverInfo();
2200
				},
2201
			},
2202
			server_health: {
2203
				type: GraphQLJSON,
2204
				resolve: async () => {
2205
					const service = new ServerService({
2206
						accountability: this.accountability,
2207
						schema: this.schema,
2208
					});
2209

2210
					return await service.health();
2211
				},
2212
			},
2213
		});
2214

2215
		const Collection = schemaComposer.createObjectTC({
2216
			name: 'directus_collections',
2217
		});
2218

2219
		const Field = schemaComposer.createObjectTC({
2220
			name: 'directus_fields',
2221
		});
2222

2223
		const Relation = schemaComposer.createObjectTC({
2224
			name: 'directus_relations',
2225
		});
2226

2227
		const Extension = schemaComposer.createObjectTC({
2228
			name: 'directus_extensions',
2229
		});
2230

2231
		/**
2232
		 * Globally available mutations
2233
		 */
2234
		schemaComposer.Mutation.addFields({
2235
			auth_login: {
2236
				type: AuthTokens,
2237
				args: {
2238
					email: new GraphQLNonNull(GraphQLString),
2239
					password: new GraphQLNonNull(GraphQLString),
2240
					mode: AuthMode,
2241
					otp: GraphQLString,
2242
				},
2243
				resolve: async (_, args, { req, res }) => {
2244
					const accountability: Accountability = { role: null };
2245

2246
					if (req?.ip) accountability.ip = req.ip;
2247

2248
					const userAgent = req?.get('user-agent');
2249
					if (userAgent) accountability.userAgent = userAgent;
2250

2251
					const origin = req?.get('origin');
2252
					if (origin) accountability.origin = origin;
2253

2254
					const authenticationService = new AuthenticationService({
2255
						accountability: accountability,
2256
						schema: this.schema,
2257
					});
2258

2259
					const mode: AuthenticationMode = args['mode'] ?? 'json';
2260

2261
					const { accessToken, refreshToken, expires } = await authenticationService.login(
2262
						DEFAULT_AUTH_PROVIDER,
2263
						args,
2264
						{
2265
							session: mode === 'session',
2266
							otp: args?.otp,
2267
						},
2268
					);
2269

2270
					const payload = { expires } as { expires: number; access_token?: string; refresh_token?: string };
2271

2272
					if (mode === 'json') {
2273
						payload.refresh_token = refreshToken;
2274
						payload.access_token = accessToken;
2275
					}
2276

2277
					if (mode === 'cookie') {
2278
						res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'] as string, refreshToken, REFRESH_COOKIE_OPTIONS);
2279
						payload.access_token = accessToken;
2280
					}
2281

2282
					if (mode === 'session') {
2283
						res?.cookie(env['SESSION_COOKIE_NAME'] as string, accessToken, SESSION_COOKIE_OPTIONS);
2284
					}
2285

2286
					return payload;
2287
				},
2288
			},
2289
			auth_refresh: {
2290
				type: AuthTokens,
2291
				args: {
2292
					refresh_token: GraphQLString,
2293
					mode: AuthMode,
2294
				},
2295
				resolve: async (_, args, { req, res }) => {
2296
					const accountability: Accountability = { role: null };
2297

2298
					if (req?.ip) accountability.ip = req.ip;
2299

2300
					const userAgent = req?.get('user-agent');
2301
					if (userAgent) accountability.userAgent = userAgent;
2302

2303
					const origin = req?.get('origin');
2304
					if (origin) accountability.origin = origin;
2305

2306
					const authenticationService = new AuthenticationService({
2307
						accountability: accountability,
2308
						schema: this.schema,
2309
					});
2310

2311
					const mode: AuthenticationMode = args['mode'] ?? 'json';
2312
					let currentRefreshToken: string | undefined;
2313

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];
2320

2321
						if (isDirectusJWT(token)) {
2322
							const payload = verifyAccessJWT(token, getSecret());
2323
							currentRefreshToken = payload.session;
2324
						}
2325
					}
2326

2327
					if (!currentRefreshToken) {
2328
						throw new InvalidPayloadError({
2329
							reason: `The refresh token is required in either the payload or cookie`,
2330
						});
2331
					}
2332

2333
					const { accessToken, refreshToken, expires } = await authenticationService.refresh(currentRefreshToken, {
2334
						session: mode === 'session',
2335
					});
2336

2337
					const payload = { expires } as { expires: number; access_token?: string; refresh_token?: string };
2338

2339
					if (mode === 'json') {
2340
						payload.refresh_token = refreshToken;
2341
						payload.access_token = accessToken;
2342
					}
2343

2344
					if (mode === 'cookie') {
2345
						res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'] as string, refreshToken, REFRESH_COOKIE_OPTIONS);
2346
						payload.access_token = accessToken;
2347
					}
2348

2349
					if (mode === 'session') {
2350
						res?.cookie(env['SESSION_COOKIE_NAME'] as string, accessToken, SESSION_COOKIE_OPTIONS);
2351
					}
2352

2353
					return payload;
2354
				},
2355
			},
2356
			auth_logout: {
2357
				type: GraphQLBoolean,
2358
				args: {
2359
					refresh_token: GraphQLString,
2360
					mode: AuthMode,
2361
				},
2362
				resolve: async (_, args, { req, res }) => {
2363
					const accountability: Accountability = { role: null };
2364

2365
					if (req?.ip) accountability.ip = req.ip;
2366

2367
					const userAgent = req?.get('user-agent');
2368
					if (userAgent) accountability.userAgent = userAgent;
2369

2370
					const origin = req?.get('origin');
2371
					if (origin) accountability.origin = origin;
2372

2373
					const authenticationService = new AuthenticationService({
2374
						accountability: accountability,
2375
						schema: this.schema,
2376
					});
2377

2378
					const mode: AuthenticationMode = args['mode'] ?? 'json';
2379
					let currentRefreshToken: string | undefined;
2380

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];
2387

2388
						if (isDirectusJWT(token)) {
2389
							const payload = verifyAccessJWT(token, getSecret());
2390
							currentRefreshToken = payload.session;
2391
						}
2392
					}
2393

2394
					if (!currentRefreshToken) {
2395
						throw new InvalidPayloadError({
2396
							reason: `The refresh token is required in either the payload or cookie`,
2397
						});
2398
					}
2399

2400
					await authenticationService.logout(currentRefreshToken);
2401

2402
					if (req?.cookies[env['REFRESH_TOKEN_COOKIE_NAME'] as string]) {
2403
						res?.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'] as string, REFRESH_COOKIE_OPTIONS);
2404
					}
2405

2406
					if (req?.cookies[env['SESSION_COOKIE_NAME'] as string]) {
2407
						res?.clearCookie(env['SESSION_COOKIE_NAME'] as string, SESSION_COOKIE_OPTIONS);
2408
					}
2409

2410
					return true;
2411
				},
2412
			},
2413
			auth_password_request: {
2414
				type: GraphQLBoolean,
2415
				args: {
2416
					email: new GraphQLNonNull(GraphQLString),
2417
					reset_url: GraphQLString,
2418
				},
2419
				resolve: async (_, args, { req }) => {
2420
					const accountability: Accountability = { role: null };
2421

2422
					if (req?.ip) accountability.ip = req.ip;
2423

2424
					const userAgent = req?.get('user-agent');
2425
					if (userAgent) accountability.userAgent = userAgent;
2426

2427
					const origin = req?.get('origin');
2428
					if (origin) accountability.origin = origin;
2429
					const service = new UsersService({ accountability, schema: this.schema });
2430

2431
					try {
2432
						await service.requestPasswordReset(args['email'], args['reset_url'] || null);
2433
					} catch (err: any) {
2434
						if (isDirectusError(err, ErrorCode.InvalidPayload)) {
2435
							throw err;
2436
						}
2437
					}
2438

2439
					return true;
2440
				},
2441
			},
2442
			auth_password_reset: {
2443
				type: GraphQLBoolean,
2444
				args: {
2445
					token: new GraphQLNonNull(GraphQLString),
2446
					password: new GraphQLNonNull(GraphQLString),
2447
				},
2448
				resolve: async (_, args, { req }) => {
2449
					const accountability: Accountability = { role: null };
2450

2451
					if (req?.ip) accountability.ip = req.ip;
2452

2453
					const userAgent = req?.get('user-agent');
2454
					if (userAgent) accountability.userAgent = userAgent;
2455

2456
					const origin = req?.get('origin');
2457
					if (origin) accountability.origin = origin;
2458

2459
					const service = new UsersService({ accountability, schema: this.schema });
2460
					await service.resetPassword(args['token'], args['password']);
2461
					return true;
2462
				},
2463
			},
2464
			users_me_tfa_generate: {
2465
				type: new GraphQLObjectType({
2466
					name: 'users_me_tfa_generate_data',
2467
					fields: {
2468
						secret: { type: GraphQLString },
2469
						otpauth_url: { type: GraphQLString },
2470
					},
2471
				}),
2472
				args: {
2473
					password: new GraphQLNonNull(GraphQLString),
2474
				},
2475
				resolve: async (_, args) => {
2476
					if (!this.accountability?.user) return null;
2477

2478
					const service = new TFAService({
2479
						accountability: this.accountability,
2480
						schema: this.schema,
2481
					});
2482

2483
					const authService = new AuthenticationService({
2484
						accountability: this.accountability,
2485
						schema: this.schema,
2486
					});
2487

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 };
2491
				},
2492
			},
2493
			users_me_tfa_enable: {
2494
				type: GraphQLBoolean,
2495
				args: {
2496
					otp: new GraphQLNonNull(GraphQLString),
2497
					secret: new GraphQLNonNull(GraphQLString),
2498
				},
2499
				resolve: async (_, args) => {
2500
					if (!this.accountability?.user) return null;
2501

2502
					const service = new TFAService({
2503
						accountability: this.accountability,
2504
						schema: this.schema,
2505
					});
2506

2507
					await service.enableTFA(this.accountability.user, args['otp'], args['secret']);
2508
					return true;
2509
				},
2510
			},
2511
			users_me_tfa_disable: {
2512
				type: GraphQLBoolean,
2513
				args: {
2514
					otp: new GraphQLNonNull(GraphQLString),
2515
				},
2516
				resolve: async (_, args) => {
2517
					if (!this.accountability?.user) return null;
2518

2519
					const service = new TFAService({
2520
						accountability: this.accountability,
2521
						schema: this.schema,
2522
					});
2523

2524
					const otpValid = await service.verifyOTP(this.accountability.user, args['otp']);
2525

2526
					if (otpValid === false) {
2527
						throw new InvalidPayloadError({ reason: `"otp" is invalid` });
2528
					}
2529

2530
					await service.disableTFA(this.accountability.user);
2531
					return true;
2532
				},
2533
			},
2534
			utils_random_string: {
2535
				type: GraphQLString,
2536
				args: {
2537
					length: GraphQLInt,
2538
				},
2539
				resolve: async (_, args) => {
2540
					const { nanoid } = await import('nanoid');
2541

2542
					if (args['length'] !== undefined && (args['length'] < 1 || args['length'] > 500)) {
2543
						throw new InvalidPayloadError({ reason: `"length" must be between 1 and 500` });
2544
					}
2545

2546
					return nanoid(args['length'] ? args['length'] : 32);
2547
				},
2548
			},
2549
			utils_hash_generate: {
2550
				type: GraphQLString,
2551
				args: {
2552
					string: new GraphQLNonNull(GraphQLString),
2553
				},
2554
				resolve: async (_, args) => {
2555
					return await generateHash(args['string']);
2556
				},
2557
			},
2558
			utils_hash_verify: {
2559
				type: GraphQLBoolean,
2560
				args: {
2561
					string: new GraphQLNonNull(GraphQLString),
2562
					hash: new GraphQLNonNull(GraphQLString),
2563
				},
2564
				resolve: async (_, args) => {
2565
					return await argon2.verify(args['hash'], args['string']);
2566
				},
2567
			},
2568
			utils_sort: {
2569
				type: GraphQLBoolean,
2570
				args: {
2571
					collection: new GraphQLNonNull(GraphQLString),
2572
					item: new GraphQLNonNull(GraphQLID),
2573
					to: new GraphQLNonNull(GraphQLID),
2574
				},
2575
				resolve: async (_, args) => {
2576
					const service = new UtilsService({
2577
						accountability: this.accountability,
2578
						schema: this.schema,
2579
					});
2580

2581
					const { item, to } = args;
2582
					await service.sort(args['collection'], { item, to });
2583
					return true;
2584
				},
2585
			},
2586
			utils_revert: {
2587
				type: GraphQLBoolean,
2588
				args: {
2589
					revision: new GraphQLNonNull(GraphQLID),
2590
				},
2591
				resolve: async (_, args) => {
2592
					const service = new RevisionsService({
2593
						accountability: this.accountability,
2594
						schema: this.schema,
2595
					});
2596

2597
					await service.revert(args['revision']);
2598
					return true;
2599
				},
2600
			},
2601
			utils_cache_clear: {
2602
				type: GraphQLVoid,
2603
				resolve: async () => {
2604
					if (this.accountability?.admin !== true) {
2605
						throw new ForbiddenError();
2606
					}
2607

2608
					const { cache } = getCache();
2609

2610
					await cache?.clear();
2611
					await clearSystemCache();
2612

2613
					return;
2614
				},
2615
			},
2616
			users_invite_accept: {
2617
				type: GraphQLBoolean,
2618
				args: {
2619
					token: new GraphQLNonNull(GraphQLString),
2620
					password: new GraphQLNonNull(GraphQLString),
2621
				},
2622
				resolve: async (_, args) => {
2623
					const service = new UsersService({
2624
						accountability: this.accountability,
2625
						schema: this.schema,
2626
					});
2627

2628
					await service.acceptInvite(args['token'], args['password']);
2629
					return true;
2630
				},
2631
			},
2632
			users_register: {
2633
				type: GraphQLBoolean,
2634
				args: {
2635
					email: new GraphQLNonNull(GraphQLString),
2636
					password: new GraphQLNonNull(GraphQLString),
2637
					verification_url: GraphQLString,
2638
					first_name: GraphQLString,
2639
					last_name: GraphQLString,
2640
				},
2641
				resolve: async (_, args, { req }) => {
2642
					const service = new UsersService({ accountability: null, schema: this.schema });
2643

2644
					const ip = req ? getIPFromReq(req) : null;
2645

2646
					if (ip) {
2647
						await rateLimiter.consume(ip);
2648
					}
2649

2650
					await service.registerUser({
2651
						email: args.email,
2652
						password: args.password,
2653
						verification_url: args.verification_url,
2654
						first_name: args.first_name,
2655
						last_name: args.last_name,
2656
					});
2657

2658
					return true;
2659
				},
2660
			},
2661
			users_register_verify: {
2662
				type: GraphQLBoolean,
2663
				args: {
2664
					token: new GraphQLNonNull(GraphQLString),
2665
				},
2666
				resolve: async (_, args) => {
2667
					const service = new UsersService({ accountability: null, schema: this.schema });
2668
					await service.verifyRegistration(args.token);
2669
					return true;
2670
				},
2671
			},
2672
		});
2673

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(
2680
						(acc, field) => {
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>;
2687

2688
							return acc;
2689
						},
2690
						{} as ObjectTypeComposerFieldConfigAsObjectDefinition<any, any>,
2691
					),
2692
				}),
2693
				schema: schemaComposer.createObjectTC({
2694
					name: 'directus_collections_schema',
2695
					fields: {
2696
						name: GraphQLString,
2697
						comment: GraphQLString,
2698
					},
2699
				}),
2700
			});
2701

2702
			schemaComposer.Query.addFields({
2703
				collections: {
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,
2709
						});
2710

2711
						return await collectionsService.readByQuery();
2712
					},
2713
				},
2714

2715
				collections_by_name: {
2716
					type: Collection,
2717
					args: {
2718
						name: new GraphQLNonNull(GraphQLString),
2719
					},
2720
					resolve: async (_, args) => {
2721
						const collectionsService = new CollectionsService({
2722
							accountability: this.accountability,
2723
							schema: this.schema,
2724
						});
2725

2726
						return await collectionsService.readOne(args['name']);
2727
					},
2728
				},
2729
			});
2730
		}
2731

2732
		if ('directus_fields' in schema.read.collections) {
2733
			Field.addFields({
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(
2740
						(acc, field) => {
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>;
2747

2748
							return acc;
2749
						},
2750
						{} as ObjectTypeComposerFieldConfigAsObjectDefinition<any, any>,
2751
					),
2752
				}),
2753
				schema: schemaComposer.createObjectTC({
2754
					name: 'directus_fields_schema',
2755
					fields: {
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,
2770
					},
2771
				}),
2772
			});
2773

2774
			schemaComposer.Query.addFields({
2775
				fields: {
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,
2781
						});
2782

2783
						return await service.readAll();
2784
					},
2785
				},
2786
				fields_in_collection: {
2787
					type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Field.getType()))),
2788
					args: {
2789
						collection: new GraphQLNonNull(GraphQLString),
2790
					},
2791
					resolve: async (_, args) => {
2792
						const service = new FieldsService({
2793
							accountability: this.accountability,
2794
							schema: this.schema,
2795
						});
2796

2797
						return await service.readAll(args['collection']);
2798
					},
2799
				},
2800
				fields_by_name: {
2801
					type: Field,
2802
					args: {
2803
						collection: new GraphQLNonNull(GraphQLString),
2804
						field: new GraphQLNonNull(GraphQLString),
2805
					},
2806
					resolve: async (_, args) => {
2807
						const service = new FieldsService({
2808
							accountability: this.accountability,
2809
							schema: this.schema,
2810
						});
2811

2812
						return await service.readOne(args['collection'], args['field']);
2813
					},
2814
				},
2815
			});
2816
		}
2817

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',
2825
					fields: {
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),
2833
					},
2834
				}),
2835
				meta: schemaComposer.createObjectTC({
2836
					name: 'directus_relations_meta',
2837
					fields: Object.values(schema.read.collections['directus_relations']!.fields).reduce(
2838
						(acc, field) => {
2839
							acc[field.field] = {
2840
								type: getGraphQLType(field.type, field.special),
2841
								description: field.note,
2842
							} as ObjectTypeComposerFieldConfigDefinition<any, any, any>;
2843

2844
							return acc;
2845
						},
2846
						{} as ObjectTypeComposerFieldConfigAsObjectDefinition<any, any>,
2847
					),
2848
				}),
2849
			});
2850

2851
			schemaComposer.Query.addFields({
2852
				relations: {
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,
2858
						});
2859

2860
						return await service.readAll();
2861
					},
2862
				},
2863
				relations_in_collection: {
2864
					type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Relation.getType()))),
2865
					args: {
2866
						collection: new GraphQLNonNull(GraphQLString),
2867
					},
2868
					resolve: async (_, args) => {
2869
						const service = new RelationsService({
2870
							accountability: this.accountability,
2871
							schema: this.schema,
2872
						});
2873

2874
						return await service.readAll(args['collection']);
2875
					},
2876
				},
2877
				relations_by_name: {
2878
					type: Relation,
2879
					args: {
2880
						collection: new GraphQLNonNull(GraphQLString),
2881
						field: new GraphQLNonNull(GraphQLString),
2882
					},
2883
					resolve: async (_, args) => {
2884
						const service = new RelationsService({
2885
							accountability: this.accountability,
2886
							schema: this.schema,
2887
						});
2888

2889
						return await service.readOne(args['collection'], args['field']);
2890
					},
2891
				},
2892
			});
2893
		}
2894

2895
		if (this.accountability?.admin === true) {
2896
			schemaComposer.Mutation.addFields({
2897
				create_collections_item: {
2898
					type: Collection,
2899
					args: {
2900
						data: toInputObjectType(Collection.clone('create_directus_collections'), {
2901
							postfix: '_input',
2902
						}).addFields({
2903
							fields: [
2904
								toInputObjectType(Field.clone('create_directus_collections_fields'), { postfix: '_input' }).NonNull,
2905
							],
2906
						}).NonNull,
2907
					},
2908
					resolve: async (_, args) => {
2909
						const collectionsService = new CollectionsService({
2910
							accountability: this.accountability,
2911
							schema: this.schema,
2912
						});
2913

2914
						const collectionKey = await collectionsService.createOne(args['data']);
2915
						return await collectionsService.readOne(collectionKey);
2916
					},
2917
				},
2918
				update_collections_item: {
2919
					type: Collection,
2920
					args: {
2921
						collection: new GraphQLNonNull(GraphQLString),
2922
						data: toInputObjectType(Collection.clone('update_directus_collections'), {
2923
							postfix: '_input',
2924
						}).removeField(['collection', 'schema']).NonNull,
2925
					},
2926
					resolve: async (_, args) => {
2927
						const collectionsService = new CollectionsService({
2928
							accountability: this.accountability,
2929
							schema: this.schema,
2930
						});
2931

2932
						const collectionKey = await collectionsService.updateOne(args['collection'], args['data']);
2933
						return await collectionsService.readOne(collectionKey);
2934
					},
2935
				},
2936
				delete_collections_item: {
2937
					type: schemaComposer.createObjectTC({
2938
						name: 'delete_collection',
2939
						fields: {
2940
							collection: GraphQLString,
2941
						},
2942
					}),
2943
					args: {
2944
						collection: new GraphQLNonNull(GraphQLString),
2945
					},
2946
					resolve: async (_, args) => {
2947
						const collectionsService = new CollectionsService({
2948
							accountability: this.accountability,
2949
							schema: this.schema,
2950
						});
2951

2952
						await collectionsService.deleteOne(args['collection']);
2953
						return { collection: args['collection'] };
2954
					},
2955
				},
2956
			});
2957

2958
			schemaComposer.Mutation.addFields({
2959
				create_fields_item: {
2960
					type: Field,
2961
					args: {
2962
						collection: new GraphQLNonNull(GraphQLString),
2963
						data: toInputObjectType(Field.clone('create_directus_fields'), { postfix: '_input' }).NonNull,
2964
					},
2965
					resolve: async (_, args) => {
2966
						const service = new FieldsService({
2967
							accountability: this.accountability,
2968
							schema: this.schema,
2969
						});
2970

2971
						await service.createField(args['collection'], args['data']);
2972
						return await service.readOne(args['collection'], args['data'].field);
2973
					},
2974
				},
2975
				update_fields_item: {
2976
					type: Field,
2977
					args: {
2978
						collection: new GraphQLNonNull(GraphQLString),
2979
						field: new GraphQLNonNull(GraphQLString),
2980
						data: toInputObjectType(Field.clone('update_directus_fields'), { postfix: '_input' }).NonNull,
2981
					},
2982
					resolve: async (_, args) => {
2983
						const service = new FieldsService({
2984
							accountability: this.accountability,
2985
							schema: this.schema,
2986
						});
2987

2988
						await service.updateField(args['collection'], {
2989
							...args['data'],
2990
							field: args['field'],
2991
						});
2992

2993
						return await service.readOne(args['collection'], args['data'].field);
2994
					},
2995
				},
2996
				delete_fields_item: {
2997
					type: schemaComposer.createObjectTC({
2998
						name: 'delete_field',
2999
						fields: {
3000
							collection: GraphQLString,
3001
							field: GraphQLString,
3002
						},
3003
					}),
3004
					args: {
3005
						collection: new GraphQLNonNull(GraphQLString),
3006
						field: new GraphQLNonNull(GraphQLString),
3007
					},
3008
					resolve: async (_, args) => {
3009
						const service = new FieldsService({
3010
							accountability: this.accountability,
3011
							schema: this.schema,
3012
						});
3013

3014
						await service.deleteField(args['collection'], args['field']);
3015
						const { collection, field } = args;
3016
						return { collection, field };
3017
					},
3018
				},
3019
			});
3020

3021
			schemaComposer.Mutation.addFields({
3022
				create_relations_item: {
3023
					type: Relation,
3024
					args: {
3025
						data: toInputObjectType(Relation.clone('create_directus_relations'), { postfix: '_input' }).NonNull,
3026
					},
3027
					resolve: async (_, args) => {
3028
						const relationsService = new RelationsService({
3029
							accountability: this.accountability,
3030
							schema: this.schema,
3031
						});
3032

3033
						await relationsService.createOne(args['data']);
3034
						return await relationsService.readOne(args['data'].collection, args['data'].field);
3035
					},
3036
				},
3037
				update_relations_item: {
3038
					type: Relation,
3039
					args: {
3040
						collection: new GraphQLNonNull(GraphQLString),
3041
						field: new GraphQLNonNull(GraphQLString),
3042
						data: toInputObjectType(Relation.clone('update_directus_relations'), { postfix: '_input' }).NonNull,
3043
					},
3044
					resolve: async (_, args) => {
3045
						const relationsService = new RelationsService({
3046
							accountability: this.accountability,
3047
							schema: this.schema,
3048
						});
3049

3050
						await relationsService.updateOne(args['collection'], args['field'], args['data']);
3051
						return await relationsService.readOne(args['data'].collection, args['data'].field);
3052
					},
3053
				},
3054
				delete_relations_item: {
3055
					type: schemaComposer.createObjectTC({
3056
						name: 'delete_relation',
3057
						fields: {
3058
							collection: GraphQLString,
3059
							field: GraphQLString,
3060
						},
3061
					}),
3062
					args: {
3063
						collection: new GraphQLNonNull(GraphQLString),
3064
						field: new GraphQLNonNull(GraphQLString),
3065
					},
3066
					resolve: async (_, args) => {
3067
						const relationsService = new RelationsService({
3068
							accountability: this.accountability,
3069
							schema: this.schema,
3070
						});
3071

3072
						await relationsService.deleteOne(args['collection'], args['field']);
3073
						return { collection: args['collection'], field: args['field'] };
3074
					},
3075
				},
3076
			});
3077

3078
			Extension.addFields({
3079
				bundle: GraphQLString,
3080
				name: new GraphQLNonNull(GraphQLString),
3081
				schema: schemaComposer.createObjectTC({
3082
					name: 'directus_extensions_schema',
3083
					fields: {
3084
						type: GraphQLString,
3085
						local: GraphQLBoolean,
3086
					},
3087
				}),
3088
				meta: schemaComposer.createObjectTC({
3089
					name: 'directus_extensions_meta',
3090
					fields: {
3091
						enabled: GraphQLBoolean,
3092
					},
3093
				}),
3094
			});
3095

3096
			schemaComposer.Query.addFields({
3097
				extensions: {
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,
3103
						});
3104

3105
						return await service.readAll();
3106
					},
3107
				},
3108
			});
3109

3110
			schemaComposer.Mutation.addFields({
3111
				update_extensions_item: {
3112
					type: Extension,
3113
					args: {
3114
						id: GraphQLID,
3115
						data: toInputObjectType(
3116
							schemaComposer.createObjectTC({
3117
								name: 'update_directus_extensions_input',
3118
								fields: {
3119
									meta: schemaComposer.createObjectTC({
3120
										name: 'update_directus_extensions_input_meta',
3121
										fields: {
3122
											enabled: GraphQLBoolean,
3123
										},
3124
									}),
3125
								},
3126
							}),
3127
						),
3128
					},
3129
					resolve: async (_, args) => {
3130
						const extensionsService = new ExtensionsService({
3131
							accountability: this.accountability,
3132
							schema: this.schema,
3133
						});
3134

3135
						await extensionsService.updateOne(args['id'], args['data']);
3136
						return await extensionsService.readOne(args['id']);
3137
					},
3138
				},
3139
			});
3140
		}
3141

3142
		if ('directus_users' in schema.read.collections) {
3143
			schemaComposer.Query.addFields({
3144
				users_me: {
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 });
3149

3150
						const selections = this.replaceFragmentsInSelections(
3151
							info.fieldNodes[0]?.selectionSet?.selections,
3152
							info.fragments,
3153
						);
3154

3155
						const query = this.getQuery(args, selections || [], info.variableValues);
3156

3157
						return await service.readOne(this.accountability.user, query);
3158
					},
3159
				},
3160
			});
3161
		}
3162

3163
		if ('directus_users' in schema.update.collections && this.accountability?.user) {
3164
			schemaComposer.Mutation.addFields({
3165
				update_users_me: {
3166
					type: ReadCollectionTypes['directus_users']!,
3167
					args: {
3168
						data: toInputObjectType(UpdateCollectionTypes['directus_users']!),
3169
					},
3170
					resolve: async (_, args, __, info) => {
3171
						if (!this.accountability?.user) return null;
3172

3173
						const service = new UsersService({
3174
							schema: this.schema,
3175
							accountability: this.accountability,
3176
						});
3177

3178
						await service.updateOne(this.accountability.user, args['data']);
3179

3180
						if ('directus_users' in ReadCollectionTypes) {
3181
							const selections = this.replaceFragmentsInSelections(
3182
								info.fieldNodes[0]?.selectionSet?.selections,
3183
								info.fragments,
3184
							);
3185

3186
							const query = this.getQuery(args, selections || [], info.variableValues);
3187

3188
							return await service.readOne(this.accountability.user, query);
3189
						}
3190

3191
						return true;
3192
					},
3193
				},
3194
			});
3195
		}
3196

3197
		if ('directus_activity' in schema.create.collections) {
3198
			schemaComposer.Mutation.addFields({
3199
				create_comment: {
3200
					type: ReadCollectionTypes['directus_activity'] ?? GraphQLBoolean,
3201
					args: {
3202
						collection: new GraphQLNonNull(GraphQLString),
3203
						item: new GraphQLNonNull(GraphQLID),
3204
						comment: new GraphQLNonNull(GraphQLString),
3205
					},
3206
					resolve: async (_, args, __, info) => {
3207
						const service = new ActivityService({
3208
							accountability: this.accountability,
3209
							schema: this.schema,
3210
						});
3211

3212
						const primaryKey = await service.createOne({
3213
							...args,
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,
3219
						});
3220

3221
						if ('directus_activity' in ReadCollectionTypes) {
3222
							const selections = this.replaceFragmentsInSelections(
3223
								info.fieldNodes[0]?.selectionSet?.selections,
3224
								info.fragments,
3225
							);
3226

3227
							const query = this.getQuery(args, selections || [], info.variableValues);
3228

3229
							return await service.readOne(primaryKey, query);
3230
						}
3231

3232
						return true;
3233
					},
3234
				},
3235
			});
3236
		}
3237

3238
		if ('directus_activity' in schema.update.collections) {
3239
			schemaComposer.Mutation.addFields({
3240
				update_comment: {
3241
					type: ReadCollectionTypes['directus_activity'] ?? GraphQLBoolean,
3242
					args: {
3243
						id: new GraphQLNonNull(GraphQLID),
3244
						comment: new GraphQLNonNull(GraphQLString),
3245
					},
3246
					resolve: async (_, args, __, info) => {
3247
						const service = new ActivityService({
3248
							accountability: this.accountability,
3249
							schema: this.schema,
3250
						});
3251

3252
						const primaryKey = await service.updateOne(args['id'], { comment: args['comment'] });
3253

3254
						if ('directus_activity' in ReadCollectionTypes) {
3255
							const selections = this.replaceFragmentsInSelections(
3256
								info.fieldNodes[0]?.selectionSet?.selections,
3257
								info.fragments,
3258
							);
3259

3260
							const query = this.getQuery(args, selections || [], info.variableValues);
3261

3262
							return await service.readOne(primaryKey, query);
3263
						}
3264

3265
						return true;
3266
					},
3267
				},
3268
			});
3269
		}
3270

3271
		if ('directus_activity' in schema.delete.collections) {
3272
			schemaComposer.Mutation.addFields({
3273
				delete_comment: {
3274
					type: DeleteCollectionTypes['one']!,
3275
					args: {
3276
						id: new GraphQLNonNull(GraphQLID),
3277
					},
3278
					resolve: async (_, args) => {
3279
						const service = new ActivityService({
3280
							accountability: this.accountability,
3281
							schema: this.schema,
3282
						});
3283

3284
						await service.deleteOne(args['id']);
3285
						return { id: args['id'] };
3286
					},
3287
				},
3288
			});
3289
		}
3290

3291
		if ('directus_files' in schema.create.collections) {
3292
			schemaComposer.Mutation.addFields({
3293
				import_file: {
3294
					type: ReadCollectionTypes['directus_files'] ?? GraphQLBoolean,
3295
					args: {
3296
						url: new GraphQLNonNull(GraphQLString),
3297
						data: toInputObjectType(CreateCollectionTypes['directus_files']!).setTypeName(
3298
							'create_directus_files_input',
3299
						),
3300
					},
3301
					resolve: async (_, args, __, info) => {
3302
						const service = new FilesService({
3303
							accountability: this.accountability,
3304
							schema: this.schema,
3305
						});
3306

3307
						const primaryKey = await service.importOne(args['url'], args['data']);
3308

3309
						if ('directus_files' in ReadCollectionTypes) {
3310
							const selections = this.replaceFragmentsInSelections(
3311
								info.fieldNodes[0]?.selectionSet?.selections,
3312
								info.fragments,
3313
							);
3314

3315
							const query = this.getQuery(args, selections || [], info.variableValues);
3316
							return await service.readOne(primaryKey, query);
3317
						}
3318

3319
						return true;
3320
					},
3321
				},
3322
			});
3323
		}
3324

3325
		if ('directus_users' in schema.create.collections) {
3326
			schemaComposer.Mutation.addFields({
3327
				users_invite: {
3328
					type: GraphQLBoolean,
3329
					args: {
3330
						email: new GraphQLNonNull(GraphQLString),
3331
						role: new GraphQLNonNull(GraphQLString),
3332
						invite_url: GraphQLString,
3333
					},
3334
					resolve: async (_, args) => {
3335
						const service = new UsersService({
3336
							accountability: this.accountability,
3337
							schema: this.schema,
3338
						});
3339

3340
						await service.inviteUser(args['email'], args['role'], args['invite_url'] || null);
3341
						return true;
3342
					},
3343
				},
3344
			});
3345
		}
3346

3347
		return schemaComposer;
3348
	}
3349
}
3350

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.