@directus/api 30.0.0 → 31.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 (83) hide show
  1. package/dist/app.js +5 -0
  2. package/dist/auth/drivers/oauth2.js +17 -3
  3. package/dist/auth/drivers/openid.js +17 -3
  4. package/dist/controllers/mcp.d.ts +2 -0
  5. package/dist/controllers/mcp.js +33 -0
  6. package/dist/controllers/users.js +17 -7
  7. package/dist/controllers/versions.js +3 -2
  8. package/dist/database/errors/dialects/mssql.d.ts +1 -1
  9. package/dist/database/errors/dialects/mssql.js +18 -10
  10. package/dist/database/migrations/20250813A-add-mcp.d.ts +3 -0
  11. package/dist/database/migrations/20250813A-add-mcp.js +18 -0
  12. package/dist/database/run-ast/README.md +46 -0
  13. package/dist/mcp/define.d.ts +2 -0
  14. package/dist/mcp/define.js +3 -0
  15. package/dist/mcp/index.d.ts +1 -0
  16. package/dist/mcp/index.js +1 -0
  17. package/dist/mcp/schema.d.ts +485 -0
  18. package/dist/mcp/schema.js +219 -0
  19. package/dist/mcp/server.d.ts +97 -0
  20. package/dist/mcp/server.js +310 -0
  21. package/dist/mcp/tools/assets.d.ts +3 -0
  22. package/dist/mcp/tools/assets.js +54 -0
  23. package/dist/mcp/tools/collections.d.ts +84 -0
  24. package/dist/mcp/tools/collections.js +90 -0
  25. package/dist/mcp/tools/fields.d.ts +101 -0
  26. package/dist/mcp/tools/fields.js +157 -0
  27. package/dist/mcp/tools/files.d.ts +235 -0
  28. package/dist/mcp/tools/files.js +103 -0
  29. package/dist/mcp/tools/flows.d.ts +323 -0
  30. package/dist/mcp/tools/flows.js +85 -0
  31. package/dist/mcp/tools/folders.d.ts +95 -0
  32. package/dist/mcp/tools/folders.js +96 -0
  33. package/dist/mcp/tools/index.d.ts +15 -0
  34. package/dist/mcp/tools/index.js +29 -0
  35. package/dist/mcp/tools/items.d.ts +87 -0
  36. package/dist/mcp/tools/items.js +141 -0
  37. package/dist/mcp/tools/operations.d.ts +171 -0
  38. package/dist/mcp/tools/operations.js +77 -0
  39. package/dist/mcp/tools/prompts/assets.md +8 -0
  40. package/dist/mcp/tools/prompts/collections.md +336 -0
  41. package/dist/mcp/tools/prompts/fields.md +521 -0
  42. package/dist/mcp/tools/prompts/files.md +180 -0
  43. package/dist/mcp/tools/prompts/flows.md +495 -0
  44. package/dist/mcp/tools/prompts/folders.md +34 -0
  45. package/dist/mcp/tools/prompts/index.d.ts +16 -0
  46. package/dist/mcp/tools/prompts/index.js +19 -0
  47. package/dist/mcp/tools/prompts/items.md +317 -0
  48. package/dist/mcp/tools/prompts/operations.md +721 -0
  49. package/dist/mcp/tools/prompts/relations.md +386 -0
  50. package/dist/mcp/tools/prompts/schema.md +130 -0
  51. package/dist/mcp/tools/prompts/system-prompt-description.md +1 -0
  52. package/dist/mcp/tools/prompts/system-prompt.md +44 -0
  53. package/dist/mcp/tools/prompts/trigger-flow.md +214 -0
  54. package/dist/mcp/tools/relations.d.ts +73 -0
  55. package/dist/mcp/tools/relations.js +93 -0
  56. package/dist/mcp/tools/schema.d.ts +54 -0
  57. package/dist/mcp/tools/schema.js +317 -0
  58. package/dist/mcp/tools/system.d.ts +3 -0
  59. package/dist/mcp/tools/system.js +22 -0
  60. package/dist/mcp/tools/trigger-flow.d.ts +8 -0
  61. package/dist/mcp/tools/trigger-flow.js +48 -0
  62. package/dist/mcp/transport.d.ts +13 -0
  63. package/dist/mcp/transport.js +18 -0
  64. package/dist/mcp/types.d.ts +56 -0
  65. package/dist/mcp/types.js +1 -0
  66. package/dist/services/authentication.js +36 -0
  67. package/dist/services/fields.js +4 -4
  68. package/dist/services/items.js +14 -4
  69. package/dist/services/payload.d.ts +7 -3
  70. package/dist/services/payload.js +26 -12
  71. package/dist/services/server.js +1 -0
  72. package/dist/services/tfa.d.ts +1 -1
  73. package/dist/services/tfa.js +20 -5
  74. package/dist/services/versions.d.ts +6 -4
  75. package/dist/services/versions.js +84 -25
  76. package/dist/types/auth.d.ts +2 -1
  77. package/dist/utils/versioning/deep-map-with-schema.d.ts +23 -0
  78. package/dist/utils/versioning/deep-map-with-schema.js +81 -0
  79. package/dist/utils/versioning/handle-version.d.ts +2 -2
  80. package/dist/utils/versioning/handle-version.js +47 -43
  81. package/dist/utils/versioning/split-recursive.d.ts +4 -0
  82. package/dist/utils/versioning/split-recursive.js +27 -0
  83. package/package.json +30 -29
@@ -1,4 +1,4 @@
1
- import type { AbstractServiceOptions, Accountability, Aggregate, FieldOverview, Item, MutationOptions, PayloadAction, PayloadServiceProcessRelationResult, PrimaryKey, SchemaOverview } from '@directus/types';
1
+ import type { AbstractServiceOptions, Accountability, Aggregate, DefaultOverwrite, FieldOverview, Item, MutationOptions, PayloadAction, PayloadServiceProcessRelationResult, PrimaryKey, SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
3
  import type { Helpers } from '../database/helpers/index.js';
4
4
  type Transformers = {
@@ -9,6 +9,7 @@ type Transformers = {
9
9
  accountability: Accountability | null;
10
10
  specials: string[];
11
11
  helpers: Helpers;
12
+ overwriteDefaults: DefaultOverwrite | undefined;
12
13
  }) => Promise<any>;
13
14
  };
14
15
  /**
@@ -22,7 +23,10 @@ export declare class PayloadService {
22
23
  collection: string;
23
24
  schema: SchemaOverview;
24
25
  nested: string[];
25
- constructor(collection: string, options: AbstractServiceOptions);
26
+ overwriteDefaults: DefaultOverwrite | undefined;
27
+ constructor(collection: string, options: AbstractServiceOptions & {
28
+ overwriteDefaults?: DefaultOverwrite | undefined;
29
+ });
26
30
  transformers: Transformers;
27
31
  processValues(action: PayloadAction, payloads: Partial<Item>[]): Promise<Partial<Item>[]>;
28
32
  processValues(action: PayloadAction, payload: Partial<Item>): Promise<Partial<Item>>;
@@ -62,6 +66,6 @@ export declare class PayloadService {
62
66
  * Transforms the input partial payload to match the output structure, to have consistency
63
67
  * between delta and data
64
68
  */
65
- prepareDelta(data: Partial<Item>): Promise<string | null>;
69
+ prepareDelta(delta: Partial<Item>): Promise<string | null>;
66
70
  }
67
71
  export {};
@@ -21,6 +21,7 @@ export class PayloadService {
21
21
  collection;
22
22
  schema;
23
23
  nested;
24
+ overwriteDefaults;
24
25
  constructor(collection, options) {
25
26
  this.accountability = options.accountability || null;
26
27
  this.knex = options.knex || getDatabase();
@@ -28,6 +29,7 @@ export class PayloadService {
28
29
  this.collection = collection;
29
30
  this.schema = options.schema;
30
31
  this.nested = options.nested ?? [];
32
+ this.overwriteDefaults = options.overwriteDefaults;
31
33
  return this;
32
34
  }
33
35
  transformers = {
@@ -77,14 +79,14 @@ export class PayloadService {
77
79
  return value ? '**********' : null;
78
80
  return value;
79
81
  },
80
- async 'user-created'({ action, value, accountability }) {
82
+ async 'user-created'({ action, value, accountability, overwriteDefaults }) {
81
83
  if (action === 'create')
82
- return accountability?.user || null;
84
+ return (overwriteDefaults ? overwriteDefaults._user : accountability?.user) ?? null;
83
85
  return value;
84
86
  },
85
- async 'user-updated'({ action, value, accountability }) {
87
+ async 'user-updated'({ action, value, accountability, overwriteDefaults }) {
86
88
  if (action === 'update')
87
- return accountability?.user || null;
89
+ return (overwriteDefaults ? overwriteDefaults._user : accountability?.user) ?? null;
88
90
  return value;
89
91
  },
90
92
  async 'role-created'({ action, value, accountability }) {
@@ -97,14 +99,14 @@ export class PayloadService {
97
99
  return accountability?.role || null;
98
100
  return value;
99
101
  },
100
- async 'date-created'({ action, value, helpers }) {
102
+ async 'date-created'({ action, value, helpers, overwriteDefaults }) {
101
103
  if (action === 'create')
102
- return new Date(helpers.date.writeTimestamp(new Date().toISOString()));
104
+ return new Date(overwriteDefaults ? overwriteDefaults._date : helpers.date.writeTimestamp(new Date().toISOString()));
103
105
  return value;
104
106
  },
105
- async 'date-updated'({ action, value, helpers }) {
107
+ async 'date-updated'({ action, value, helpers, overwriteDefaults }) {
106
108
  if (action === 'update')
107
- return new Date(helpers.date.writeTimestamp(new Date().toISOString()));
109
+ return new Date(overwriteDefaults ? overwriteDefaults._date : helpers.date.writeTimestamp(new Date().toISOString()));
108
110
  return value;
109
111
  },
110
112
  async 'cast-csv'({ action, value }) {
@@ -212,6 +214,7 @@ export class PayloadService {
212
214
  accountability,
213
215
  specials: fieldSpecials,
214
216
  helpers: this.helpers,
217
+ overwriteDefaults: this.overwriteDefaults,
215
218
  });
216
219
  }
217
220
  }
@@ -405,6 +408,7 @@ export class PayloadService {
405
408
  autoPurgeCache: opts?.autoPurgeCache,
406
409
  autoPurgeSystemCache: opts?.autoPurgeSystemCache,
407
410
  skipTracking: opts?.skipTracking,
411
+ overwriteDefaults: opts?.overwriteDefaults?.[relation.field],
408
412
  onItemCreate: opts?.onItemCreate,
409
413
  mutationTracker: opts?.mutationTracker,
410
414
  });
@@ -419,6 +423,7 @@ export class PayloadService {
419
423
  autoPurgeCache: opts?.autoPurgeCache,
420
424
  autoPurgeSystemCache: opts?.autoPurgeSystemCache,
421
425
  skipTracking: opts?.skipTracking,
426
+ overwriteDefaults: opts?.overwriteDefaults?.[relation.field],
422
427
  onItemCreate: opts?.onItemCreate,
423
428
  mutationTracker: opts?.mutationTracker,
424
429
  });
@@ -479,6 +484,7 @@ export class PayloadService {
479
484
  autoPurgeCache: opts?.autoPurgeCache,
480
485
  autoPurgeSystemCache: opts?.autoPurgeSystemCache,
481
486
  skipTracking: opts?.skipTracking,
487
+ overwriteDefaults: opts?.overwriteDefaults?.[relation.field],
482
488
  onItemCreate: opts?.onItemCreate,
483
489
  mutationTracker: opts?.mutationTracker,
484
490
  });
@@ -493,6 +499,7 @@ export class PayloadService {
493
499
  autoPurgeCache: opts?.autoPurgeCache,
494
500
  autoPurgeSystemCache: opts?.autoPurgeSystemCache,
495
501
  skipTracking: opts?.skipTracking,
502
+ overwriteDefaults: opts?.overwriteDefaults?.[relation.field],
496
503
  onItemCreate: opts?.onItemCreate,
497
504
  mutationTracker: opts?.mutationTracker,
498
505
  });
@@ -589,6 +596,7 @@ export class PayloadService {
589
596
  autoPurgeCache: opts?.autoPurgeCache,
590
597
  autoPurgeSystemCache: opts?.autoPurgeSystemCache,
591
598
  skipTracking: opts?.skipTracking,
599
+ overwriteDefaults: opts?.overwriteDefaults?.[relation.meta.one_field],
592
600
  onItemCreate: opts?.onItemCreate,
593
601
  mutationTracker: opts?.mutationTracker,
594
602
  })));
@@ -619,6 +627,7 @@ export class PayloadService {
619
627
  autoPurgeCache: opts?.autoPurgeCache,
620
628
  autoPurgeSystemCache: opts?.autoPurgeSystemCache,
621
629
  skipTracking: opts?.skipTracking,
630
+ overwriteDefaults: opts?.overwriteDefaults?.[relation.meta.one_field],
622
631
  onItemCreate: opts?.onItemCreate,
623
632
  mutationTracker: opts?.mutationTracker,
624
633
  });
@@ -632,6 +641,7 @@ export class PayloadService {
632
641
  autoPurgeCache: opts?.autoPurgeCache,
633
642
  autoPurgeSystemCache: opts?.autoPurgeSystemCache,
634
643
  skipTracking: opts?.skipTracking,
644
+ overwriteDefaults: opts?.overwriteDefaults?.[relation.meta.one_field],
635
645
  onItemCreate: opts?.onItemCreate,
636
646
  mutationTracker: opts?.mutationTracker,
637
647
  });
@@ -679,13 +689,14 @@ export class PayloadService {
679
689
  autoPurgeCache: opts?.autoPurgeCache,
680
690
  autoPurgeSystemCache: opts?.autoPurgeSystemCache,
681
691
  skipTracking: opts?.skipTracking,
692
+ overwriteDefaults: opts?.overwriteDefaults?.[relation.meta.one_field]?.['create'],
682
693
  onItemCreate: opts?.onItemCreate,
683
694
  mutationTracker: opts?.mutationTracker,
684
695
  });
685
696
  }
686
697
  if (alterations.update) {
687
- for (const item of alterations.update) {
688
- const { [relatedPrimaryKeyField]: key, ...record } = item;
698
+ for (const index in alterations.update) {
699
+ const { [relatedPrimaryKeyField]: key, ...record } = alterations.update[index];
689
700
  const existingRecord = await this.knex
690
701
  .select(relatedPrimaryKeyField, relation.field)
691
702
  .from(relation.collection)
@@ -702,6 +713,7 @@ export class PayloadService {
702
713
  autoPurgeCache: opts?.autoPurgeCache,
703
714
  autoPurgeSystemCache: opts?.autoPurgeSystemCache,
704
715
  skipTracking: opts?.skipTracking,
716
+ overwriteDefaults: opts?.overwriteDefaults?.[relation.meta.one_field]?.['update'][index],
705
717
  onItemCreate: opts?.onItemCreate,
706
718
  mutationTracker: opts?.mutationTracker,
707
719
  });
@@ -733,6 +745,7 @@ export class PayloadService {
733
745
  autoPurgeCache: opts?.autoPurgeCache,
734
746
  autoPurgeSystemCache: opts?.autoPurgeSystemCache,
735
747
  skipTracking: opts?.skipTracking,
748
+ overwriteDefaults: opts?.overwriteDefaults?.[relation.meta.one_field]?.['delete'],
736
749
  onItemCreate: opts?.onItemCreate,
737
750
  mutationTracker: opts?.mutationTracker,
738
751
  });
@@ -746,6 +759,7 @@ export class PayloadService {
746
759
  autoPurgeCache: opts?.autoPurgeCache,
747
760
  autoPurgeSystemCache: opts?.autoPurgeSystemCache,
748
761
  skipTracking: opts?.skipTracking,
762
+ overwriteDefaults: opts?.overwriteDefaults?.[relation.meta.one_field]?.['delete'],
749
763
  onItemCreate: opts?.onItemCreate,
750
764
  mutationTracker: opts?.mutationTracker,
751
765
  });
@@ -759,8 +773,8 @@ export class PayloadService {
759
773
  * Transforms the input partial payload to match the output structure, to have consistency
760
774
  * between delta and data
761
775
  */
762
- async prepareDelta(data) {
763
- let payload = cloneDeep(data);
776
+ async prepareDelta(delta) {
777
+ let payload = cloneDeep(delta);
764
778
  for (const key in payload) {
765
779
  if (payload[key]?.isRawInstance) {
766
780
  payload[key] = payload[key].bindings[0];
@@ -53,6 +53,7 @@ export class ServerService {
53
53
  ],
54
54
  });
55
55
  info['project'] = projectInfo;
56
+ info['mcp_enabled'] = toBoolean(env['MCP_ENABLED'] ?? true);
56
57
  if (this.accountability?.user) {
57
58
  if (env['RATE_LIMITER_ENABLED']) {
58
59
  info['rateLimit'] = {
@@ -6,7 +6,7 @@ export declare class TFAService {
6
6
  itemsService: ItemsService;
7
7
  constructor(options: AbstractServiceOptions);
8
8
  verifyOTP(key: PrimaryKey, otp: string, secret?: string): Promise<boolean>;
9
- generateTFA(key: PrimaryKey): Promise<Record<string, string>>;
9
+ generateTFA(key: PrimaryKey, requiresPassword?: boolean): Promise<Record<string, string>>;
10
10
  enableTFA(key: PrimaryKey, otp: string, secret: string): Promise<void>;
11
11
  disableTFA(key: PrimaryKey): Promise<void>;
12
12
  }
@@ -2,6 +2,7 @@ import { InvalidPayloadError } from '@directus/errors';
2
2
  import { authenticator } from 'otplib';
3
3
  import getDatabase from '../database/index.js';
4
4
  import { ItemsService } from './items.js';
5
+ import { DEFAULT_AUTH_PROVIDER } from '../constants.js';
5
6
  export class TFAService {
6
7
  knex;
7
8
  itemsService;
@@ -19,26 +20,40 @@ export class TFAService {
19
20
  }
20
21
  return authenticator.check(otp, user.tfa_secret);
21
22
  }
22
- async generateTFA(key) {
23
- const user = await this.knex.select('email', 'tfa_secret').from('directus_users').where({ id: key }).first();
23
+ async generateTFA(key, requiresPassword = true) {
24
+ const user = await this.knex
25
+ .select('email', 'tfa_secret', 'provider', 'external_identifier')
26
+ .from('directus_users')
27
+ .where({ id: key })
28
+ .first();
24
29
  if (user?.tfa_secret !== null) {
25
30
  throw new InvalidPayloadError({ reason: 'TFA Secret is already set for this user' });
26
31
  }
27
- if (!user?.email) {
32
+ // Only require email for non-OAuth users
33
+ if (user?.provider === DEFAULT_AUTH_PROVIDER && !user?.email) {
28
34
  throw new InvalidPayloadError({ reason: 'User must have a valid email to enable TFA' });
29
35
  }
36
+ if (!requiresPassword && user?.provider === DEFAULT_AUTH_PROVIDER) {
37
+ throw new InvalidPayloadError({ reason: 'This method is only available for OAuth users' });
38
+ }
30
39
  const secret = authenticator.generateSecret();
31
40
  const project = await this.knex.select('project_name').from('directus_settings').limit(1).first();
41
+ // For OAuth users without email, use external_identifier as fallback
42
+ const accountName = user.email || user.external_identifier || `user_${key}`;
32
43
  return {
33
44
  secret,
34
- url: authenticator.keyuri(user.email, project?.project_name || 'Directus', secret),
45
+ url: authenticator.keyuri(accountName, project?.project_name || 'Directus', secret),
35
46
  };
36
47
  }
37
48
  async enableTFA(key, otp, secret) {
38
- const user = await this.knex.select('tfa_secret').from('directus_users').where({ id: key }).first();
49
+ const user = await this.knex.select('tfa_secret', 'provider').from('directus_users').where({ id: key }).first();
50
+ const requiresPassword = user?.['provider'] === DEFAULT_AUTH_PROVIDER;
39
51
  if (user?.tfa_secret !== null) {
40
52
  throw new InvalidPayloadError({ reason: 'TFA Secret is already set for this user' });
41
53
  }
54
+ if (!requiresPassword && user?.provider === DEFAULT_AUTH_PROVIDER) {
55
+ throw new InvalidPayloadError({ reason: 'This method is only available for OAuth users' });
56
+ }
42
57
  if (!authenticator.check(otp, secret)) {
43
58
  throw new InvalidPayloadError({ reason: `"otp" is invalid` });
44
59
  }
@@ -1,6 +1,6 @@
1
- import type { AbstractServiceOptions, Item, MutationOptions, PrimaryKey, Query } from '@directus/types';
1
+ import type { AbstractServiceOptions, ContentVersion, Item, MutationOptions, PrimaryKey, Query, QueryOptions } from '@directus/types';
2
2
  import { ItemsService } from './items.js';
3
- export declare class VersionsService extends ItemsService {
3
+ export declare class VersionsService extends ItemsService<ContentVersion> {
4
4
  constructor(options: AbstractServiceOptions);
5
5
  private validateCreateData;
6
6
  getMainItem(collection: string, item: PrimaryKey, query?: Query): Promise<Item>;
@@ -8,10 +8,12 @@ export declare class VersionsService extends ItemsService {
8
8
  outdated: boolean;
9
9
  mainHash: string;
10
10
  }>;
11
- getVersionSaves(key: string, collection: string, item: string | undefined): Promise<Partial<Item>[] | null>;
11
+ getVersionSave(key: string, collection: string, item: string, mapDelta?: boolean): Promise<ContentVersion | undefined>;
12
12
  createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
13
+ readOne(key: PrimaryKey, query?: Query, opts?: QueryOptions): Promise<ContentVersion>;
13
14
  createMany(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]>;
14
15
  updateMany(keys: PrimaryKey[], data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey[]>;
15
- save(key: PrimaryKey, data: Partial<Item>): Promise<Partial<Item>>;
16
+ save(key: PrimaryKey, delta: Partial<Item>): Promise<Partial<Item>>;
16
17
  promote(version: PrimaryKey, mainHash: string, fields?: string[]): Promise<PrimaryKey>;
18
+ private mapDelta;
17
19
  }
@@ -1,16 +1,19 @@
1
1
  import { Action } from '@directus/constants';
2
2
  import { ForbiddenError, InvalidPayloadError, UnprocessableContentError } from '@directus/errors';
3
3
  import Joi from 'joi';
4
- import { assign, pick } from 'lodash-es';
4
+ import { assign, get, isEqual, isPlainObject, pick } from 'lodash-es';
5
5
  import objectHash from 'object-hash';
6
6
  import { getCache } from '../cache.js';
7
+ import { getHelpers } from '../database/helpers/index.js';
7
8
  import emitter from '../emitter.js';
8
9
  import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
9
10
  import { shouldClearCache } from '../utils/should-clear-cache.js';
11
+ import { splitRecursive } from '../utils/versioning/split-recursive.js';
10
12
  import { ActivityService } from './activity.js';
11
13
  import { ItemsService } from './items.js';
12
14
  import { PayloadService } from './payload.js';
13
15
  import { RevisionsService } from './revisions.js';
16
+ import { deepMapWithSchema } from '../utils/versioning/deep-map-with-schema.js';
14
17
  export class VersionsService extends ItemsService {
15
18
  constructor(options) {
16
19
  super('directus_versions', options);
@@ -59,10 +62,10 @@ export class VersionsService extends ItemsService {
59
62
  knex: this.knex,
60
63
  schema: this.schema,
61
64
  });
62
- const existingVersions = await sudoService.readByQuery({
65
+ const existingVersions = (await sudoService.readByQuery({
63
66
  aggregate: { count: ['*'] },
64
67
  filter: { key: { _eq: data['key'] }, collection: { _eq: data['collection'] }, item: { _eq: data['item'] } },
65
- });
68
+ }));
66
69
  if (existingVersions[0]['count'] > 0) {
67
70
  throw new UnprocessableContentError({
68
71
  reason: `Version "${data['key']}" already exists for item "${data['item']}" in collection "${data['collection']}"`,
@@ -82,21 +85,17 @@ export class VersionsService extends ItemsService {
82
85
  const mainHash = objectHash(mainItem);
83
86
  return { outdated: hash !== mainHash, mainHash };
84
87
  }
85
- async getVersionSaves(key, collection, item) {
86
- const filter = {
87
- key: { _eq: key },
88
- collection: { _eq: collection },
89
- };
90
- if (item) {
91
- filter['item'] = { _eq: item };
92
- }
93
- const versions = await this.readByQuery({ filter });
94
- if (!versions?.[0])
95
- return null;
96
- if (versions[0]['delta']) {
97
- return [versions[0]['delta']];
98
- }
99
- return null;
88
+ async getVersionSave(key, collection, item, mapDelta = true) {
89
+ const version = (await this.readByQuery({
90
+ filter: {
91
+ key: { _eq: key },
92
+ collection: { _eq: collection },
93
+ item: { _eq: item },
94
+ },
95
+ }))[0];
96
+ if (mapDelta && version?.delta)
97
+ version.delta = this.mapDelta(version);
98
+ return version;
100
99
  }
101
100
  async createOne(data, opts) {
102
101
  await this.validateCreateData(data);
@@ -104,6 +103,12 @@ export class VersionsService extends ItemsService {
104
103
  data['hash'] = objectHash(mainItem);
105
104
  return super.createOne(data, opts);
106
105
  }
106
+ async readOne(key, query = {}, opts) {
107
+ const version = await super.readOne(key, query, opts);
108
+ if (version?.delta)
109
+ version.delta = this.mapDelta(version);
110
+ return version;
111
+ }
107
112
  async createMany(data, opts) {
108
113
  if (!Array.isArray(data)) {
109
114
  throw new InvalidPayloadError({ reason: 'Input should be an array of items' });
@@ -156,7 +161,7 @@ export class VersionsService extends ItemsService {
156
161
  }
157
162
  return super.updateMany(keys, data, opts);
158
163
  }
159
- async save(key, data) {
164
+ async save(key, delta) {
160
165
  const version = await super.readOne(key);
161
166
  const payloadService = new PayloadService(this.collection, {
162
167
  accountability: this.accountability,
@@ -171,7 +176,7 @@ export class VersionsService extends ItemsService {
171
176
  knex: this.knex,
172
177
  schema: this.schema,
173
178
  });
174
- const { item, collection } = version;
179
+ const { item, collection, delta: existingDelta } = version;
175
180
  const activity = await activityService.createOne({
176
181
  action: Action.VERSION_SAVE,
177
182
  user: this.accountability?.user ?? null,
@@ -181,7 +186,8 @@ export class VersionsService extends ItemsService {
181
186
  origin: this.accountability?.origin ?? null,
182
187
  item,
183
188
  });
184
- const revisionDelta = await payloadService.prepareDelta(data);
189
+ const helpers = getHelpers(this.knex);
190
+ let revisionDelta = await payloadService.prepareDelta(delta);
185
191
  await revisionsService.createOne({
186
192
  activity,
187
193
  version: key,
@@ -190,10 +196,23 @@ export class VersionsService extends ItemsService {
190
196
  data: revisionDelta,
191
197
  delta: revisionDelta,
192
198
  });
193
- const finalVersionDelta = assign({}, version['delta'], revisionDelta ? JSON.parse(revisionDelta) : null);
199
+ revisionDelta = revisionDelta ? JSON.parse(revisionDelta) : null;
200
+ const date = new Date(helpers.date.writeTimestamp(new Date().toISOString()));
201
+ deepMapObjects(revisionDelta, (object, path) => {
202
+ const existing = get(existingDelta, path);
203
+ if (existing && isEqual(existing, object))
204
+ return;
205
+ object['_user'] = this.accountability?.user;
206
+ object['_date'] = date;
207
+ });
208
+ const finalVersionDelta = assign({}, existingDelta, revisionDelta);
194
209
  const sudoService = new ItemsService(this.collection, {
195
210
  knex: this.knex,
196
211
  schema: this.schema,
212
+ accountability: {
213
+ ...this.accountability,
214
+ admin: true,
215
+ },
197
216
  });
198
217
  await sudoService.updateOne(key, { delta: finalVersionDelta });
199
218
  const { cache } = getCache();
@@ -203,7 +222,7 @@ export class VersionsService extends ItemsService {
203
222
  return finalVersionDelta;
204
223
  }
205
224
  async promote(version, mainHash, fields) {
206
- const { collection, item, delta } = (await this.readOne(version));
225
+ const { collection, item, delta } = (await super.readOne(version));
207
226
  // will throw an error if the accountability does not have permission to update the item
208
227
  if (this.accountability) {
209
228
  await validateAccess({
@@ -227,7 +246,8 @@ export class VersionsService extends ItemsService {
227
246
  reason: `Main item has changed since this version was last updated`,
228
247
  });
229
248
  }
230
- const payloadToUpdate = fields ? pick(delta, fields) : delta;
249
+ const { rawDelta, defaultOverwrites } = splitRecursive(delta);
250
+ const payloadToUpdate = fields ? pick(rawDelta, fields) : rawDelta;
231
251
  const itemsService = new ItemsService(collection, {
232
252
  accountability: this.accountability,
233
253
  knex: this.knex,
@@ -242,7 +262,9 @@ export class VersionsService extends ItemsService {
242
262
  schema: this.schema,
243
263
  accountability: this.accountability,
244
264
  });
245
- const updatedItemKey = await itemsService.updateOne(item, payloadAfterHooks);
265
+ const updatedItemKey = await itemsService.updateOne(item, payloadAfterHooks, {
266
+ overwriteDefaults: defaultOverwrites,
267
+ });
246
268
  emitter.emitAction(['items.promote', `${collection}.items.promote`], {
247
269
  payload: payloadAfterHooks,
248
270
  collection,
@@ -255,4 +277,41 @@ export class VersionsService extends ItemsService {
255
277
  });
256
278
  return updatedItemKey;
257
279
  }
280
+ mapDelta(version) {
281
+ const delta = version.delta ?? {};
282
+ delta[this.schema.collections[version.collection].primary] = version.item;
283
+ return deepMapWithSchema(delta, ([key, value], context) => {
284
+ if (key === '_user' || key === '_date')
285
+ return;
286
+ if (context.collection.primary in context.object) {
287
+ if (context.field.special.includes('user-updated')) {
288
+ return [key, context.object['_user']];
289
+ }
290
+ if (context.field.special.includes('date-updated')) {
291
+ return [key, context.object['_date']];
292
+ }
293
+ }
294
+ else {
295
+ if (context.field.special.includes('user-created')) {
296
+ return [key, context.object['_user']];
297
+ }
298
+ if (context.field.special.includes('date-created')) {
299
+ return [key, context.object['_date']];
300
+ }
301
+ }
302
+ if (key in context.object)
303
+ return [key, value];
304
+ return undefined;
305
+ }, { collection: version.collection, schema: this.schema }, { mapNonExistentFields: true, detailedUpdateSyntax: true });
306
+ }
307
+ }
308
+ /** Deeply maps all objects of a structure. Only calls the callback for objects, not for arrays. Objects in arrays will continued to be mapped. */
309
+ function deepMapObjects(object, fn, path = []) {
310
+ if (isPlainObject(object) && typeof object === 'object' && object !== null) {
311
+ fn(object, path);
312
+ Object.entries(object).map(([key, value]) => deepMapObjects(value, fn, [...path, key]));
313
+ }
314
+ else if (Array.isArray(object)) {
315
+ object.map((value, index) => deepMapObjects(value, fn, [...path, String(index)]));
316
+ }
258
317
  }
@@ -27,10 +27,11 @@ export interface Session {
27
27
  export type DirectusTokenPayload = {
28
28
  id?: string;
29
29
  role: string | null;
30
- session?: string;
31
30
  app_access: boolean | number;
32
31
  admin_access: boolean | number;
33
32
  share?: string;
33
+ session?: string;
34
+ enforce_tfa?: boolean;
34
35
  };
35
36
  export type ShareData = {
36
37
  share_id: string;
@@ -0,0 +1,23 @@
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 data like a response or delta changes with collection, field and relation context for each entry.
5
+ * Bottom to Top depth first mapping of values.
6
+ */
7
+ export declare function deepMapWithSchema(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
+ object: Record<string, any>;
14
+ }) => [key: string | number, value: unknown] | undefined, context: {
15
+ schema: SchemaOverview;
16
+ collection: string;
17
+ relationInfo?: RelationInfo;
18
+ }, options?: {
19
+ /** If set to true, non-existent fields will be included in the mapping and will have a value of undefined */
20
+ mapNonExistentFields?: boolean;
21
+ /** If set to true, it will map the "create", "update" and "delete" syntax for o2m relations found in deltas */
22
+ detailedUpdateSyntax?: boolean;
23
+ }): any;
@@ -0,0 +1,81 @@
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 data like a response or delta changes with collection, field and relation context for each entry.
7
+ * Bottom to Top depth first mapping of values.
8
+ */
9
+ export function deepMapWithSchema(object, callback, context, options) {
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
+ let fields;
13
+ if (options?.mapNonExistentFields) {
14
+ fields = Object.entries(collection.fields);
15
+ }
16
+ else {
17
+ fields = Object.keys(object).map((key) => [key, collection.fields[key]]);
18
+ }
19
+ return Object.fromEntries(fields
20
+ .map(([key, field]) => {
21
+ let value = object[key];
22
+ if (!field)
23
+ return [key, value];
24
+ const relationInfo = getRelationInfo(context.schema.relations, collection.collection, field.field);
25
+ let leaf = true;
26
+ if (relationInfo.relation && typeof value === 'object' && value !== null && isPlainObject(object)) {
27
+ switch (relationInfo.relationType) {
28
+ case 'm2o':
29
+ value = deepMapWithSchema(value, callback, {
30
+ schema: context.schema,
31
+ collection: relationInfo.relation.related_collection,
32
+ relationInfo,
33
+ }, options);
34
+ leaf = false;
35
+ break;
36
+ case 'o2m': {
37
+ function map(childValue) {
38
+ if (isPlainObject(childValue) && typeof childValue === 'object' && childValue !== null) {
39
+ leaf = false;
40
+ return deepMapWithSchema(childValue, callback, {
41
+ schema: context.schema,
42
+ collection: relationInfo.relation.collection,
43
+ relationInfo,
44
+ }, options);
45
+ }
46
+ else
47
+ return childValue;
48
+ }
49
+ if (Array.isArray(value)) {
50
+ value = value.map(map);
51
+ }
52
+ else if (options?.detailedUpdateSyntax && isPlainObject(value)) {
53
+ value = {
54
+ create: value['create']?.map(map),
55
+ update: value['update']?.map(map),
56
+ delete: value['delete']?.map(map),
57
+ };
58
+ }
59
+ break;
60
+ }
61
+ case 'a2o': {
62
+ const related_collection = object[relationInfo.relation.meta.one_collection_field];
63
+ if (!related_collection) {
64
+ throw new InvalidQueryError({
65
+ 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 `,
66
+ });
67
+ }
68
+ value = deepMapWithSchema(value, callback, {
69
+ schema: context.schema,
70
+ collection: related_collection,
71
+ relationInfo,
72
+ }, options);
73
+ leaf = false;
74
+ break;
75
+ }
76
+ }
77
+ }
78
+ return callback([key, value], { collection, field, ...relationInfo, leaf, object });
79
+ })
80
+ .filter((f) => f));
81
+ }
@@ -1,3 +1,3 @@
1
- import type { Item, PrimaryKey, Query, QueryOptions } from '@directus/types';
1
+ import type { PrimaryKey, Query, QueryOptions } from '@directus/types';
2
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)[]>;
3
+ export declare function handleVersion(self: ItemsServiceType, key: PrimaryKey, queryWithKey: Query, opts?: QueryOptions): Promise<any>;