@directus/api 31.0.0 → 32.0.0

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 (135) hide show
  1. package/dist/app.js +2 -0
  2. package/dist/auth/auth.d.ts +2 -1
  3. package/dist/auth/auth.js +7 -2
  4. package/dist/auth/drivers/ldap.d.ts +0 -2
  5. package/dist/auth/drivers/ldap.js +9 -7
  6. package/dist/auth/drivers/oauth2.d.ts +0 -2
  7. package/dist/auth/drivers/oauth2.js +11 -8
  8. package/dist/auth/drivers/openid.d.ts +0 -2
  9. package/dist/auth/drivers/openid.js +11 -8
  10. package/dist/auth/drivers/saml.d.ts +0 -2
  11. package/dist/auth/drivers/saml.js +5 -5
  12. package/dist/auth.js +1 -2
  13. package/dist/cli/commands/bootstrap/index.js +12 -33
  14. package/dist/cli/commands/init/index.js +1 -1
  15. package/dist/cli/commands/schema/apply.d.ts +4 -0
  16. package/dist/cli/commands/schema/apply.js +26 -3
  17. package/dist/controllers/collections.js +7 -2
  18. package/dist/controllers/fields.js +31 -8
  19. package/dist/controllers/server.js +26 -1
  20. package/dist/controllers/settings.js +9 -2
  21. package/dist/controllers/users.js +2 -2
  22. package/dist/database/helpers/fn/types.js +3 -3
  23. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +2 -1
  24. package/dist/database/helpers/schema/dialects/cockroachdb.js +13 -0
  25. package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -1
  26. package/dist/database/helpers/schema/dialects/mssql.js +23 -0
  27. package/dist/database/helpers/schema/dialects/mysql.d.ts +2 -1
  28. package/dist/database/helpers/schema/dialects/mysql.js +25 -0
  29. package/dist/database/helpers/schema/dialects/oracle.d.ts +2 -1
  30. package/dist/database/helpers/schema/dialects/oracle.js +13 -0
  31. package/dist/database/helpers/schema/dialects/postgres.d.ts +2 -1
  32. package/dist/database/helpers/schema/dialects/postgres.js +13 -0
  33. package/dist/database/helpers/schema/types.d.ts +5 -0
  34. package/dist/database/helpers/schema/types.js +6 -0
  35. package/dist/database/migrations/20251012A-add-field-searchable.d.ts +3 -0
  36. package/dist/database/migrations/20251012A-add-field-searchable.js +10 -0
  37. package/dist/database/migrations/20251014A-add-project-owner.d.ts +3 -0
  38. package/dist/database/migrations/20251014A-add-project-owner.js +37 -0
  39. package/dist/database/migrations/20251028A-add-retention-indexes.d.ts +3 -0
  40. package/dist/database/migrations/20251028A-add-retention-indexes.js +42 -0
  41. package/dist/database/run-ast/lib/apply-query/add-join.js +2 -2
  42. package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
  43. package/dist/database/run-ast/lib/apply-query/index.d.ts +0 -1
  44. package/dist/database/run-ast/lib/apply-query/index.js +4 -6
  45. package/dist/database/run-ast/lib/apply-query/search.js +2 -0
  46. package/dist/database/run-ast/lib/get-db-query.js +7 -6
  47. package/dist/database/run-ast/utils/generate-alias.d.ts +6 -0
  48. package/dist/database/run-ast/utils/generate-alias.js +57 -0
  49. package/dist/flows.js +1 -0
  50. package/dist/mcp/schema.d.ts +14 -14
  51. package/dist/mcp/schema.js +6 -6
  52. package/dist/mcp/server.d.ts +9 -3
  53. package/dist/mcp/server.js +1 -1
  54. package/dist/mcp/tools/collections.d.ts +1 -1
  55. package/dist/mcp/tools/fields.d.ts +1 -1
  56. package/dist/mcp/tools/files.d.ts +25 -25
  57. package/dist/mcp/tools/flows.d.ts +36 -36
  58. package/dist/mcp/tools/folders.d.ts +18 -18
  59. package/dist/mcp/tools/items.d.ts +18 -18
  60. package/dist/mcp/tools/operations.d.ts +19 -19
  61. package/dist/mcp/tools/prompts/items.md +1 -1
  62. package/dist/metrics/lib/create-metrics.js +16 -25
  63. package/dist/middleware/collection-exists.js +2 -2
  64. package/dist/operations/mail/index.js +3 -1
  65. package/dist/operations/mail/rate-limiter.d.ts +1 -0
  66. package/dist/operations/mail/rate-limiter.js +29 -0
  67. package/dist/permissions/modules/process-payload/process-payload.js +3 -10
  68. package/dist/permissions/modules/validate-access/validate-access.js +2 -3
  69. package/dist/schedules/metrics.js +6 -2
  70. package/dist/schedules/project.d.ts +4 -0
  71. package/dist/schedules/project.js +27 -0
  72. package/dist/services/collections.d.ts +3 -3
  73. package/dist/services/collections.js +16 -1
  74. package/dist/services/fields.d.ts +21 -5
  75. package/dist/services/fields.js +105 -28
  76. package/dist/services/graphql/resolvers/query.js +1 -1
  77. package/dist/services/graphql/resolvers/system-admin.js +49 -5
  78. package/dist/services/graphql/schema/parse-query.js +8 -8
  79. package/dist/services/graphql/utils/aggregate-query.d.ts +1 -1
  80. package/dist/services/graphql/utils/aggregate-query.js +5 -1
  81. package/dist/services/graphql/utils/filter-replace-m2a.js +2 -1
  82. package/dist/services/import-export.d.ts +9 -1
  83. package/dist/services/import-export.js +287 -101
  84. package/dist/services/items.d.ts +1 -1
  85. package/dist/services/items.js +36 -20
  86. package/dist/services/mail/index.js +2 -0
  87. package/dist/services/mail/rate-limiter.d.ts +1 -0
  88. package/dist/services/mail/rate-limiter.js +29 -0
  89. package/dist/services/meta.js +28 -24
  90. package/dist/services/schema.js +4 -1
  91. package/dist/services/server.d.ts +1 -0
  92. package/dist/services/server.js +14 -18
  93. package/dist/services/settings.d.ts +2 -1
  94. package/dist/services/settings.js +15 -0
  95. package/dist/services/tus/server.js +14 -9
  96. package/dist/telemetry/lib/get-report.js +4 -4
  97. package/dist/telemetry/lib/send-report.d.ts +6 -1
  98. package/dist/telemetry/lib/send-report.js +3 -1
  99. package/dist/telemetry/types/report.d.ts +17 -1
  100. package/dist/telemetry/utils/get-settings.d.ts +9 -0
  101. package/dist/telemetry/utils/get-settings.js +14 -0
  102. package/dist/test-utils/README.md +760 -0
  103. package/dist/test-utils/cache.d.ts +51 -0
  104. package/dist/test-utils/cache.js +59 -0
  105. package/dist/test-utils/database.d.ts +48 -0
  106. package/dist/test-utils/database.js +52 -0
  107. package/dist/test-utils/emitter.d.ts +35 -0
  108. package/dist/test-utils/emitter.js +38 -0
  109. package/dist/test-utils/fields-service.d.ts +28 -0
  110. package/dist/test-utils/fields-service.js +36 -0
  111. package/dist/test-utils/items-service.d.ts +23 -0
  112. package/dist/test-utils/items-service.js +37 -0
  113. package/dist/test-utils/knex.d.ts +164 -0
  114. package/dist/test-utils/knex.js +268 -0
  115. package/dist/test-utils/schema.d.ts +26 -0
  116. package/dist/test-utils/schema.js +35 -0
  117. package/dist/types/auth.d.ts +0 -2
  118. package/dist/utils/apply-diff.js +15 -0
  119. package/dist/utils/create-admin.d.ts +11 -0
  120. package/dist/utils/create-admin.js +50 -0
  121. package/dist/utils/get-schema.js +5 -3
  122. package/dist/utils/get-snapshot-diff.js +49 -5
  123. package/dist/utils/get-snapshot.js +13 -7
  124. package/dist/utils/sanitize-schema.d.ts +11 -4
  125. package/dist/utils/sanitize-schema.js +9 -6
  126. package/dist/utils/schedule.js +15 -19
  127. package/dist/utils/validate-diff.js +31 -0
  128. package/dist/utils/validate-snapshot.js +7 -0
  129. package/dist/websocket/controllers/hooks.js +12 -20
  130. package/dist/websocket/messages.d.ts +3 -3
  131. package/package.json +63 -65
  132. package/dist/cli/utils/defaults.d.ts +0 -4
  133. package/dist/cli/utils/defaults.js +0 -17
  134. package/dist/telemetry/utils/get-project-id.d.ts +0 -2
  135. package/dist/telemetry/utils/get-project-id.js +0 -4
@@ -2,8 +2,10 @@ import { DEFAULT_NUMERIC_PRECISION, DEFAULT_NUMERIC_SCALE, KNEX_TYPES, REGEX_BET
2
2
  import { useEnv } from '@directus/env';
3
3
  import { ForbiddenError, InvalidPayloadError } from '@directus/errors';
4
4
  import { createInspector } from '@directus/schema';
5
+ import { isSystemField } from '@directus/system-data';
5
6
  import { addFieldFlag, getRelations, toArray } from '@directus/utils';
6
7
  import { isEqual, isNil, merge } from 'lodash-es';
8
+ import { z } from 'zod';
7
9
  import { clearSystemCache, getCache, getCacheValue, setCacheValue } from '../cache.js';
8
10
  import { ALIAS_TYPES, ALLOWED_DB_DEFAULT_FUNCTIONS } from '../constants.js';
9
11
  import { translateDatabaseError } from '../database/errors/translate.js';
@@ -28,6 +30,17 @@ import { PayloadService } from './payload.js';
28
30
  import { RelationsService } from './relations.js';
29
31
  const systemFieldRows = getSystemFieldRowsWithAuthProviders();
30
32
  const env = useEnv();
33
+ export const systemFieldUpdateSchema = z
34
+ .object({
35
+ collection: z.string().optional(),
36
+ field: z.string().optional(),
37
+ schema: z
38
+ .object({
39
+ is_indexed: z.boolean().optional(),
40
+ })
41
+ .strict(),
42
+ })
43
+ .strict();
31
44
  export class FieldsService {
32
45
  knex;
33
46
  helpers;
@@ -89,14 +102,14 @@ export class FieldsService {
89
102
  schema: this.schema,
90
103
  });
91
104
  if (collection) {
92
- fields = (await nonAuthorizedItemsService.readByQuery({
105
+ fields = await nonAuthorizedItemsService.readByQuery({
93
106
  filter: { collection: { _eq: collection } },
94
107
  limit: -1,
95
- }));
108
+ });
96
109
  fields.push(...systemFieldRows.filter((fieldMeta) => fieldMeta.collection === collection));
97
110
  }
98
111
  else {
99
- fields = (await nonAuthorizedItemsService.readByQuery({ limit: -1 }));
112
+ fields = await nonAuthorizedItemsService.readByQuery({ limit: -1 });
100
113
  fields.push(...systemFieldRows);
101
114
  }
102
115
  const columns = (await this.columnInfo(collection)).map((column) => ({
@@ -269,28 +282,35 @@ export class FieldsService {
269
282
  if (flagToAdd) {
270
283
  addFieldFlag(field, flagToAdd);
271
284
  }
285
+ let hookAdjustedField = field;
286
+ const attemptConcurrentIndex = Boolean(opts?.attemptConcurrentIndex);
272
287
  await transaction(this.knex, async (trx) => {
273
288
  const itemsService = new ItemsService('directus_fields', {
274
289
  knex: trx,
275
290
  accountability: this.accountability,
276
291
  schema: this.schema,
277
292
  });
278
- const hookAdjustedField = opts?.emitEvents !== false
279
- ? await emitter.emitFilter(`fields.create`, field, {
280
- collection: collection,
281
- }, {
282
- database: trx,
283
- schema: this.schema,
284
- accountability: this.accountability,
285
- })
286
- : field;
293
+ hookAdjustedField =
294
+ opts?.emitEvents !== false
295
+ ? await emitter.emitFilter(`fields.create`, field, {
296
+ collection: collection,
297
+ }, {
298
+ database: trx,
299
+ schema: this.schema,
300
+ accountability: this.accountability,
301
+ })
302
+ : field;
287
303
  if (hookAdjustedField.type && ALIAS_TYPES.includes(hookAdjustedField.type) === false) {
288
304
  if (table) {
289
- this.addColumnToTable(table, collection, hookAdjustedField);
305
+ this.addColumnToTable(table, collection, hookAdjustedField, {
306
+ attemptConcurrentIndex,
307
+ });
290
308
  }
291
309
  else {
292
310
  await trx.schema.alterTable(collection, (table) => {
293
- this.addColumnToTable(table, collection, hookAdjustedField);
311
+ this.addColumnToTable(table, collection, hookAdjustedField, {
312
+ attemptConcurrentIndex,
313
+ });
294
314
  });
295
315
  }
296
316
  }
@@ -327,6 +347,12 @@ export class FieldsService {
327
347
  nestedActionEvents.push(actionEvent);
328
348
  }
329
349
  });
350
+ // concurrent index creation cannot be done inside the transaction
351
+ if (attemptConcurrentIndex && hookAdjustedField.type && ALIAS_TYPES.includes(hookAdjustedField.type) === false) {
352
+ await this.addColumnIndex(collection, hookAdjustedField, {
353
+ attemptConcurrentIndex,
354
+ });
355
+ }
330
356
  }
331
357
  finally {
332
358
  if (runPostColumnChange) {
@@ -390,20 +416,32 @@ export class FieldsService {
390
416
  const columnToCompare = opts?.bypassLimits && opts.autoPurgeSystemCache === false ? sanitizeColumn(existingColumn) : existingColumn;
391
417
  if (!isEqual(columnToCompare, hookAdjustedField.schema)) {
392
418
  try {
419
+ const attemptConcurrentIndex = Boolean(opts?.attemptConcurrentIndex);
393
420
  await transaction(this.knex, async (trx) => {
394
- await trx.schema.alterTable(collection, async (table) => {
421
+ await trx.schema.alterTable(collection, (table) => {
395
422
  if (!hookAdjustedField.schema)
396
423
  return;
397
- this.addColumnToTable(table, collection, field, existingColumn);
424
+ this.addColumnToTable(table, collection, field, {
425
+ existing: existingColumn,
426
+ attemptConcurrentIndex,
427
+ });
398
428
  });
399
429
  });
430
+ // concurrent index creation cannot be done inside the transaction
431
+ if (attemptConcurrentIndex) {
432
+ await this.addColumnIndex(collection, field, {
433
+ existing: existingColumn,
434
+ attemptConcurrentIndex,
435
+ });
436
+ }
400
437
  }
401
438
  catch (err) {
402
439
  throw await translateDatabaseError(err, field);
403
440
  }
404
441
  }
405
442
  }
406
- if (hookAdjustedField.meta) {
443
+ // Only create/update a database record if this is not a system field
444
+ if (hookAdjustedField.meta && !isSystemField(collection, hookAdjustedField.field)) {
407
445
  if (record) {
408
446
  await this.itemsService.updateOne(record.id, {
409
447
  ...hookAdjustedField.meta,
@@ -463,11 +501,13 @@ export class FieldsService {
463
501
  const nestedActionEvents = [];
464
502
  try {
465
503
  const fieldNames = [];
504
+ const attemptConcurrentIndex = Boolean(opts?.attemptConcurrentIndex);
466
505
  for (const field of fields) {
467
506
  fieldNames.push(await this.updateField(collection, field, {
468
507
  autoPurgeCache: false,
469
508
  autoPurgeSystemCache: false,
470
509
  bypassEmitAction: (params) => nestedActionEvents.push(params),
510
+ attemptConcurrentIndex,
471
511
  }));
472
512
  }
473
513
  return fieldNames;
@@ -590,6 +630,22 @@ export class FieldsService {
590
630
  field: { _eq: field },
591
631
  },
592
632
  }, { emitEvents: false });
633
+ // cleanup permissions for deleted field
634
+ const permissionRows = await trx
635
+ .select('id', 'collection', 'fields')
636
+ .from('directus_permissions')
637
+ .whereRaw('?? = ? AND ?? LIKE ?', ['collection', collection, 'fields', '%' + field + '%']);
638
+ if (permissionRows.length > 0) {
639
+ for (const permissionRow of permissionRows) {
640
+ const newFields = permissionRow['fields']
641
+ .split(',')
642
+ .filter((v) => v !== field)
643
+ .join(',');
644
+ await trx('directus_permissions')
645
+ .update('fields', newFields.length > 0 ? newFields : null)
646
+ .where('id', '=', permissionRow['id']);
647
+ }
648
+ }
593
649
  });
594
650
  const actionEvent = {
595
651
  event: 'fields.delete',
@@ -629,11 +685,12 @@ export class FieldsService {
629
685
  }
630
686
  }
631
687
  }
632
- addColumnToTable(table, collection, field, existing = null) {
688
+ addColumnToTable(table, collection, field, options) {
633
689
  let column;
634
690
  // Don't attempt to add a DB column for alias / corrupt fields
635
691
  if (field.type === 'alias' || field.type === 'unknown')
636
692
  return;
693
+ const existing = options?.existing ?? null;
637
694
  if (field.schema?.has_auto_increment) {
638
695
  if (field.type === 'bigInteger') {
639
696
  column = table.bigIncrements(field.field);
@@ -707,28 +764,48 @@ export class FieldsService {
707
764
  else if (!existing?.is_primary_key) {
708
765
  // primary key will already have unique/index constraints
709
766
  if (field.schema?.is_unique === true) {
710
- if (!existing || existing.is_unique === false) {
767
+ if ((!existing || existing.is_unique === false) && !options?.attemptConcurrentIndex) {
711
768
  column.unique({ indexName: this.helpers.schema.generateIndexName('unique', collection, field.field) });
712
769
  }
713
770
  }
714
- else if (field.schema?.is_unique === false) {
715
- if (existing?.is_unique === true) {
716
- table.dropUnique([field.field], this.helpers.schema.generateIndexName('unique', collection, field.field));
717
- }
771
+ else if (field.schema?.is_unique === false && existing?.is_unique === true) {
772
+ table.dropUnique([field.field], this.helpers.schema.generateIndexName('unique', collection, field.field));
718
773
  }
719
774
  if (field.schema?.is_indexed === true) {
720
- if (!existing || existing.is_indexed === false) {
775
+ if ((!existing || existing.is_indexed === false) && !options?.attemptConcurrentIndex) {
721
776
  column.index(this.helpers.schema.generateIndexName('index', collection, field.field));
722
777
  }
723
778
  }
724
- else if (field.schema?.is_indexed === false) {
725
- if (existing?.is_indexed === true) {
726
- table.dropIndex([field.field], this.helpers.schema.generateIndexName('index', collection, field.field));
727
- }
779
+ else if (field.schema?.is_indexed === false && existing?.is_indexed === true) {
780
+ table.dropIndex([field.field], this.helpers.schema.generateIndexName('index', collection, field.field));
728
781
  }
729
782
  }
730
783
  if (existing) {
731
784
  column.alter();
732
785
  }
733
786
  }
787
+ async addColumnIndex(collection, field, options) {
788
+ const attemptConcurrentIndex = Boolean(options?.attemptConcurrentIndex);
789
+ const knex = options?.knex ?? this.knex;
790
+ const existing = options?.existing ?? null;
791
+ // Don't attempt to index a DB column for alias / corrupt fields
792
+ if (field.type === 'alias' || field.type === 'unknown')
793
+ return;
794
+ // primary key will already have unique/index constraints
795
+ if (field.schema?.is_primary_key || existing?.is_primary_key)
796
+ return;
797
+ const helpers = getHelpers(knex);
798
+ if (field.schema?.is_unique === true && (!existing || existing.is_unique == false)) {
799
+ await helpers.schema.createIndex(collection, field.field, {
800
+ unique: true,
801
+ attemptConcurrentIndex,
802
+ });
803
+ }
804
+ if (field.schema?.is_indexed === true && (!existing || existing.is_indexed === false)) {
805
+ await helpers.schema.createIndex(collection, field.field, {
806
+ unique: false,
807
+ attemptConcurrentIndex,
808
+ });
809
+ }
810
+ }
734
811
  }
@@ -19,8 +19,8 @@ export async function resolveQuery(gql, info) {
19
19
  let query;
20
20
  const isAggregate = collection.endsWith('_aggregated') && collection in gql.schema.collections === false;
21
21
  if (isAggregate) {
22
- query = await getAggregateQuery(args, selections, gql.schema, gql.accountability);
23
22
  collection = collection.slice(0, -11);
23
+ query = await getAggregateQuery(args, selections, gql.schema, gql.accountability, collection);
24
24
  }
25
25
  else {
26
26
  query = await getQuery(args, gql.schema, selections, info.variableValues, gql.accountability, collection);
@@ -1,13 +1,16 @@
1
+ import { InvalidPayloadError } from '@directus/errors';
2
+ import { isSystemField } from '@directus/system-data';
1
3
  import { GraphQLBoolean, GraphQLID, GraphQLList, GraphQLNonNull, GraphQLString } from 'graphql';
2
4
  import { SchemaComposer, toInputObjectType } from 'graphql-compose';
5
+ import { fromZodError } from 'zod-validation-error';
3
6
  import { CollectionsService } from '../../collections.js';
4
7
  import { ExtensionsService } from '../../extensions.js';
5
- import { FieldsService } from '../../fields.js';
8
+ import { FieldsService, systemFieldUpdateSchema } from '../../fields.js';
6
9
  import { RelationsService } from '../../relations.js';
7
10
  import { GraphQLService } from '../index.js';
11
+ import { getCollectionType } from './get-collection-type.js';
8
12
  import { getFieldType } from './get-field-type.js';
9
13
  import { getRelationType } from './get-relation-type.js';
10
- import { getCollectionType } from './get-collection-type.js';
11
14
  export function resolveSystemAdmin(gql, schema, schemaComposer) {
12
15
  if (!gql.accountability?.admin) {
13
16
  return;
@@ -24,13 +27,16 @@ export function resolveSystemAdmin(gql, schema, schemaComposer) {
24
27
  }).addFields({
25
28
  fields: [toInputObjectType(Field, { postfix: '_input' }).NonNull],
26
29
  }).NonNull,
30
+ concurrentIndexCreation: { type: GraphQLBoolean, defaultValue: false },
27
31
  },
28
32
  resolve: async (_, args) => {
29
33
  const collectionsService = new CollectionsService({
30
34
  accountability: gql.accountability,
31
35
  schema: gql.schema,
32
36
  });
33
- const collectionKey = await collectionsService.createOne(args['data']);
37
+ const collectionKey = await collectionsService.createOne(args['data'], {
38
+ attemptConcurrentIndex: Boolean(args['concurrentIndexCreation']),
39
+ });
34
40
  return await collectionsService.readOne(collectionKey);
35
41
  },
36
42
  },
@@ -77,13 +83,16 @@ export function resolveSystemAdmin(gql, schema, schemaComposer) {
77
83
  args: {
78
84
  collection: new GraphQLNonNull(GraphQLString),
79
85
  data: toInputObjectType(Field, { postfix: '_input' }).NonNull,
86
+ concurrentIndexCreation: { type: GraphQLBoolean, defaultValue: false },
80
87
  },
81
88
  resolve: async (_, args) => {
82
89
  const service = new FieldsService({
83
90
  accountability: gql.accountability,
84
91
  schema: gql.schema,
85
92
  });
86
- await service.createField(args['collection'], args['data']);
93
+ await service.createField(args['collection'], args['data'], undefined, {
94
+ attemptConcurrentIndex: Boolean(args['concurrentIndexCreation']),
95
+ });
87
96
  return await service.readOne(args['collection'], args['data'].field);
88
97
  },
89
98
  },
@@ -93,17 +102,52 @@ export function resolveSystemAdmin(gql, schema, schemaComposer) {
93
102
  collection: new GraphQLNonNull(GraphQLString),
94
103
  field: new GraphQLNonNull(GraphQLString),
95
104
  data: toInputObjectType(Field, { postfix: '_input' }).NonNull,
105
+ concurrentIndexCreation: { type: GraphQLBoolean, defaultValue: false },
96
106
  },
97
107
  resolve: async (_, args) => {
98
108
  const service = new FieldsService({
99
109
  accountability: gql.accountability,
100
110
  schema: gql.schema,
101
111
  });
112
+ if (isSystemField(args['collection'], args['field'])) {
113
+ const validationResult = systemFieldUpdateSchema.safeParse(args['data']);
114
+ if (!validationResult.success) {
115
+ throw new InvalidPayloadError({ reason: fromZodError(validationResult.error).message });
116
+ }
117
+ }
102
118
  await service.updateField(args['collection'], {
103
119
  ...args['data'],
104
120
  field: args['field'],
121
+ }, {
122
+ attemptConcurrentIndex: Boolean(args['concurrentIndexCreation']),
105
123
  });
106
- return await service.readOne(args['collection'], args['data'].field);
124
+ return await service.readOne(args['collection'], args['field']);
125
+ },
126
+ },
127
+ update_fields_items: {
128
+ type: Field,
129
+ args: {
130
+ collection: new GraphQLNonNull(GraphQLString),
131
+ data: [toInputObjectType(Field, { postfix: '_input' }).NonNull],
132
+ concurrentIndexCreation: { type: GraphQLBoolean, defaultValue: false },
133
+ },
134
+ resolve: async (_, args) => {
135
+ const service = new FieldsService({
136
+ accountability: gql.accountability,
137
+ schema: gql.schema,
138
+ });
139
+ for (const fieldData of args['data']) {
140
+ if (isSystemField(args['collection'], fieldData['field'])) {
141
+ const validationResult = systemFieldUpdateSchema.safeParse(fieldData);
142
+ if (!validationResult.success) {
143
+ throw new InvalidPayloadError({ reason: fromZodError(validationResult.error).message });
144
+ }
145
+ }
146
+ }
147
+ await service.updateFields(args['collection'], args['data'], {
148
+ attemptConcurrentIndex: Boolean(args['concurrentIndexCreation']),
149
+ });
150
+ return await service.readOne(args['collection'], args['field']);
107
151
  },
108
152
  },
109
153
  delete_fields_item: {
@@ -1,9 +1,9 @@
1
1
  import { get, mapKeys, merge, set, uniq } from 'lodash-es';
2
2
  import { sanitizeQuery } from '../../../utils/sanitize-query.js';
3
3
  import { validateQuery } from '../../../utils/validate-query.js';
4
+ import { filterReplaceM2A, filterReplaceM2ADeep } from '../utils/filter-replace-m2a.js';
4
5
  import { replaceFuncs } from '../utils/replace-funcs.js';
5
6
  import { parseArgs } from './parse-args.js';
6
- import { filterReplaceM2A, filterReplaceM2ADeep } from '../utils/filter-replace-m2a.js';
7
7
  /**
8
8
  * Get a Directus Query object from the parsed arguments (rawQuery) and GraphQL AST selectionSet. Converts SelectionSet into
9
9
  * Directus' `fields` query for use in the resolver. Also applies variables where appropriate.
@@ -52,7 +52,8 @@ export async function getQuery(rawQuery, schema, selections, variableValues, acc
52
52
  if (selection.selectionSet) {
53
53
  if (!query.deep)
54
54
  query.deep = {};
55
- set(query.deep, parent, merge({}, get(query.deep, parent), { _alias: { [selection.alias.value]: selection.name.value } }));
55
+ const path = parent.replaceAll(':', '__');
56
+ set(query.deep, path, merge({}, get(query.deep, parent), { _alias: { [selection.alias.value]: selection.name.value } }));
56
57
  }
57
58
  }
58
59
  }
@@ -79,12 +80,11 @@ export async function getQuery(rawQuery, schema, selections, variableValues, acc
79
80
  fields.push(current);
80
81
  }
81
82
  if (selection.kind === 'Field' && selection.arguments && selection.arguments.length > 0) {
82
- if (selection.arguments && selection.arguments.length > 0) {
83
- if (!query.deep)
84
- query.deep = {};
85
- const args = parseArgs(selection.arguments, variableValues);
86
- set(query.deep, currentAlias ?? current, merge({}, get(query.deep, currentAlias ?? current), mapKeys(await sanitizeQuery(args, schema, accountability), (_value, key) => `_${key}`)));
87
- }
83
+ if (!query.deep)
84
+ query.deep = {};
85
+ const args = parseArgs(selection.arguments, variableValues);
86
+ const path = (currentAlias ?? current).replaceAll(':', '__');
87
+ set(query.deep, path, merge({}, get(query.deep, path), mapKeys(await sanitizeQuery(args, schema, accountability), (_value, key) => `_${key}`)));
88
88
  }
89
89
  }
90
90
  return uniq(fields);
@@ -3,4 +3,4 @@ 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[], schema: SchemaOverview, accountability?: Accountability | null): Promise<Query>;
6
+ export declare function getAggregateQuery(rawQuery: Query, selections: readonly SelectionNode[], schema: SchemaOverview, accountability?: Accountability | null, collection?: string): Promise<Query>;
@@ -1,10 +1,11 @@
1
1
  import { sanitizeQuery } from '../../../utils/sanitize-query.js';
2
2
  import { validateQuery } from '../../../utils/validate-query.js';
3
+ import { filterReplaceM2A } from './filter-replace-m2a.js';
3
4
  import { replaceFuncs } from './replace-funcs.js';
4
5
  /**
5
6
  * Resolve the aggregation query based on the requested aggregated fields
6
7
  */
7
- export async function getAggregateQuery(rawQuery, selections, schema, accountability) {
8
+ export async function getAggregateQuery(rawQuery, selections, schema, accountability, collection) {
8
9
  const query = await sanitizeQuery(rawQuery, schema, accountability);
9
10
  query.aggregate = {};
10
11
  for (let aggregationGroup of selections) {
@@ -27,6 +28,9 @@ export async function getAggregateQuery(rawQuery, selections, schema, accountabi
27
28
  if (query.filter) {
28
29
  query.filter = replaceFuncs(query.filter);
29
30
  }
31
+ if (collection && query.filter) {
32
+ query.filter = filterReplaceM2A(query.filter, collection, schema);
33
+ }
30
34
  validateQuery(query);
31
35
  return query;
32
36
  }
@@ -48,7 +48,8 @@ export function filterReplaceM2ADeep(deep_arg, collection, schema) {
48
48
  deep[key] = filterReplaceM2ADeep(deep[key], relation.related_collection, schema);
49
49
  }
50
50
  else if (type === 'a2o' && any_collection && relation.meta?.one_allowed_collections?.includes(any_collection)) {
51
- deep[key] = filterReplaceM2ADeep(deep[key], any_collection, schema);
51
+ deep[`${field}:${any_collection}`] = filterReplaceM2ADeep(deep[key], any_collection, schema);
52
+ delete deep[key];
52
53
  }
53
54
  }
54
55
  if (key === '_filter') {
@@ -1,7 +1,15 @@
1
- import type { AbstractServiceOptions, Accountability, ExportFormat, File, Query, SchemaOverview } from '@directus/types';
1
+ import type { AbstractServiceOptions, Accountability, DirectusError, ExportFormat, File, Query, SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
3
  import type { Readable } from 'node:stream';
4
4
  import type { FunctionFieldNode, FieldNode, NestedCollectionNode } from '../types/index.js';
5
+ export declare function createErrorTracker(): {
6
+ addCapturedError: (err: any, rowNumber: number) => void;
7
+ buildFinalErrors: () => DirectusError<any>[];
8
+ getCount: () => number;
9
+ hasErrors: () => boolean;
10
+ shouldStop: () => boolean;
11
+ hasGenericError: () => boolean;
12
+ };
5
13
  export declare class ImportService {
6
14
  knex: Knex;
7
15
  accountability: Accountability | null;