@directus/api 25.0.1 → 26.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/dist/app.js +3 -3
  2. package/dist/auth/drivers/oauth2.d.ts +2 -0
  3. package/dist/auth/drivers/oauth2.js +40 -2
  4. package/dist/auth/drivers/openid.js +8 -1
  5. package/dist/controllers/access.js +2 -2
  6. package/dist/controllers/comments.js +2 -2
  7. package/dist/controllers/dashboards.js +2 -2
  8. package/dist/controllers/files.js +2 -2
  9. package/dist/controllers/flows.js +2 -2
  10. package/dist/controllers/folders.js +2 -2
  11. package/dist/controllers/items.js +2 -2
  12. package/dist/controllers/notifications.js +2 -2
  13. package/dist/controllers/operations.js +2 -2
  14. package/dist/controllers/panels.js +2 -2
  15. package/dist/controllers/permissions.js +2 -2
  16. package/dist/controllers/policies.js +2 -2
  17. package/dist/controllers/presets.js +2 -2
  18. package/dist/controllers/roles.js +2 -2
  19. package/dist/controllers/shares.js +2 -2
  20. package/dist/controllers/translations.js +2 -2
  21. package/dist/controllers/users.js +2 -2
  22. package/dist/controllers/utils.js +8 -3
  23. package/dist/controllers/versions.js +2 -2
  24. package/dist/controllers/webhooks.js +1 -1
  25. package/dist/database/helpers/capabilities/dialects/default.d.ts +3 -0
  26. package/dist/database/helpers/capabilities/dialects/default.js +3 -0
  27. package/dist/database/helpers/capabilities/dialects/mysql.d.ts +4 -0
  28. package/dist/database/helpers/capabilities/dialects/mysql.js +9 -0
  29. package/dist/database/helpers/capabilities/dialects/postgres.d.ts +5 -0
  30. package/dist/database/helpers/capabilities/dialects/postgres.js +14 -0
  31. package/dist/database/helpers/capabilities/index.d.ts +7 -0
  32. package/dist/database/helpers/capabilities/index.js +7 -0
  33. package/dist/database/helpers/capabilities/types.d.ts +11 -0
  34. package/dist/database/helpers/capabilities/types.js +15 -0
  35. package/dist/database/helpers/index.d.ts +2 -0
  36. package/dist/database/helpers/index.js +2 -0
  37. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +1 -2
  38. package/dist/database/helpers/schema/dialects/cockroachdb.js +0 -4
  39. package/dist/database/helpers/schema/dialects/postgres.d.ts +1 -2
  40. package/dist/database/helpers/schema/dialects/postgres.js +0 -4
  41. package/dist/database/index.js +1 -1
  42. package/dist/database/migrations/20250224A-visual-editor.d.ts +3 -0
  43. package/dist/database/migrations/20250224A-visual-editor.js +35 -0
  44. package/dist/database/run-ast/lib/get-db-query.js +16 -4
  45. package/dist/logger/index.js +3 -3
  46. package/dist/middleware/sanitize-query.js +17 -7
  47. package/dist/middleware/validate-batch.js +1 -1
  48. package/dist/operations/item-delete/index.js +1 -1
  49. package/dist/operations/item-read/index.js +1 -1
  50. package/dist/operations/item-update/index.js +1 -1
  51. package/dist/permissions/lib/fetch-permissions.js +6 -4
  52. package/dist/permissions/modules/process-ast/utils/context-has-dynamic-variables.d.ts +2 -0
  53. package/dist/permissions/modules/process-ast/utils/context-has-dynamic-variables.js +3 -0
  54. package/dist/permissions/modules/process-payload/process-payload.d.ts +1 -0
  55. package/dist/permissions/modules/process-payload/process-payload.js +19 -4
  56. package/dist/permissions/types.d.ts +2 -1
  57. package/dist/permissions/utils/extract-required-dynamic-variable-context.d.ts +3 -2
  58. package/dist/permissions/utils/extract-required-dynamic-variable-context.js +24 -5
  59. package/dist/permissions/utils/fetch-dynamic-variable-data.d.ts +9 -0
  60. package/dist/permissions/utils/{fetch-dynamic-variable-context.js → fetch-dynamic-variable-data.js} +13 -12
  61. package/dist/rate-limiter.js +1 -1
  62. package/dist/services/assets.js +12 -2
  63. package/dist/services/authentication.js +2 -2
  64. package/dist/services/collections.js +8 -2
  65. package/dist/services/graphql/resolvers/get-collection-type.d.ts +3 -0
  66. package/dist/services/graphql/resolvers/get-collection-type.js +34 -0
  67. package/dist/services/graphql/resolvers/get-field-type.d.ts +3 -0
  68. package/dist/services/graphql/resolvers/get-field-type.js +51 -0
  69. package/dist/services/graphql/resolvers/get-relation-type.d.ts +3 -0
  70. package/dist/services/graphql/resolvers/get-relation-type.js +39 -0
  71. package/dist/services/graphql/resolvers/mutation.js +1 -1
  72. package/dist/services/graphql/resolvers/query.js +4 -4
  73. package/dist/services/graphql/resolvers/system-admin.d.ts +2 -2
  74. package/dist/services/graphql/resolvers/system-admin.js +207 -199
  75. package/dist/services/graphql/resolvers/system.d.ts +1 -7
  76. package/dist/services/graphql/resolvers/system.js +12 -113
  77. package/dist/services/graphql/schema/index.js +1 -1
  78. package/dist/services/graphql/schema/parse-query.d.ts +2 -2
  79. package/dist/services/graphql/schema/parse-query.js +6 -6
  80. package/dist/services/graphql/schema/read.d.ts +2 -2
  81. package/dist/services/graphql/schema/read.js +86 -2
  82. package/dist/services/graphql/schema-cache.d.ts +2 -2
  83. package/dist/services/graphql/schema-cache.js +1 -3
  84. package/dist/services/graphql/subscription.d.ts +3 -3
  85. package/dist/services/graphql/subscription.js +3 -3
  86. package/dist/services/graphql/utils/{aggrgate-query.d.ts → aggregate-query.d.ts} +2 -2
  87. package/dist/services/graphql/utils/{aggrgate-query.js → aggregate-query.js} +3 -3
  88. package/dist/services/items.d.ts +1 -0
  89. package/dist/services/items.js +30 -16
  90. package/dist/services/payload.d.ts +1 -0
  91. package/dist/services/payload.js +32 -17
  92. package/dist/services/shares.js +1 -1
  93. package/dist/services/specifications.js +10 -5
  94. package/dist/services/tus/lockers.d.ts +1 -1
  95. package/dist/services/tus/lockers.js +6 -5
  96. package/dist/services/tus/server.js +24 -0
  97. package/dist/services/users.js +1 -0
  98. package/dist/types/services.d.ts +2 -0
  99. package/dist/utils/apply-query.d.ts +1 -0
  100. package/dist/utils/apply-query.js +21 -12
  101. package/dist/utils/generate-hash.js +1 -1
  102. package/dist/utils/get-config-from-env.d.ts +6 -1
  103. package/dist/utils/get-config-from-env.js +16 -11
  104. package/dist/utils/get-graphql-type.js +3 -1
  105. package/dist/utils/is-login-redirect-allowed.js +2 -0
  106. package/dist/utils/redact-object.js +5 -1
  107. package/dist/utils/sanitize-query.d.ts +5 -2
  108. package/dist/utils/sanitize-query.js +34 -9
  109. package/dist/websocket/controllers/base.d.ts +2 -2
  110. package/dist/websocket/handlers/items.js +4 -4
  111. package/dist/websocket/handlers/subscribe.js +2 -2
  112. package/dist/websocket/messages.d.ts +7 -7
  113. package/dist/websocket/messages.js +1 -1
  114. package/package.json +59 -59
  115. package/dist/permissions/utils/fetch-dynamic-variable-context.d.ts +0 -8
@@ -1,11 +1,10 @@
1
1
  import { useEnv } from '@directus/env';
2
2
  import { toBoolean } from '@directus/utils';
3
3
  import { GraphQLBoolean, GraphQLEnumType, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLString, } from 'graphql';
4
- import { GraphQLJSON, ObjectTypeComposer, SchemaComposer, toInputObjectType } from 'graphql-compose';
4
+ import { GraphQLJSON, SchemaComposer, toInputObjectType } from 'graphql-compose';
5
5
  import getDatabase from '../../../database/index.js';
6
6
  import { fetchAccountabilityCollectionAccess } from '../../../permissions/modules/fetch-accountability-collection-access/fetch-accountability-collection-access.js';
7
7
  import { fetchAccountabilityPolicyGlobals } from '../../../permissions/modules/fetch-accountability-policy-globals/fetch-accountability-policy-globals.js';
8
- import { getGraphQLType } from '../../../utils/get-graphql-type.js';
9
8
  import { CollectionsService } from '../../collections.js';
10
9
  import { FieldsService } from '../../fields.js';
11
10
  import { FilesService } from '../../files.js';
@@ -18,6 +17,9 @@ import { GraphQLService } from '../index.js';
18
17
  import { generateSchema } from '../schema/index.js';
19
18
  import { getQuery } from '../schema/parse-query.js';
20
19
  import { replaceFragmentsInSelections } from '../utils/replace-fragments.js';
20
+ import { getCollectionType } from './get-collection-type.js';
21
+ import { getFieldType } from './get-field-type.js';
22
+ import { getRelationType } from './get-relation-type.js';
21
23
  import { resolveSystemAdmin } from './system-admin.js';
22
24
  import { globalResolvers } from './system-global.js';
23
25
  const env = useEnv();
@@ -187,47 +189,8 @@ export function injectSystemResolvers(gql, schemaComposer, { CreateCollectionTyp
187
189
  },
188
190
  },
189
191
  });
190
- const Collection = schemaComposer.createObjectTC({
191
- name: 'directus_collections',
192
- });
193
- const Field = schemaComposer.createObjectTC({
194
- name: 'directus_fields',
195
- });
196
- const Relation = schemaComposer.createObjectTC({
197
- name: 'directus_relations',
198
- });
199
- const Extension = schemaComposer.createObjectTC({
200
- name: 'directus_extensions',
201
- });
202
- const composers = {
203
- Collection,
204
- Field,
205
- Relation,
206
- Extension,
207
- };
208
192
  if ('directus_collections' in schema.read.collections) {
209
- Collection.addFields({
210
- collection: GraphQLString,
211
- meta: schemaComposer.createObjectTC({
212
- name: 'directus_collections_meta',
213
- fields: Object.values(schema.read.collections['directus_collections'].fields).reduce((acc, field) => {
214
- acc[field.field] = {
215
- type: field.nullable
216
- ? getGraphQLType(field.type, field.special)
217
- : new GraphQLNonNull(getGraphQLType(field.type, field.special)),
218
- description: field.note,
219
- };
220
- return acc;
221
- }, {}),
222
- }),
223
- schema: schemaComposer.createObjectTC({
224
- name: 'directus_collections_schema',
225
- fields: {
226
- name: GraphQLString,
227
- comment: GraphQLString,
228
- },
229
- }),
230
- });
193
+ const Collection = getCollectionType(schemaComposer, schema, 'read');
231
194
  schemaComposer.Query.addFields({
232
195
  collections: {
233
196
  type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Collection.getType()))),
@@ -255,45 +218,7 @@ export function injectSystemResolvers(gql, schemaComposer, { CreateCollectionTyp
255
218
  });
256
219
  }
257
220
  if ('directus_fields' in schema.read.collections) {
258
- Field.addFields({
259
- collection: GraphQLString,
260
- field: GraphQLString,
261
- type: GraphQLString,
262
- meta: schemaComposer.createObjectTC({
263
- name: 'directus_fields_meta',
264
- fields: Object.values(schema.read.collections['directus_fields'].fields).reduce((acc, field) => {
265
- acc[field.field] = {
266
- type: field.nullable
267
- ? getGraphQLType(field.type, field.special)
268
- : new GraphQLNonNull(getGraphQLType(field.type, field.special)),
269
- description: field.note,
270
- };
271
- return acc;
272
- }, {}),
273
- }),
274
- schema: schemaComposer.createObjectTC({
275
- name: 'directus_fields_schema',
276
- fields: {
277
- name: GraphQLString,
278
- table: GraphQLString,
279
- data_type: GraphQLString,
280
- default_value: GraphQLString,
281
- max_length: GraphQLInt,
282
- numeric_precision: GraphQLInt,
283
- numeric_scale: GraphQLInt,
284
- is_generated: GraphQLBoolean,
285
- generation_expression: GraphQLString,
286
- is_indexed: GraphQLBoolean,
287
- is_nullable: GraphQLBoolean,
288
- is_unique: GraphQLBoolean,
289
- is_primary_key: GraphQLBoolean,
290
- has_auto_increment: GraphQLBoolean,
291
- foreign_key_column: GraphQLString,
292
- foreign_key_table: GraphQLString,
293
- comment: GraphQLString,
294
- },
295
- }),
296
- });
221
+ const Field = getFieldType(schemaComposer, schema, 'read');
297
222
  schemaComposer.Query.addFields({
298
223
  fields: {
299
224
  type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Field.getType()))),
@@ -335,33 +260,7 @@ export function injectSystemResolvers(gql, schemaComposer, { CreateCollectionTyp
335
260
  });
336
261
  }
337
262
  if ('directus_relations' in schema.read.collections) {
338
- Relation.addFields({
339
- collection: GraphQLString,
340
- field: GraphQLString,
341
- related_collection: GraphQLString,
342
- schema: schemaComposer.createObjectTC({
343
- name: 'directus_relations_schema',
344
- fields: {
345
- table: new GraphQLNonNull(GraphQLString),
346
- column: new GraphQLNonNull(GraphQLString),
347
- foreign_key_table: new GraphQLNonNull(GraphQLString),
348
- foreign_key_column: new GraphQLNonNull(GraphQLString),
349
- constraint_name: GraphQLString,
350
- on_update: new GraphQLNonNull(GraphQLString),
351
- on_delete: new GraphQLNonNull(GraphQLString),
352
- },
353
- }),
354
- meta: schemaComposer.createObjectTC({
355
- name: 'directus_relations_meta',
356
- fields: Object.values(schema.read.collections['directus_relations'].fields).reduce((acc, field) => {
357
- acc[field.field] = {
358
- type: getGraphQLType(field.type, field.special),
359
- description: field.note,
360
- };
361
- return acc;
362
- }, {}),
363
- }),
364
- });
263
+ const Relation = getRelationType(schemaComposer, schema, 'read');
365
264
  schemaComposer.Query.addFields({
366
265
  relations: {
367
266
  type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Relation.getType()))),
@@ -402,7 +301,7 @@ export function injectSystemResolvers(gql, schemaComposer, { CreateCollectionTyp
402
301
  },
403
302
  });
404
303
  }
405
- resolveSystemAdmin(gql, schemaComposer, composers);
304
+ resolveSystemAdmin(gql, schema, schemaComposer);
406
305
  if ('directus_users' in schema.read.collections) {
407
306
  schemaComposer.Query.addFields({
408
307
  users_me: {
@@ -412,7 +311,7 @@ export function injectSystemResolvers(gql, schemaComposer, { CreateCollectionTyp
412
311
  return null;
413
312
  const service = new UsersService({ schema: gql.schema, accountability: gql.accountability });
414
313
  const selections = replaceFragmentsInSelections(info.fieldNodes[0]?.selectionSet?.selections, info.fragments);
415
- const query = getQuery(args, selections || [], info.variableValues, gql.accountability);
314
+ const query = await getQuery(args, selections || [], info.variableValues, gql.schema, gql.accountability);
416
315
  return await service.readOne(gql.accountability.user, query);
417
316
  },
418
317
  },
@@ -450,7 +349,7 @@ export function injectSystemResolvers(gql, schemaComposer, { CreateCollectionTyp
450
349
  schema: gql.schema,
451
350
  });
452
351
  const selections = replaceFragmentsInSelections(info.fieldNodes[0]?.selectionSet?.selections, info.fragments);
453
- const query = getQuery(args, selections || [], info.variableValues, gql.accountability);
352
+ const query = await getQuery(args, selections || [], info.variableValues, gql.schema, gql.accountability);
454
353
  query.limit = -1;
455
354
  const roles = await service.readMany(gql.accountability.roles, query);
456
355
  return roles;
@@ -498,7 +397,7 @@ export function injectSystemResolvers(gql, schemaComposer, { CreateCollectionTyp
498
397
  await service.updateOne(gql.accountability.user, args['data']);
499
398
  if ('directus_users' in ReadCollectionTypes) {
500
399
  const selections = replaceFragmentsInSelections(info.fieldNodes[0]?.selectionSet?.selections, info.fragments);
501
- const query = getQuery(args, selections || [], info.variableValues, gql.accountability);
400
+ const query = await getQuery(args, selections || [], info.variableValues, gql.schema, gql.accountability);
502
401
  return await service.readOne(gql.accountability.user, query);
503
402
  }
504
403
  return true;
@@ -522,7 +421,7 @@ export function injectSystemResolvers(gql, schemaComposer, { CreateCollectionTyp
522
421
  const primaryKey = await service.importOne(args['url'], args['data']);
523
422
  if ('directus_files' in ReadCollectionTypes) {
524
423
  const selections = replaceFragmentsInSelections(info.fieldNodes[0]?.selectionSet?.selections, info.fragments);
525
- const query = getQuery(args, selections || [], info.variableValues, gql.accountability);
424
+ const query = await getQuery(args, selections || [], info.variableValues, gql.schema, gql.accountability);
526
425
  return await service.readOne(primaryKey, query);
527
426
  }
528
427
  return true;
@@ -89,7 +89,7 @@ export async function generateSchema(gql, type = 'schema') {
89
89
  action: 'delete',
90
90
  }, { schema: gql.schema, knex: gql.knex }),
91
91
  };
92
- const { ReadCollectionTypes, VersionCollectionTypes } = getReadableTypes(gql, schemaComposer, schema, inconsistentFields);
92
+ const { ReadCollectionTypes, VersionCollectionTypes } = await getReadableTypes(gql, schemaComposer, schema, inconsistentFields);
93
93
  const { CreateCollectionTypes, UpdateCollectionTypes, DeleteCollectionTypes } = getWritableTypes(gql, schemaComposer, schema, inconsistentFields, ReadCollectionTypes);
94
94
  const CollectionTypes = {
95
95
  CreateCollectionTypes,
@@ -1,7 +1,7 @@
1
- import type { Accountability, Query } from '@directus/types';
1
+ import type { Accountability, Query, SchemaOverview } from '@directus/types';
2
2
  import type { GraphQLResolveInfo, SelectionNode } from 'graphql';
3
3
  /**
4
4
  * Get a Directus Query object from the parsed arguments (rawQuery) and GraphQL AST selectionSet. Converts SelectionSet into
5
5
  * Directus' `fields` query for use in the resolver. Also applies variables where appropriate.
6
6
  */
7
- export declare function getQuery(rawQuery: Query, selections: readonly SelectionNode[], variableValues: GraphQLResolveInfo['variableValues'], accountability?: Accountability | null): Query;
7
+ export declare function getQuery(rawQuery: Query, selections: readonly SelectionNode[], variableValues: GraphQLResolveInfo['variableValues'], schema: SchemaOverview, accountability?: Accountability | null): Promise<Query>;
@@ -7,8 +7,8 @@ import { parseArgs } from './parse-args.js';
7
7
  * Get a Directus Query object from the parsed arguments (rawQuery) and GraphQL AST selectionSet. Converts SelectionSet into
8
8
  * Directus' `fields` query for use in the resolver. Also applies variables where appropriate.
9
9
  */
10
- export function getQuery(rawQuery, selections, variableValues, accountability) {
11
- const query = sanitizeQuery(rawQuery, accountability);
10
+ export async function getQuery(rawQuery, selections, variableValues, schema, accountability) {
11
+ const query = await sanitizeQuery(rawQuery, schema, accountability);
12
12
  const parseAliases = (selections) => {
13
13
  const aliases = {};
14
14
  for (const selection of selections) {
@@ -20,7 +20,7 @@ export function getQuery(rawQuery, selections, variableValues, accountability) {
20
20
  }
21
21
  return aliases;
22
22
  };
23
- const parseFields = (selections, parent) => {
23
+ const parseFields = async (selections, parent) => {
24
24
  const fields = [];
25
25
  for (let selection of selections) {
26
26
  if ((selection.kind === 'Field' || selection.kind === 'InlineFragment') !== true)
@@ -70,7 +70,7 @@ export function getQuery(rawQuery, selections, variableValues, accountability) {
70
70
  }
71
71
  }
72
72
  else {
73
- children = parseFields(selection.selectionSet.selections, currentAlias ?? current);
73
+ children = await parseFields(selection.selectionSet.selections, currentAlias ?? current);
74
74
  }
75
75
  fields.push(...children);
76
76
  }
@@ -82,14 +82,14 @@ export function getQuery(rawQuery, selections, variableValues, accountability) {
82
82
  if (!query.deep)
83
83
  query.deep = {};
84
84
  const args = parseArgs(selection.arguments, variableValues);
85
- set(query.deep, currentAlias ?? current, merge({}, get(query.deep, currentAlias ?? current), mapKeys(sanitizeQuery(args, accountability), (_value, key) => `_${key}`)));
85
+ set(query.deep, currentAlias ?? current, merge({}, get(query.deep, currentAlias ?? current), mapKeys(await sanitizeQuery(args, schema, accountability), (_value, key) => `_${key}`)));
86
86
  }
87
87
  }
88
88
  }
89
89
  return uniq(fields);
90
90
  };
91
91
  query.alias = parseAliases(selections);
92
- query.fields = parseFields(selections);
92
+ query.fields = await parseFields(selections);
93
93
  if (query.filter)
94
94
  query.filter = replaceFuncs(query.filter);
95
95
  query.deep = replaceFuncs(query.deep);
@@ -5,8 +5,8 @@ import { type InconsistentFields, type Schema } from './index.js';
5
5
  /**
6
6
  * Create readable types and attach resolvers for each. Also prepares full filter argument structures
7
7
  */
8
- export declare function getReadableTypes(gql: GraphQLService, schemaComposer: SchemaComposer, schema: Schema, inconsistentFields: InconsistentFields): {
8
+ export declare function getReadableTypes(gql: GraphQLService, schemaComposer: SchemaComposer, schema: Schema, inconsistentFields: InconsistentFields): Promise<{
9
9
  ReadCollectionTypes: Record<string, ObjectTypeComposer<any, any>>;
10
10
  VersionCollectionTypes: Record<string, ObjectTypeComposer<any, any>>;
11
11
  ReadableCollectionFilterTypes: Record<string, InputTypeComposer<any>>;
12
- };
12
+ }>;
@@ -14,12 +14,75 @@ import { getTypes } from './get-types.js';
14
14
  /**
15
15
  * Create readable types and attach resolvers for each. Also prepares full filter argument structures
16
16
  */
17
- export function getReadableTypes(gql, schemaComposer, schema, inconsistentFields) {
17
+ export async function getReadableTypes(gql, schemaComposer, schema, inconsistentFields) {
18
18
  const { CollectionTypes: ReadCollectionTypes, VersionTypes: VersionCollectionTypes } = getTypes(schemaComposer, gql.scope, schema, inconsistentFields, 'read');
19
19
  const ReadableCollectionFilterTypes = {};
20
+ const ReadableCollectionQuantifierFilterTypes = {};
20
21
  const AggregatedFunctions = {};
21
22
  const AggregatedFields = {};
22
23
  const AggregateMethods = {};
24
+ const IDFilterOperators = schemaComposer.createInputTC({
25
+ name: 'id_filter_operators',
26
+ fields: {
27
+ _eq: {
28
+ type: GraphQLID,
29
+ },
30
+ _neq: {
31
+ type: GraphQLID,
32
+ },
33
+ _contains: {
34
+ type: GraphQLID,
35
+ },
36
+ _icontains: {
37
+ type: GraphQLID,
38
+ },
39
+ _ncontains: {
40
+ type: GraphQLID,
41
+ },
42
+ _starts_with: {
43
+ type: GraphQLID,
44
+ },
45
+ _nstarts_with: {
46
+ type: GraphQLID,
47
+ },
48
+ _istarts_with: {
49
+ type: GraphQLID,
50
+ },
51
+ _nistarts_with: {
52
+ type: GraphQLID,
53
+ },
54
+ _ends_with: {
55
+ type: GraphQLID,
56
+ },
57
+ _nends_with: {
58
+ type: GraphQLID,
59
+ },
60
+ _iends_with: {
61
+ type: GraphQLID,
62
+ },
63
+ _niends_with: {
64
+ type: GraphQLID,
65
+ },
66
+ _in: {
67
+ type: new GraphQLList(GraphQLID),
68
+ },
69
+ _nin: {
70
+ type: new GraphQLList(GraphQLID),
71
+ },
72
+ _null: {
73
+ type: GraphQLBoolean,
74
+ },
75
+ _nnull: {
76
+ type: GraphQLBoolean,
77
+ },
78
+ _empty: {
79
+ type: GraphQLBoolean,
80
+ },
81
+ _nempty: {
82
+ type: GraphQLBoolean,
83
+ },
84
+ },
85
+ });
23
86
  const StringFilterOperators = schemaComposer.createInputTC({
24
87
  name: 'string_filter_operators',
25
88
  fields: {
@@ -356,6 +419,9 @@ export function getReadableTypes(gql, schemaComposer, schema, inconsistentFields
356
419
  case GraphQLHash:
357
420
  filterOperatorType = HashFilterOperators;
358
421
  break;
422
+ case GraphQLID:
423
+ filterOperatorType = IDFilterOperators;
424
+ break;
359
425
  default:
360
426
  filterOperatorType = StringFilterOperators;
361
427
  }
@@ -591,10 +657,21 @@ export function getReadableTypes(gql, schemaComposer, schema, inconsistentFields
591
657
  });
592
658
  }
593
659
  }
660
+ for (const collection in ReadableCollectionFilterTypes) {
661
+ const quantifier_collection = ReadableCollectionFilterTypes[collection]?.clone(`${collection}_quantifier_filter`);
662
+ quantifier_collection?.addFields({
663
+ _some: ReadableCollectionFilterTypes[collection],
664
+ _none: ReadableCollectionFilterTypes[collection],
665
+ });
666
+ ReadableCollectionQuantifierFilterTypes[collection] = quantifier_collection;
667
+ }
594
668
  for (const relation of schema.read.relations) {
595
669
  if (relation.related_collection) {
596
670
  if (SYSTEM_DENY_LIST.includes(relation.related_collection))
597
671
  continue;
672
+ ReadableCollectionQuantifierFilterTypes[relation.collection]?.addFields({
673
+ [relation.field]: ReadableCollectionFilterTypes[relation.related_collection],
674
+ });
598
675
  ReadableCollectionFilterTypes[relation.collection]?.addFields({
599
676
  [relation.field]: ReadableCollectionFilterTypes[relation.related_collection],
600
677
  });
@@ -617,8 +694,11 @@ export function getReadableTypes(gql, schemaComposer, schema, inconsistentFields
617
694
  },
618
695
  });
619
696
  if (relation.meta?.one_field) {
697
+ ReadableCollectionQuantifierFilterTypes[relation.related_collection]?.addFields({
698
+ [relation.meta.one_field]: ReadableCollectionQuantifierFilterTypes[relation.collection],
699
+ });
620
700
  ReadableCollectionFilterTypes[relation.related_collection]?.addFields({
621
- [relation.meta.one_field]: ReadableCollectionFilterTypes[relation.collection],
701
+ [relation.meta.one_field]: ReadableCollectionQuantifierFilterTypes[relation.collection],
622
702
  });
623
703
  ReadCollectionTypes[relation.related_collection]?.addFieldArgs(relation.meta.one_field, {
624
704
  filter: ReadableCollectionFilterTypes[relation.collection],
@@ -641,8 +721,12 @@ export function getReadableTypes(gql, schemaComposer, schema, inconsistentFields
641
721
  }
642
722
  }
643
723
  else if (relation.meta?.one_allowed_collections) {
724
+ ReadableCollectionQuantifierFilterTypes[relation.collection]?.removeField('item');
644
725
  ReadableCollectionFilterTypes[relation.collection]?.removeField('item');
645
726
  for (const collection of relation.meta.one_allowed_collections) {
727
+ ReadableCollectionQuantifierFilterTypes[relation.collection]?.addFields({
728
+ [`item__${collection}`]: ReadableCollectionFilterTypes[collection],
729
+ });
646
730
  ReadableCollectionFilterTypes[relation.collection]?.addFields({
647
731
  [`item__${collection}`]: ReadableCollectionFilterTypes[collection],
648
732
  });
@@ -1,3 +1,3 @@
1
1
  import { GraphQLSchema } from 'graphql';
2
- import LRUMapDefault from 'mnemonist/lru-map.js';
3
- export declare const cache: LRUMapDefault.default<string, string | GraphQLSchema>;
2
+ import { LRUMap } from 'mnemonist';
3
+ export declare const cache: LRUMap<string, string | GraphQLSchema>;
@@ -1,9 +1,7 @@
1
1
  import { useEnv } from '@directus/env';
2
2
  import { GraphQLSchema } from 'graphql';
3
- import LRUMapDefault from 'mnemonist/lru-map.js';
3
+ import { LRUMap } from 'mnemonist';
4
4
  import { useBus } from '../../bus/index.js';
5
- // Workaround for misaligned types in mnemonist package exports
6
- const LRUMap = LRUMapDefault;
7
5
  const env = useEnv();
8
6
  const bus = useBus();
9
7
  export const cache = new LRUMap(Number(env['GRAPHQL_SCHEMA_CACHE_CAPACITY'] ?? 100));
@@ -2,19 +2,19 @@ import type { GraphQLService } from './index.js';
2
2
  import type { GraphQLResolveInfo } from 'graphql';
3
3
  export declare function bindPubSub(): void;
4
4
  export declare function createSubscriptionGenerator(gql: GraphQLService, event: string): (_x: unknown, _y: unknown, _z: unknown, request: GraphQLResolveInfo) => AsyncGenerator<{
5
- [x: string]: {
5
+ [event]: {
6
6
  key: string | number;
7
7
  data: null;
8
8
  event: "delete";
9
9
  };
10
10
  } | {
11
- [x: string]: {
11
+ [event]: {
12
12
  key: string | number;
13
13
  data: any;
14
14
  event: "create";
15
15
  };
16
16
  } | {
17
- [x: string]: {
17
+ [event]: {
18
18
  key: string | number;
19
19
  data: any;
20
20
  event: "update";
@@ -12,7 +12,7 @@ export function bindPubSub() {
12
12
  }
13
13
  export function createSubscriptionGenerator(gql, event) {
14
14
  return async function* (_x, _y, _z, request) {
15
- const fields = parseFields(gql, request);
15
+ const fields = await parseFields(gql, request);
16
16
  const args = parseArguments(request);
17
17
  for await (const payload of messages.subscribe(event)) {
18
18
  const eventData = payload;
@@ -79,7 +79,7 @@ function createPubSub(emitter) {
79
79
  },
80
80
  };
81
81
  }
82
- function parseFields(gql, request) {
82
+ async function parseFields(gql, request) {
83
83
  const selections = request.fieldNodes[0]?.selectionSet?.selections ?? [];
84
84
  const dataSelections = selections.reduce((result, selection) => {
85
85
  if (selection.kind === 'Field' &&
@@ -89,7 +89,7 @@ function parseFields(gql, request) {
89
89
  }
90
90
  return result;
91
91
  }, []);
92
- const { fields } = getQuery({}, dataSelections, request.variableValues, gql.accountability);
92
+ const { fields } = await getQuery({}, dataSelections, request.variableValues, gql.schema, gql.accountability);
93
93
  return fields ?? [];
94
94
  }
95
95
  function parseArguments(request) {
@@ -1,6 +1,6 @@
1
- import type { Accountability, Query } from '@directus/types';
1
+ import type { Accountability, Query, SchemaOverview } from '@directus/types';
2
2
  import type { SelectionNode } from 'graphql';
3
3
  /**
4
4
  * Resolve the aggregation query based on the requested aggregated fields
5
5
  */
6
- export declare function getAggregateQuery(rawQuery: Query, selections: readonly SelectionNode[], accountability?: Accountability | null): Query;
6
+ export declare function getAggregateQuery(rawQuery: Query, selections: readonly SelectionNode[], schema: SchemaOverview, accountability?: Accountability | null): Promise<Query>;
@@ -1,11 +1,11 @@
1
- import { replaceFuncs } from './replace-funcs.js';
2
1
  import { sanitizeQuery } from '../../../utils/sanitize-query.js';
3
2
  import { validateQuery } from '../../../utils/validate-query.js';
3
+ import { replaceFuncs } from './replace-funcs.js';
4
4
  /**
5
5
  * Resolve the aggregation query based on the requested aggregated fields
6
6
  */
7
- export function getAggregateQuery(rawQuery, selections, accountability) {
8
- const query = sanitizeQuery(rawQuery, accountability);
7
+ export async function getAggregateQuery(rawQuery, selections, schema, accountability) {
8
+ const query = await sanitizeQuery(rawQuery, schema, accountability);
9
9
  query.aggregate = {};
10
10
  for (let aggregationGroup of selections) {
11
11
  if ((aggregationGroup.kind === 'Field') !== true)
@@ -18,6 +18,7 @@ export declare class ItemsService<Item extends AnyItem = AnyItem, Collection ext
18
18
  eventScope: string;
19
19
  schema: SchemaOverview;
20
20
  cache: Keyv<any> | null;
21
+ nested: string[];
21
22
  constructor(collection: Collection, options: AbstractServiceOptions);
22
23
  /**
23
24
  * Create a fork of the current service, allowing instantiation with different options.
@@ -26,6 +26,7 @@ export class ItemsService {
26
26
  eventScope;
27
27
  schema;
28
28
  cache;
29
+ nested;
29
30
  constructor(collection, options) {
30
31
  this.collection = collection;
31
32
  this.knex = options.knex || getDatabase();
@@ -33,6 +34,7 @@ export class ItemsService {
33
34
  this.eventScope = isSystemCollection(this.collection) ? this.collection.substring(9) : 'items';
34
35
  this.schema = options.schema;
35
36
  this.cache = getCache().cache;
37
+ this.nested = options.nested ?? [];
36
38
  return this;
37
39
  }
38
40
  /**
@@ -43,7 +45,13 @@ export class ItemsService {
43
45
  // ItemsService expects `collection` and `options` as parameters,
44
46
  // while the other services only expect `options`
45
47
  const isItemsService = Service.length === 2;
46
- const newOptions = { knex: this.knex, accountability: this.accountability, schema: this.schema, ...options };
48
+ const newOptions = {
49
+ knex: this.knex,
50
+ accountability: this.accountability,
51
+ schema: this.schema,
52
+ nested: this.nested,
53
+ ...options,
54
+ };
47
55
  if (isItemsService) {
48
56
  return new ItemsService(this.collection, newOptions);
49
57
  }
@@ -95,19 +103,15 @@ export class ItemsService {
95
103
  .filter((field) => field.alias === true)
96
104
  .map((field) => field.field);
97
105
  const payload = cloneDeep(data);
106
+ let actionHookPayload = payload;
98
107
  const nestedActionEvents = [];
99
- // By wrapping the logic in a transaction, we make sure we automatically roll back all the
100
- // changes in the DB if any of the parts contained within throws an error. This also means
101
- // that any errors thrown in any nested relational changes will bubble up and cancel the whole
102
- // update tree
108
+ /**
109
+ * By wrapping the logic in a transaction, we make sure we automatically roll back all the
110
+ * changes in the DB if any of the parts contained within throws an error. This also means
111
+ * that any errors thrown in any nested relational changes will bubble up and cancel the whole
112
+ * update tree
113
+ */
103
114
  const primaryKey = await transaction(this.knex, async (trx) => {
104
- const serviceOptions = {
105
- accountability: this.accountability,
106
- knex: trx,
107
- schema: this.schema,
108
- };
109
- // We're creating new services instances so they can use the transaction as their Knex interface
110
- const payloadService = new PayloadService(this.collection, serviceOptions);
111
115
  // Run all hooks that are attached to this event so the end user has the chance to augment the
112
116
  // item that is about to be saved
113
117
  const payloadAfterHooks = opts.emitEvents !== false
@@ -127,6 +131,7 @@ export class ItemsService {
127
131
  action: 'create',
128
132
  collection: this.collection,
129
133
  payload: payloadAfterHooks,
134
+ nested: this.nested,
130
135
  }, {
131
136
  knex: trx,
132
137
  schema: this.schema,
@@ -135,6 +140,15 @@ export class ItemsService {
135
140
  if (opts.preMutationError) {
136
141
  throw opts.preMutationError;
137
142
  }
143
+ // Ensure the action hook payload has the post filter hook + preset changes
144
+ actionHookPayload = payloadWithPresets;
145
+ // We're creating new services instances so they can use the transaction as their Knex interface
146
+ const payloadService = new PayloadService(this.collection, {
147
+ accountability: this.accountability,
148
+ knex: trx,
149
+ schema: this.schema,
150
+ nested: this.nested,
151
+ });
138
152
  const { payload: payloadWithM2O, revisions: revisionsM2O, nestedActionEvents: nestedActionEventsM2O, userIntegrityCheckFlags: userIntegrityCheckFlagsM2O, } = await payloadService.processM2O(payloadWithPresets, opts);
139
153
  const { payload: payloadWithA2O, revisions: revisionsA2O, nestedActionEvents: nestedActionEventsA2O, userIntegrityCheckFlags: userIntegrityCheckFlagsA2O, } = await payloadService.processA2O(payloadWithM2O, opts);
140
154
  const payloadWithoutAliases = pick(payloadWithA2O, without(fields, ...aliases));
@@ -190,7 +204,7 @@ export class ItemsService {
190
204
  primaryKey = result.id;
191
205
  // Set the primary key on the input item, in order for the "after" event hook to be able
192
206
  // to read from it
193
- payload[primaryKeyField] = primaryKey;
207
+ actionHookPayload[primaryKeyField] = primaryKey;
194
208
  }
195
209
  // At this point, the primary key is guaranteed to be set.
196
210
  primaryKey = primaryKey;
@@ -260,7 +274,7 @@ export class ItemsService {
260
274
  ? ['items.create', `${this.collection}.items.create`]
261
275
  : `${this.eventScope}.create`,
262
276
  meta: {
263
- payload,
277
+ payload: actionHookPayload,
264
278
  key: primaryKey,
265
279
  collection: this.collection,
266
280
  },
@@ -450,8 +464,6 @@ export class ItemsService {
450
464
  * Uses `this.updateMany` under the hood.
451
465
  */
452
466
  async updateOne(key, data, opts) {
453
- const primaryKeyField = this.schema.collections[this.collection].primary;
454
- validateKeys(this.schema, this.collection, primaryKeyField, key);
455
467
  await this.updateMany([key], data, opts);
456
468
  return key;
457
469
  }
@@ -553,6 +565,7 @@ export class ItemsService {
553
565
  action: 'update',
554
566
  collection: this.collection,
555
567
  payload: payloadAfterHooks,
568
+ nested: this.nested,
556
569
  }, {
557
570
  knex: this.knex,
558
571
  schema: this.schema,
@@ -566,6 +579,7 @@ export class ItemsService {
566
579
  accountability: this.accountability,
567
580
  knex: trx,
568
581
  schema: this.schema,
582
+ nested: this.nested,
569
583
  });
570
584
  const { payload: payloadWithM2O, revisions: revisionsM2O, nestedActionEvents: nestedActionEventsM2O, userIntegrityCheckFlags: userIntegrityCheckFlagsM2O, } = await payloadService.processM2O(payloadWithPresets, opts);
571
585
  const { payload: payloadWithA2O, revisions: revisionsA2O, nestedActionEvents: nestedActionEventsA2O, userIntegrityCheckFlags: userIntegrityCheckFlagsA2O, } = await payloadService.processA2O(payloadWithM2O, opts);
@@ -29,6 +29,7 @@ export declare class PayloadService {
29
29
  helpers: Helpers;
30
30
  collection: string;
31
31
  schema: SchemaOverview;
32
+ nested: string[];
32
33
  constructor(collection: string, options: AbstractServiceOptions);
33
34
  transformers: Transformers;
34
35
  processValues(action: Action, payloads: Partial<Item>[]): Promise<Partial<Item>[]>;