@directus/api 29.1.1 → 30.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.
@@ -6,7 +6,7 @@ export declare const FILTER_VARIABLES: string[];
6
6
  export declare const ALIAS_TYPES: string[];
7
7
  export declare const DEFAULT_AUTH_PROVIDER = "default";
8
8
  export declare const COLUMN_TRANSFORMS: string[];
9
- export declare const GENERATE_SPECIAL: readonly ["uuid", "date-created", "role-created", "user-created"];
9
+ export declare const GENERATE_SPECIAL: readonly ["uuid", "date-created", "date-updated", "role-created", "role-updated", "user-created", "user-updated"];
10
10
  export declare const UUID_REGEX = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
11
11
  export declare const REFRESH_COOKIE_OPTIONS: CookieOptions;
12
12
  export declare const SESSION_COOKIE_OPTIONS: CookieOptions;
package/dist/constants.js CHANGED
@@ -51,7 +51,15 @@ export const FILTER_VARIABLES = ['$NOW', '$CURRENT_USER', '$CURRENT_ROLE'];
51
51
  export const ALIAS_TYPES = ['alias', 'o2m', 'm2m', 'm2a', 'o2a', 'files', 'translations'];
52
52
  export const DEFAULT_AUTH_PROVIDER = 'default';
53
53
  export const COLUMN_TRANSFORMS = ['year', 'month', 'day', 'weekday', 'hour', 'minute', 'second'];
54
- export const GENERATE_SPECIAL = ['uuid', 'date-created', 'role-created', 'user-created'];
54
+ export const GENERATE_SPECIAL = [
55
+ 'uuid',
56
+ 'date-created',
57
+ 'date-updated',
58
+ 'role-created',
59
+ 'role-updated',
60
+ 'user-created',
61
+ 'user-updated',
62
+ ];
55
63
  export const UUID_REGEX = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}';
56
64
  export const REFRESH_COOKIE_OPTIONS = {
57
65
  httpOnly: true,
@@ -2,7 +2,6 @@ import { ErrorCode, ForbiddenError, RouteNotFoundError, isDirectusError } from '
2
2
  import { isSystemCollection } from '@directus/system-data';
3
3
  import express from 'express';
4
4
  import collectionExists from '../middleware/collection-exists.js';
5
- import { mergeContentVersions } from '../middleware/merge-content-versions.js';
6
5
  import { respond } from '../middleware/respond.js';
7
6
  import { validateBatch } from '../middleware/validate-batch.js';
8
7
  import { ItemsService } from '../services/items.js';
@@ -75,8 +74,8 @@ const readHandler = asyncHandler(async (req, res, next) => {
75
74
  };
76
75
  return next();
77
76
  });
78
- router.search('/:collection', collectionExists, validateBatch('read'), readHandler, mergeContentVersions, respond);
79
- router.get('/:collection', collectionExists, readHandler, mergeContentVersions, respond);
77
+ router.search('/:collection', collectionExists, validateBatch('read'), readHandler, respond);
78
+ router.get('/:collection', collectionExists, readHandler, respond);
80
79
  router.get('/:collection/:pk', collectionExists, asyncHandler(async (req, res, next) => {
81
80
  if (isSystemCollection(req.params['collection']))
82
81
  throw new ForbiddenError();
@@ -89,7 +88,7 @@ router.get('/:collection/:pk', collectionExists, asyncHandler(async (req, res, n
89
88
  data: result || null,
90
89
  };
91
90
  return next();
92
- }), mergeContentVersions, respond);
91
+ }), respond);
93
92
  router.patch('/:collection', collectionExists, validateBatch('update'), asyncHandler(async (req, res, next) => {
94
93
  if (isSystemCollection(req.params['collection']))
95
94
  throw new ForbiddenError();
package/dist/mailer.js CHANGED
@@ -19,11 +19,11 @@ export default function getMailer() {
19
19
  });
20
20
  }
21
21
  else if (transportName === 'ses') {
22
- const aws = require('@aws-sdk/client-ses');
22
+ const { SESv2Client, SendEmailCommand } = require('@aws-sdk/client-sesv2');
23
23
  const sesOptions = getConfigFromEnv('EMAIL_SES_');
24
- const ses = new aws.SES(sesOptions);
24
+ const sesClient = new SESv2Client(sesOptions);
25
25
  transporter = nodemailer.createTransport({
26
- SES: { ses, aws },
26
+ SES: { sesClient, SendEmailCommand },
27
27
  });
28
28
  }
29
29
  else if (transportName === 'smtp') {
@@ -10,7 +10,7 @@ import { getCacheKey } from '../utils/get-cache-key.js';
10
10
  import { getDateFormatted } from '../utils/get-date-formatted.js';
11
11
  import { getMilliseconds } from '../utils/get-milliseconds.js';
12
12
  import { stringByteSize } from '../utils/get-string-byte-size.js';
13
- import { permissionsCachable } from '../utils/permissions-cachable.js';
13
+ import { permissionsCacheable } from '../utils/permissions-cacheable.js';
14
14
  export const respond = asyncHandler(async (req, res) => {
15
15
  const env = useEnv();
16
16
  const logger = useLogger();
@@ -29,7 +29,7 @@ export const respond = asyncHandler(async (req, res) => {
29
29
  !req.sanitizedQuery.export &&
30
30
  res.locals['cache'] !== false &&
31
31
  exceedsMaxSize === false &&
32
- (await permissionsCachable(req.collection, {
32
+ (await permissionsCacheable(req.collection, {
33
33
  knex: getDatabase(),
34
34
  schema: req.schema,
35
35
  }, req.accountability))) {
@@ -1,4 +1,4 @@
1
- import type { AbstractServiceOptions, Accountability, GraphQLParams, GQLScope, Item, Query, SchemaOverview } from '@directus/types';
1
+ import type { AbstractServiceOptions, Accountability, GraphQLParams, GQLScope, Item, Query, SchemaOverview, PrimaryKey } from '@directus/types';
2
2
  import type { FormattedExecutionResult, GraphQLSchema } from 'graphql';
3
3
  import type { Knex } from 'knex';
4
4
  export declare class GraphQLService {
@@ -22,7 +22,7 @@ export declare class GraphQLService {
22
22
  /**
23
23
  * Execute the read action on the correct service. Checks for singleton as well.
24
24
  */
25
- read(collection: string, query: Query): Promise<Partial<Item>>;
25
+ read(collection: string, query: Query, id?: PrimaryKey): Promise<Partial<Item>>;
26
26
  /**
27
27
  * Upsert and read singleton item
28
28
  */
@@ -61,16 +61,17 @@ export class GraphQLService {
61
61
  /**
62
62
  * Execute the read action on the correct service. Checks for singleton as well.
63
63
  */
64
- async read(collection, query) {
64
+ async read(collection, query, id) {
65
65
  const service = getService(collection, {
66
66
  knex: this.knex,
67
67
  accountability: this.accountability,
68
68
  schema: this.schema,
69
69
  });
70
- const result = this.schema.collections[collection].singleton
71
- ? await service.readSingleton(query, { stripNonRequested: false })
72
- : await service.readByQuery(query, { stripNonRequested: false });
73
- return result;
70
+ if (this.schema.collections[collection].singleton)
71
+ return await service.readSingleton(query, { stripNonRequested: false });
72
+ if (id)
73
+ return await service.readOne(id, query, { stripNonRequested: false });
74
+ return await service.readByQuery(query, { stripNonRequested: false });
74
75
  }
75
76
  /**
76
77
  * Upsert and read singleton item
@@ -1,7 +1,5 @@
1
1
  import { parseFilterFunctionPath } from '@directus/utils';
2
2
  import { omit } from 'lodash-es';
3
- import { mergeVersionsRaw, mergeVersionsRecursive } from '../../../utils/merge-version-data.js';
4
- import { VersionsService } from '../../versions.js';
5
3
  import { parseArgs } from '../schema/parse-args.js';
6
4
  import { getQuery } from '../schema/parse-query.js';
7
5
  import { getAggregateQuery } from '../utils/aggregate-query.js';
@@ -19,7 +17,6 @@ export async function resolveQuery(gql, info) {
19
17
  return null;
20
18
  const args = parseArgs(info.fieldNodes[0].arguments || [], info.variableValues);
21
19
  let query;
22
- let versionRaw = false;
23
20
  const isAggregate = collection.endsWith('_aggregated') && collection in gql.schema.collections === false;
24
21
  if (isAggregate) {
25
22
  query = await getAggregateQuery(args, selections, gql.schema, gql.accountability);
@@ -32,50 +29,18 @@ export async function resolveQuery(gql, info) {
32
29
  }
33
30
  if (collection.endsWith('_by_version') && collection in gql.schema.collections === false) {
34
31
  collection = collection.slice(0, -11);
35
- versionRaw = true;
32
+ query.versionRaw = true;
36
33
  }
37
34
  }
38
- if (args['id']) {
39
- query.filter = {
40
- _and: [
41
- query.filter || {},
42
- {
43
- [gql.schema.collections[collection].primary]: {
44
- _eq: args['id'],
45
- },
46
- },
47
- ],
48
- };
49
- query.limit = 1;
50
- }
51
35
  // Transform count(a.b.c) into a.b.count(c)
52
36
  if (query.fields?.length) {
53
37
  for (let fieldIndex = 0; fieldIndex < query.fields.length; fieldIndex++) {
54
38
  query.fields[fieldIndex] = parseFilterFunctionPath(query.fields[fieldIndex]);
55
39
  }
56
40
  }
57
- const result = await gql.read(collection, query);
58
- if (args['version']) {
59
- const versionsService = new VersionsService({ accountability: gql.accountability, schema: gql.schema });
60
- const saves = await versionsService.getVersionSaves(args['version'], collection, args['id']);
61
- if (saves) {
62
- if (gql.schema.collections[collection].singleton) {
63
- return versionRaw
64
- ? mergeVersionsRaw(result, saves)
65
- : mergeVersionsRecursive(result, saves, collection, gql.schema);
66
- }
67
- else {
68
- if (result?.[0] === undefined)
69
- return null;
70
- return versionRaw
71
- ? mergeVersionsRaw(result[0], saves)
72
- : mergeVersionsRecursive(result[0], saves, collection, gql.schema);
73
- }
74
- }
75
- }
76
- if (args['id']) {
77
- return result?.[0] || null;
78
- }
41
+ const result = await gql.read(collection, query, args['id']);
42
+ if (args['id'])
43
+ return result;
79
44
  if (query.group) {
80
45
  // for every entry in result add a group field based on query.group;
81
46
  const aggregateKeys = Object.keys(query.aggregate ?? {});
@@ -18,6 +18,7 @@ import { shouldClearCache } from '../utils/should-clear-cache.js';
18
18
  import { transaction } from '../utils/transaction.js';
19
19
  import { validateKeys } from '../utils/validate-keys.js';
20
20
  import { validateUserCountIntegrity } from '../utils/validate-user-count-integrity.js';
21
+ import { handleVersion } from '../utils/versioning/handle-version.js';
21
22
  import { PayloadService } from './payload.js';
22
23
  const env = useEnv();
23
24
  export class ItemsService {
@@ -96,8 +97,6 @@ export class ItemsService {
96
97
  if (!opts.bypassLimits) {
97
98
  opts.mutationTracker.trackMutations(1);
98
99
  }
99
- const { ActivityService } = await import('./activity.js');
100
- const { RevisionsService } = await import('./revisions.js');
101
100
  const primaryKeyField = this.schema.collections[this.collection].primary;
102
101
  const fields = Object.keys(this.schema.collections[this.collection].fields);
103
102
  const aliases = Object.values(this.schema.collections[this.collection].fields)
@@ -226,7 +225,11 @@ export class ItemsService {
226
225
  }
227
226
  }
228
227
  // If this is an authenticated action, and accountability tracking is enabled, save activity row
229
- if (this.accountability && this.schema.collections[this.collection].accountability !== null) {
228
+ if (opts.skipTracking !== true &&
229
+ this.accountability &&
230
+ this.schema.collections[this.collection].accountability !== null) {
231
+ const { ActivityService } = await import('./activity.js');
232
+ const { RevisionsService } = await import('./revisions.js');
230
233
  const activityService = new ActivityService({
231
234
  knex: trx,
232
235
  schema: this.schema,
@@ -267,6 +270,9 @@ export class ItemsService {
267
270
  if (autoIncrementSequenceNeedsToBeReset) {
268
271
  await getHelpers(trx).sequence.resetAutoIncrementSequence(this.collection, primaryKeyField);
269
272
  }
273
+ if (opts.onItemCreate) {
274
+ opts.onItemCreate(this.collection, primaryKey);
275
+ }
270
276
  return primaryKey;
271
277
  });
272
278
  if (opts.emitEvents !== false) {
@@ -427,7 +433,13 @@ export class ItemsService {
427
433
  validateKeys(this.schema, this.collection, primaryKeyField, key);
428
434
  const filterWithKey = assign({}, query.filter, { [primaryKeyField]: { _eq: key } });
429
435
  const queryWithKey = assign({}, query, { filter: filterWithKey });
430
- const results = await this.readByQuery(queryWithKey, opts);
436
+ let results = [];
437
+ if (query.version) {
438
+ results = (await handleVersion(this, key, queryWithKey, opts));
439
+ }
440
+ else {
441
+ results = await this.readByQuery(queryWithKey, opts);
442
+ }
431
443
  if (results.length === 0) {
432
444
  throw new ForbiddenError();
433
445
  }
@@ -522,8 +534,6 @@ export class ItemsService {
522
534
  if (!opts.bypassLimits) {
523
535
  opts.mutationTracker.trackMutations(keys.length);
524
536
  }
525
- const { ActivityService } = await import('./activity.js');
526
- const { RevisionsService } = await import('./revisions.js');
527
537
  const primaryKeyField = this.schema.collections[this.collection].primary;
528
538
  validateKeys(this.schema, this.collection, primaryKeyField, keys);
529
539
  const fields = Object.keys(this.schema.collections[this.collection].fields);
@@ -616,7 +626,11 @@ export class ItemsService {
616
626
  }
617
627
  }
618
628
  // If this is an authenticated action, and accountability tracking is enabled, save activity row
619
- if (this.accountability && this.schema.collections[this.collection].accountability !== null) {
629
+ if (opts.skipTracking !== true &&
630
+ this.accountability &&
631
+ this.schema.collections[this.collection].accountability !== null) {
632
+ const { ActivityService } = await import('./activity.js');
633
+ const { RevisionsService } = await import('./revisions.js');
620
634
  const activityService = new ActivityService({
621
635
  knex: trx,
622
636
  schema: this.schema,
@@ -780,7 +794,6 @@ export class ItemsService {
780
794
  if (!opts.bypassLimits) {
781
795
  opts.mutationTracker.trackMutations(keys.length);
782
796
  }
783
- const { ActivityService } = await import('./activity.js');
784
797
  const primaryKeyField = this.schema.collections[this.collection].primary;
785
798
  validateKeys(this.schema, this.collection, primaryKeyField, keys);
786
799
  if (this.accountability) {
@@ -816,7 +829,10 @@ export class ItemsService {
816
829
  await validateUserCountIntegrity({ flags: opts.userIntegrityCheckFlags, knex: trx });
817
830
  }
818
831
  }
819
- if (this.accountability && this.schema.collections[this.collection].accountability !== null) {
832
+ if (opts.skipTracking !== true &&
833
+ this.accountability &&
834
+ this.schema.collections[this.collection].accountability !== null) {
835
+ const { ActivityService } = await import('./activity.js');
820
836
  const activityService = new ActivityService({
821
837
  knex: trx,
822
838
  schema: this.schema,
@@ -402,6 +402,10 @@ export class PayloadService {
402
402
  onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
403
403
  bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
404
404
  emitEvents: opts?.emitEvents,
405
+ autoPurgeCache: opts?.autoPurgeCache,
406
+ autoPurgeSystemCache: opts?.autoPurgeSystemCache,
407
+ skipTracking: opts?.skipTracking,
408
+ onItemCreate: opts?.onItemCreate,
405
409
  mutationTracker: opts?.mutationTracker,
406
410
  });
407
411
  }
@@ -412,6 +416,10 @@ export class PayloadService {
412
416
  onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
413
417
  bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
414
418
  emitEvents: opts?.emitEvents,
419
+ autoPurgeCache: opts?.autoPurgeCache,
420
+ autoPurgeSystemCache: opts?.autoPurgeSystemCache,
421
+ skipTracking: opts?.skipTracking,
422
+ onItemCreate: opts?.onItemCreate,
415
423
  mutationTracker: opts?.mutationTracker,
416
424
  });
417
425
  }
@@ -468,6 +476,10 @@ export class PayloadService {
468
476
  onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
469
477
  bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
470
478
  emitEvents: opts?.emitEvents,
479
+ autoPurgeCache: opts?.autoPurgeCache,
480
+ autoPurgeSystemCache: opts?.autoPurgeSystemCache,
481
+ skipTracking: opts?.skipTracking,
482
+ onItemCreate: opts?.onItemCreate,
471
483
  mutationTracker: opts?.mutationTracker,
472
484
  });
473
485
  }
@@ -478,6 +490,10 @@ export class PayloadService {
478
490
  onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
479
491
  bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
480
492
  emitEvents: opts?.emitEvents,
493
+ autoPurgeCache: opts?.autoPurgeCache,
494
+ autoPurgeSystemCache: opts?.autoPurgeSystemCache,
495
+ skipTracking: opts?.skipTracking,
496
+ onItemCreate: opts?.onItemCreate,
481
497
  mutationTracker: opts?.mutationTracker,
482
498
  });
483
499
  }
@@ -570,6 +586,10 @@ export class PayloadService {
570
586
  onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
571
587
  bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
572
588
  emitEvents: opts?.emitEvents,
589
+ autoPurgeCache: opts?.autoPurgeCache,
590
+ autoPurgeSystemCache: opts?.autoPurgeSystemCache,
591
+ skipTracking: opts?.skipTracking,
592
+ onItemCreate: opts?.onItemCreate,
573
593
  mutationTracker: opts?.mutationTracker,
574
594
  })));
575
595
  const query = {
@@ -596,6 +616,10 @@ export class PayloadService {
596
616
  onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
597
617
  bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
598
618
  emitEvents: opts?.emitEvents,
619
+ autoPurgeCache: opts?.autoPurgeCache,
620
+ autoPurgeSystemCache: opts?.autoPurgeSystemCache,
621
+ skipTracking: opts?.skipTracking,
622
+ onItemCreate: opts?.onItemCreate,
599
623
  mutationTracker: opts?.mutationTracker,
600
624
  });
601
625
  }
@@ -605,6 +629,10 @@ export class PayloadService {
605
629
  onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
606
630
  bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
607
631
  emitEvents: opts?.emitEvents,
632
+ autoPurgeCache: opts?.autoPurgeCache,
633
+ autoPurgeSystemCache: opts?.autoPurgeSystemCache,
634
+ skipTracking: opts?.skipTracking,
635
+ onItemCreate: opts?.onItemCreate,
608
636
  mutationTracker: opts?.mutationTracker,
609
637
  });
610
638
  }
@@ -648,6 +676,10 @@ export class PayloadService {
648
676
  onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
649
677
  bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
650
678
  emitEvents: opts?.emitEvents,
679
+ autoPurgeCache: opts?.autoPurgeCache,
680
+ autoPurgeSystemCache: opts?.autoPurgeSystemCache,
681
+ skipTracking: opts?.skipTracking,
682
+ onItemCreate: opts?.onItemCreate,
651
683
  mutationTracker: opts?.mutationTracker,
652
684
  });
653
685
  }
@@ -667,6 +699,10 @@ export class PayloadService {
667
699
  onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
668
700
  bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
669
701
  emitEvents: opts?.emitEvents,
702
+ autoPurgeCache: opts?.autoPurgeCache,
703
+ autoPurgeSystemCache: opts?.autoPurgeSystemCache,
704
+ skipTracking: opts?.skipTracking,
705
+ onItemCreate: opts?.onItemCreate,
670
706
  mutationTracker: opts?.mutationTracker,
671
707
  });
672
708
  }
@@ -694,6 +730,10 @@ export class PayloadService {
694
730
  onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
695
731
  bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
696
732
  emitEvents: opts?.emitEvents,
733
+ autoPurgeCache: opts?.autoPurgeCache,
734
+ autoPurgeSystemCache: opts?.autoPurgeSystemCache,
735
+ skipTracking: opts?.skipTracking,
736
+ onItemCreate: opts?.onItemCreate,
697
737
  mutationTracker: opts?.mutationTracker,
698
738
  });
699
739
  }
@@ -703,6 +743,10 @@ export class PayloadService {
703
743
  onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
704
744
  bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
705
745
  emitEvents: opts?.emitEvents,
746
+ autoPurgeCache: opts?.autoPurgeCache,
747
+ autoPurgeSystemCache: opts?.autoPurgeSystemCache,
748
+ skipTracking: opts?.skipTracking,
749
+ onItemCreate: opts?.onItemCreate,
706
750
  mutationTracker: opts?.mutationTracker,
707
751
  });
708
752
  }
@@ -0,0 +1,17 @@
1
+ import type { CollectionOverview, FieldOverview, Relation, SchemaOverview } from '@directus/types';
2
+ import { type RelationInfo } from './get-relation-info.js';
3
+ /**
4
+ * Allows to deep map the response from the ItemsService with collection, field and relation context for each entry.
5
+ * Bottom to Top depth first mapping of values.
6
+ */
7
+ export declare function deepMapResponse(object: Record<string, any>, callback: (entry: [key: string | number, value: unknown], context: {
8
+ collection: CollectionOverview;
9
+ field: FieldOverview;
10
+ relation: Relation | null;
11
+ leaf: boolean;
12
+ relationType: RelationInfo['relationType'] | null;
13
+ }) => [key: string | number, value: unknown], context: {
14
+ schema: SchemaOverview;
15
+ collection: string;
16
+ relationInfo?: RelationInfo;
17
+ }): any;
@@ -0,0 +1,61 @@
1
+ import { isPlainObject } from 'lodash-es';
2
+ import assert from 'node:assert';
3
+ import { getRelationInfo } from './get-relation-info.js';
4
+ import { InvalidQueryError } from '@directus/errors';
5
+ /**
6
+ * Allows to deep map the response from the ItemsService with collection, field and relation context for each entry.
7
+ * Bottom to Top depth first mapping of values.
8
+ */
9
+ export function deepMapResponse(object, callback, context) {
10
+ const collection = context.schema.collections[context.collection];
11
+ assert(isPlainObject(object) && typeof object === 'object' && object !== null, `DeepMapResponse only works on objects, received ${JSON.stringify(object)}`);
12
+ return Object.fromEntries(Object.entries(object).map(([key, value]) => {
13
+ const field = collection?.fields[key];
14
+ if (!field)
15
+ return [key, value];
16
+ const relationInfo = getRelationInfo(context.schema.relations, collection.collection, field.field);
17
+ let leaf = true;
18
+ if (relationInfo.relation && typeof value === 'object' && value !== null && isPlainObject(object)) {
19
+ switch (relationInfo.relationType) {
20
+ case 'm2o':
21
+ value = deepMapResponse(value, callback, {
22
+ schema: context.schema,
23
+ collection: relationInfo.relation.related_collection,
24
+ relationInfo,
25
+ });
26
+ leaf = false;
27
+ break;
28
+ case 'o2m':
29
+ value = value.map((childValue) => {
30
+ if (isPlainObject(childValue) && typeof childValue === 'object' && childValue !== null) {
31
+ leaf = false;
32
+ return deepMapResponse(childValue, callback, {
33
+ schema: context.schema,
34
+ collection: relationInfo.relation.collection,
35
+ relationInfo,
36
+ });
37
+ }
38
+ else
39
+ return childValue;
40
+ });
41
+ break;
42
+ case 'a2o': {
43
+ const related_collection = object[relationInfo.relation.meta.one_collection_field];
44
+ if (!related_collection) {
45
+ throw new InvalidQueryError({
46
+ reason: `When selecting '${collection.collection}.${field.field}', the field '${collection.collection}.${relationInfo.relation.meta.one_collection_field}' has to be selected when using versioning and m2a relations `,
47
+ });
48
+ }
49
+ value = deepMapResponse(value, callback, {
50
+ schema: context.schema,
51
+ collection: related_collection,
52
+ relationInfo,
53
+ });
54
+ leaf = false;
55
+ break;
56
+ }
57
+ }
58
+ }
59
+ return callback([key, value], { collection, field, ...relationInfo, leaf });
60
+ }));
61
+ }
@@ -1,7 +1,6 @@
1
1
  import type { Relation } from '@directus/types';
2
- type RelationInfo = {
2
+ export type RelationInfo = {
3
3
  relation: Relation | null;
4
4
  relationType: 'o2m' | 'm2o' | 'a2o' | 'o2a' | null;
5
5
  };
6
6
  export declare function getRelationInfo(relations: Relation[], collection: string, field: string): RelationInfo;
7
- export {};
@@ -0,0 +1,8 @@
1
+ import type { Accountability, Filter } from '@directus/types';
2
+ import type { Context } from '../permissions/types.js';
3
+ /**
4
+ * Check if the read permissions for a collection contain the dynamic variable $NOW.
5
+ * If they do, the permissions are not cacheable.
6
+ */
7
+ export declare function permissionsCacheable(collection: string | undefined, context: Context, accountability?: Accountability): Promise<boolean>;
8
+ export declare function filterHasNow(filter: Filter): boolean;
@@ -3,9 +3,9 @@ import { fetchPolicies } from '../permissions/lib/fetch-policies.js';
3
3
  import { createDefaultAccountability } from '../permissions/utils/create-default-accountability.js';
4
4
  /**
5
5
  * Check if the read permissions for a collection contain the dynamic variable $NOW.
6
- * If they do, the permissions are not cachable.
6
+ * If they do, the permissions are not cacheable.
7
7
  */
8
- export async function permissionsCachable(collection, context, accountability) {
8
+ export async function permissionsCacheable(collection, context, accountability) {
9
9
  if (!collection) {
10
10
  return true;
11
11
  }
@@ -18,17 +18,19 @@ export async function permissionsCachable(collection, context, accountability) {
18
18
  if (!permission.permissions) {
19
19
  return false;
20
20
  }
21
- return filter_has_now(permission.permissions);
21
+ return filterHasNow(permission.permissions);
22
22
  });
23
23
  return !has_now;
24
24
  }
25
- export function filter_has_now(filter) {
25
+ export function filterHasNow(filter) {
26
+ if (filter === null)
27
+ return false;
26
28
  return Object.entries(filter).some(([key, value]) => {
27
29
  if (key === '_and' || key === '_or') {
28
- return value.some((sub_filter) => filter_has_now(sub_filter));
30
+ return value.some((sub_filter) => filterHasNow(sub_filter));
29
31
  }
30
32
  else if (typeof value === 'object') {
31
- return filter_has_now(value);
33
+ return filterHasNow(value);
32
34
  }
33
35
  else if (typeof value === 'string') {
34
36
  return value.startsWith('$NOW');
@@ -6,4 +6,4 @@ import { type Knex } from 'knex';
6
6
  * Can be used to ensure the handler is run within a transaction,
7
7
  * while preventing nested transactions.
8
8
  */
9
- export declare const transaction: <T = unknown>(knex: Knex, handler: (knex: Knex) => Promise<T>) => Promise<T>;
9
+ export declare const transaction: <T = unknown>(knex: Knex, handler: (knex: Knex.Transaction) => Promise<T>) => Promise<T>;
@@ -63,7 +63,23 @@ function shouldRetryTransaction(client, error) {
63
63
  * @link https://www.sqlite.org/rescode.html#busy
64
64
  */
65
65
  const SQLITE_BUSY_ERROR_CODE = 'SQLITE_BUSY';
66
+ // Both mariadb and mysql
67
+ const MYSQL_DEADLOCK_CODE = 'ER_LOCK_DEADLOCK';
68
+ const POSTGRES_DEADLOCK_CODE = '40P01';
69
+ const ORACLE_DEADLOCK_CODE = 'ORA-00060';
70
+ const MSSQL_DEADLOCK_CODE = 'EREQUEST';
71
+ const MSSQL_DEADLOCK_NUMBER = '1205';
72
+ const codes = {
73
+ cockroachdb: [{ code: COCKROACH_RETRY_ERROR_CODE }],
74
+ sqlite: [{ code: SQLITE_BUSY_ERROR_CODE }],
75
+ mysql: [{ code: MYSQL_DEADLOCK_CODE }],
76
+ mssql: [{ code: MSSQL_DEADLOCK_CODE, number: MSSQL_DEADLOCK_NUMBER }],
77
+ oracle: [{ code: ORACLE_DEADLOCK_CODE }],
78
+ postgres: [{ code: POSTGRES_DEADLOCK_CODE }],
79
+ redshift: [],
80
+ };
66
81
  return (isObject(error) &&
67
- ((client === 'cockroachdb' && error['code'] === COCKROACH_RETRY_ERROR_CODE) ||
68
- (client === 'sqlite' && error['code'] === SQLITE_BUSY_ERROR_CODE)));
82
+ codes[client].some((code) => {
83
+ return Object.entries(code).every(([key, value]) => String(error[key]) === value);
84
+ }));
69
85
  }
@@ -0,0 +1,3 @@
1
+ import type { Item, PrimaryKey, Query, QueryOptions } from '@directus/types';
2
+ import type { ItemsService as ItemsServiceType } from '../../services/index.js';
3
+ export declare function handleVersion(self: ItemsServiceType, key: PrimaryKey, queryWithKey: Query, opts?: QueryOptions): Promise<(Item | undefined)[]>;
@@ -0,0 +1,92 @@
1
+ import { ForbiddenError } from '@directus/errors';
2
+ import { deepMapResponse } from '../deep-map-response.js';
3
+ import { transaction } from '../transaction.js';
4
+ import { mergeVersionsRaw } from './merge-version-data.js';
5
+ export async function handleVersion(self, key, queryWithKey, opts) {
6
+ const { VersionsService } = await import('../../services/versions.js');
7
+ const { ItemsService } = await import('../../services/items.js');
8
+ if (queryWithKey.versionRaw) {
9
+ const originalData = await self.readByQuery(queryWithKey, opts);
10
+ if (originalData.length === 0) {
11
+ throw new ForbiddenError();
12
+ }
13
+ const versionsService = new VersionsService({
14
+ schema: self.schema,
15
+ accountability: self.accountability,
16
+ knex: self.knex,
17
+ });
18
+ const versionData = await versionsService.getVersionSaves(queryWithKey.version, self.collection, key);
19
+ if (!versionData || versionData.length === 0)
20
+ return [originalData[0]];
21
+ return [mergeVersionsRaw(originalData[0], versionData)];
22
+ }
23
+ let results = [];
24
+ const versionsService = new VersionsService({
25
+ schema: self.schema,
26
+ accountability: self.accountability,
27
+ knex: self.knex,
28
+ });
29
+ const createdIDs = {};
30
+ const versionData = await versionsService.getVersionSaves(queryWithKey.version, self.collection, key);
31
+ await transaction(self.knex, async (trx) => {
32
+ const itemsServiceAdmin = new ItemsService(self.collection, {
33
+ schema: self.schema,
34
+ accountability: {
35
+ admin: true,
36
+ },
37
+ knex: trx,
38
+ });
39
+ await Promise.all((versionData ?? []).map((data) => {
40
+ return itemsServiceAdmin.updateOne(key, data, {
41
+ emitEvents: false,
42
+ autoPurgeCache: false,
43
+ skipTracking: true,
44
+ onItemCreate: (collection, pk) => {
45
+ if (collection in createdIDs === false)
46
+ createdIDs[collection] = [];
47
+ createdIDs[collection].push(pk);
48
+ },
49
+ });
50
+ }));
51
+ const itemsServiceUser = new ItemsService(self.collection, {
52
+ schema: self.schema,
53
+ accountability: self.accountability,
54
+ knex: trx,
55
+ });
56
+ results = await itemsServiceUser.readByQuery(queryWithKey, opts);
57
+ await trx.rollback();
58
+ });
59
+ results = results.map((result) => {
60
+ return deepMapResponse(result, ([key, value], context) => {
61
+ if (context.relationType === 'm2o' || context.relationType === 'a2o') {
62
+ const ids = createdIDs[context.relation.related_collection];
63
+ const match = ids?.find((id) => String(id) === String(value));
64
+ if (match) {
65
+ return [key, null];
66
+ }
67
+ }
68
+ else if (context.relationType === 'o2m' && Array.isArray(value)) {
69
+ const ids = createdIDs[context.relation.collection];
70
+ return [
71
+ key,
72
+ value.map((val) => {
73
+ const match = ids?.find((id) => String(id) === String(val));
74
+ if (match) {
75
+ return null;
76
+ }
77
+ return val;
78
+ }),
79
+ ];
80
+ }
81
+ if (context.field.field === context.collection.primary) {
82
+ const ids = createdIDs[context.collection.collection];
83
+ const match = ids?.find((id) => String(id) === String(value));
84
+ if (match) {
85
+ return [key, null];
86
+ }
87
+ }
88
+ return [key, value];
89
+ }, { collection: self.collection, schema: self.schema });
90
+ });
91
+ return results;
92
+ }
@@ -0,0 +1,2 @@
1
+ import type { Item } from '@directus/types';
2
+ export declare function mergeVersionsRaw(item: Item, versionData: Partial<Item>[]): Item;
@@ -0,0 +1,10 @@
1
+ import { cloneDeep } from 'lodash-es';
2
+ export function mergeVersionsRaw(item, versionData) {
3
+ const result = cloneDeep(item);
4
+ for (const versionRecord of versionData) {
5
+ for (const key of Object.keys(versionRecord)) {
6
+ result[key] = versionRecord[key];
7
+ }
8
+ }
9
+ return result;
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directus/api",
3
- "version": "29.1.1",
3
+ "version": "30.0.0",
4
4
  "description": "Directus is a real-time API and App dashboard for managing SQL database content",
5
5
  "keywords": [
6
6
  "directus",
@@ -59,7 +59,7 @@
59
59
  ],
60
60
  "dependencies": {
61
61
  "@authenio/samlify-node-xmllint": "2.0.0",
62
- "@aws-sdk/client-ses": "3.859.0",
62
+ "@aws-sdk/client-sesv2": "3.864.0",
63
63
  "@godaddy/terminus": "4.12.1",
64
64
  "@rollup/plugin-alias": "5.1.1",
65
65
  "@rollup/plugin-node-resolve": "16.0.1",
@@ -153,30 +153,30 @@
153
153
  "ws": "8.18.3",
154
154
  "zod": "4.0.14",
155
155
  "zod-validation-error": "4.0.1",
156
+ "@directus/app": "13.14.0",
156
157
  "@directus/constants": "13.0.2",
157
158
  "@directus/env": "5.1.2",
158
- "@directus/app": "13.13.1",
159
- "@directus/extensions": "3.0.9",
160
159
  "@directus/errors": "2.0.3",
161
- "@directus/extensions-sdk": "16.0.0",
162
- "@directus/extensions-registry": "3.0.9",
163
- "@directus/pressure": "3.0.8",
164
- "@directus/memory": "3.0.8",
165
- "@directus/schema": "13.0.2",
166
- "@directus/schema-builder": "0.0.4",
167
- "@directus/storage": "12.0.1",
160
+ "@directus/extensions-sdk": "16.0.1",
161
+ "@directus/extensions-registry": "3.0.10",
168
162
  "@directus/format-title": "12.0.1",
163
+ "@directus/extensions": "3.0.10",
164
+ "@directus/memory": "3.0.8",
165
+ "@directus/pressure": "3.0.8",
169
166
  "@directus/specs": "11.1.1",
167
+ "@directus/schema-builder": "0.0.5",
168
+ "@directus/storage": "12.0.1",
170
169
  "@directus/storage-driver-azure": "12.0.8",
170
+ "@directus/schema": "13.0.2",
171
171
  "@directus/storage-driver-cloudinary": "12.0.8",
172
- "@directus/storage-driver-local": "12.0.1",
173
- "@directus/storage-driver-s3": "12.0.8",
174
172
  "@directus/storage-driver-gcs": "12.0.8",
173
+ "@directus/storage-driver-s3": "12.0.8",
175
174
  "@directus/storage-driver-supabase": "3.0.8",
176
- "@directus/system-data": "3.2.1",
175
+ "@directus/storage-driver-local": "12.0.1",
177
176
  "@directus/utils": "13.0.9",
177
+ "@directus/system-data": "3.2.1",
178
178
  "@directus/validation": "2.0.8",
179
- "directus": "11.10.2"
179
+ "directus": "11.11.0"
180
180
  },
181
181
  "devDependencies": {
182
182
  "@directus/tsconfig": "3.0.0",
@@ -220,8 +220,8 @@
220
220
  "knex-mock-client": "3.0.2",
221
221
  "typescript": "5.8.3",
222
222
  "vitest": "3.2.4",
223
- "@directus/schema-builder": "0.0.4",
224
- "@directus/types": "13.2.1"
223
+ "@directus/schema-builder": "0.0.5",
224
+ "@directus/types": "13.2.2"
225
225
  },
226
226
  "optionalDependencies": {
227
227
  "@keyv/redis": "3.0.1",
@@ -1,2 +0,0 @@
1
- import type { RequestHandler } from 'express';
2
- export declare const mergeContentVersions: RequestHandler;
@@ -1,26 +0,0 @@
1
- import { isObject } from '@directus/utils';
2
- import { VersionsService } from '../services/versions.js';
3
- import asyncHandler from '../utils/async-handler.js';
4
- import { mergeVersionsRaw, mergeVersionsRecursive } from '../utils/merge-version-data.js';
5
- export const mergeContentVersions = asyncHandler(async (req, res, next) => {
6
- if (req.sanitizedQuery.version &&
7
- req.collection &&
8
- (req.singleton || req.params['pk']) &&
9
- 'data' in res.locals['payload']) {
10
- const originalData = res.locals['payload'].data;
11
- // only act on single item requests
12
- if (!isObject(originalData))
13
- return next();
14
- const versionsService = new VersionsService({ accountability: req.accountability ?? null, schema: req.schema });
15
- const versionData = await versionsService.getVersionSaves(req.sanitizedQuery.version, req.collection, req.params['pk']);
16
- if (!versionData || versionData.length === 0)
17
- return next();
18
- if (req.sanitizedQuery.versionRaw) {
19
- res.locals['payload'].data = mergeVersionsRaw(originalData, versionData);
20
- }
21
- else {
22
- res.locals['payload'].data = mergeVersionsRecursive(originalData, versionData, req.collection, req.schema);
23
- }
24
- }
25
- return next();
26
- });
@@ -1,3 +0,0 @@
1
- import type { Item, SchemaOverview } from '@directus/types';
2
- export declare function mergeVersionsRaw(item: Item, versionData: Partial<Item>[]): Item;
3
- export declare function mergeVersionsRecursive(item: Item, versionData: Item[], collection: string, schema: SchemaOverview): Item;
@@ -1,134 +0,0 @@
1
- import { isObject } from '@directus/utils';
2
- import Joi from 'joi';
3
- import { cloneDeep } from 'lodash-es';
4
- const alterationSchema = Joi.object({
5
- create: Joi.array().items(Joi.object().unknown()),
6
- update: Joi.array().items(Joi.object().unknown()),
7
- delete: Joi.array().items(Joi.string(), Joi.number()),
8
- });
9
- export function mergeVersionsRaw(item, versionData) {
10
- const result = cloneDeep(item);
11
- for (const versionRecord of versionData) {
12
- for (const key of Object.keys(versionRecord)) {
13
- result[key] = versionRecord[key];
14
- }
15
- }
16
- return result;
17
- }
18
- export function mergeVersionsRecursive(item, versionData, collection, schema) {
19
- if (versionData.length === 0)
20
- return item;
21
- return recursiveMerging(item, versionData, collection, schema);
22
- }
23
- function recursiveMerging(data, versionData, collection, schema) {
24
- const result = cloneDeep(data);
25
- const relations = getRelations(collection, schema);
26
- for (const versionRecord of versionData) {
27
- if (!isObject(versionRecord)) {
28
- continue;
29
- }
30
- for (const key of Object.keys(data)) {
31
- if (key in versionRecord === false) {
32
- continue;
33
- }
34
- const currentValue = data[key];
35
- const newValue = versionRecord[key];
36
- if (typeof newValue !== 'object' || newValue === null) {
37
- // primitive type substitution, json and non relational array values are handled in the next check
38
- result[key] = newValue;
39
- continue;
40
- }
41
- if (key in relations === false) {
42
- // check for m2a exception
43
- if (isManyToAnyCollection(collection, schema) && key === 'item') {
44
- const item = addMissingKeys(isObject(currentValue) ? currentValue : {}, newValue);
45
- result[key] = recursiveMerging(item, [newValue], data['collection'], schema);
46
- }
47
- else {
48
- // item is not a relation
49
- result[key] = newValue;
50
- }
51
- continue;
52
- }
53
- const { error } = alterationSchema.validate(newValue);
54
- if (error) {
55
- if (typeof newValue === 'object' && key in relations) {
56
- const newItem = !currentValue || typeof currentValue !== 'object' ? newValue : currentValue;
57
- result[key] = recursiveMerging(newItem, [newValue], relations[key], schema);
58
- }
59
- continue;
60
- }
61
- const alterations = newValue;
62
- const currentPrimaryKeyField = schema.collections[collection].primary;
63
- const relatedPrimaryKeyField = schema.collections[relations[key]].primary;
64
- const mergedRelation = [];
65
- if (Array.isArray(currentValue)) {
66
- if (alterations.delete.length > 0) {
67
- for (const currentItem of currentValue) {
68
- const currentId = typeof currentItem === 'object' ? currentItem[currentPrimaryKeyField] : currentItem;
69
- if (alterations.delete.includes(currentId) === false) {
70
- mergedRelation.push(currentItem);
71
- }
72
- }
73
- }
74
- else {
75
- mergedRelation.push(...currentValue);
76
- }
77
- if (alterations.update.length > 0) {
78
- for (const updatedItem of alterations.update) {
79
- // find existing item to update
80
- const itemIndex = mergedRelation.findIndex((currentItem) => currentItem[relatedPrimaryKeyField] === updatedItem[currentPrimaryKeyField]);
81
- if (itemIndex === -1) {
82
- // check for raw primary keys
83
- const pkIndex = mergedRelation.findIndex((currentItem) => currentItem === updatedItem[currentPrimaryKeyField]);
84
- if (pkIndex === -1) {
85
- // nothing to update so add the item as is
86
- mergedRelation.push(updatedItem);
87
- }
88
- else {
89
- mergedRelation[pkIndex] = updatedItem;
90
- }
91
- continue;
92
- }
93
- const item = addMissingKeys(mergedRelation[itemIndex], updatedItem);
94
- mergedRelation[itemIndex] = recursiveMerging(item, [updatedItem], relations[key], schema);
95
- }
96
- }
97
- }
98
- if (alterations.create.length > 0) {
99
- for (const createdItem of alterations.create) {
100
- const item = addMissingKeys({}, createdItem);
101
- mergedRelation.push(recursiveMerging(item, [createdItem], relations[key], schema));
102
- }
103
- }
104
- result[key] = mergedRelation;
105
- }
106
- }
107
- return result;
108
- }
109
- function addMissingKeys(item, edits) {
110
- const result = { ...item };
111
- for (const key of Object.keys(edits)) {
112
- if (key in item === false) {
113
- result[key] = null;
114
- }
115
- }
116
- return result;
117
- }
118
- function isManyToAnyCollection(collection, schema) {
119
- const relation = schema.relations.find((relation) => relation.collection === collection && relation.meta?.many_collection === collection);
120
- if (!relation || !relation.meta?.one_field || !relation.related_collection)
121
- return false;
122
- return Boolean(schema.collections[relation.related_collection]?.fields[relation.meta.one_field]?.special.includes('m2a'));
123
- }
124
- function getRelations(collection, schema) {
125
- return schema.relations.reduce((result, relation) => {
126
- if (relation.related_collection === collection && relation.meta?.one_field) {
127
- result[relation.meta.one_field] = relation.collection;
128
- }
129
- if (relation.collection === collection && relation.related_collection) {
130
- result[relation.field] = relation.related_collection;
131
- }
132
- return result;
133
- }, {});
134
- }
@@ -1,8 +0,0 @@
1
- import type { Accountability, Filter } from '@directus/types';
2
- import type { Context } from '../permissions/types.js';
3
- /**
4
- * Check if the read permissions for a collection contain the dynamic variable $NOW.
5
- * If they do, the permissions are not cachable.
6
- */
7
- export declare function permissionsCachable(collection: string | undefined, context: Context, accountability?: Accountability): Promise<boolean>;
8
- export declare function filter_has_now(filter: Filter): boolean;