@directus/api 29.1.1 → 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 (105) 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/constants.d.ts +1 -1
  5. package/dist/constants.js +9 -1
  6. package/dist/controllers/items.js +3 -4
  7. package/dist/controllers/mcp.d.ts +2 -0
  8. package/dist/controllers/mcp.js +33 -0
  9. package/dist/controllers/users.js +17 -7
  10. package/dist/controllers/versions.js +3 -2
  11. package/dist/database/errors/dialects/mssql.d.ts +1 -1
  12. package/dist/database/errors/dialects/mssql.js +18 -10
  13. package/dist/database/migrations/20250813A-add-mcp.d.ts +3 -0
  14. package/dist/database/migrations/20250813A-add-mcp.js +18 -0
  15. package/dist/database/run-ast/README.md +46 -0
  16. package/dist/mailer.js +3 -3
  17. package/dist/mcp/define.d.ts +2 -0
  18. package/dist/mcp/define.js +3 -0
  19. package/dist/mcp/index.d.ts +1 -0
  20. package/dist/mcp/index.js +1 -0
  21. package/dist/mcp/schema.d.ts +485 -0
  22. package/dist/mcp/schema.js +219 -0
  23. package/dist/mcp/server.d.ts +97 -0
  24. package/dist/mcp/server.js +310 -0
  25. package/dist/mcp/tools/assets.d.ts +3 -0
  26. package/dist/mcp/tools/assets.js +54 -0
  27. package/dist/mcp/tools/collections.d.ts +84 -0
  28. package/dist/mcp/tools/collections.js +90 -0
  29. package/dist/mcp/tools/fields.d.ts +101 -0
  30. package/dist/mcp/tools/fields.js +157 -0
  31. package/dist/mcp/tools/files.d.ts +235 -0
  32. package/dist/mcp/tools/files.js +103 -0
  33. package/dist/mcp/tools/flows.d.ts +323 -0
  34. package/dist/mcp/tools/flows.js +85 -0
  35. package/dist/mcp/tools/folders.d.ts +95 -0
  36. package/dist/mcp/tools/folders.js +96 -0
  37. package/dist/mcp/tools/index.d.ts +15 -0
  38. package/dist/mcp/tools/index.js +29 -0
  39. package/dist/mcp/tools/items.d.ts +87 -0
  40. package/dist/mcp/tools/items.js +141 -0
  41. package/dist/mcp/tools/operations.d.ts +171 -0
  42. package/dist/mcp/tools/operations.js +77 -0
  43. package/dist/mcp/tools/prompts/assets.md +8 -0
  44. package/dist/mcp/tools/prompts/collections.md +336 -0
  45. package/dist/mcp/tools/prompts/fields.md +521 -0
  46. package/dist/mcp/tools/prompts/files.md +180 -0
  47. package/dist/mcp/tools/prompts/flows.md +495 -0
  48. package/dist/mcp/tools/prompts/folders.md +34 -0
  49. package/dist/mcp/tools/prompts/index.d.ts +16 -0
  50. package/dist/mcp/tools/prompts/index.js +19 -0
  51. package/dist/mcp/tools/prompts/items.md +317 -0
  52. package/dist/mcp/tools/prompts/operations.md +721 -0
  53. package/dist/mcp/tools/prompts/relations.md +386 -0
  54. package/dist/mcp/tools/prompts/schema.md +130 -0
  55. package/dist/mcp/tools/prompts/system-prompt-description.md +1 -0
  56. package/dist/mcp/tools/prompts/system-prompt.md +44 -0
  57. package/dist/mcp/tools/prompts/trigger-flow.md +214 -0
  58. package/dist/mcp/tools/relations.d.ts +73 -0
  59. package/dist/mcp/tools/relations.js +93 -0
  60. package/dist/mcp/tools/schema.d.ts +54 -0
  61. package/dist/mcp/tools/schema.js +317 -0
  62. package/dist/mcp/tools/system.d.ts +3 -0
  63. package/dist/mcp/tools/system.js +22 -0
  64. package/dist/mcp/tools/trigger-flow.d.ts +8 -0
  65. package/dist/mcp/tools/trigger-flow.js +48 -0
  66. package/dist/mcp/transport.d.ts +13 -0
  67. package/dist/mcp/transport.js +18 -0
  68. package/dist/mcp/types.d.ts +56 -0
  69. package/dist/mcp/types.js +1 -0
  70. package/dist/middleware/respond.js +2 -2
  71. package/dist/services/authentication.js +36 -0
  72. package/dist/services/fields.js +4 -4
  73. package/dist/services/graphql/index.d.ts +2 -2
  74. package/dist/services/graphql/index.js +6 -5
  75. package/dist/services/graphql/resolvers/query.js +4 -39
  76. package/dist/services/items.js +38 -12
  77. package/dist/services/payload.d.ts +7 -3
  78. package/dist/services/payload.js +70 -12
  79. package/dist/services/server.js +1 -0
  80. package/dist/services/tfa.d.ts +1 -1
  81. package/dist/services/tfa.js +20 -5
  82. package/dist/services/versions.d.ts +6 -4
  83. package/dist/services/versions.js +84 -25
  84. package/dist/types/auth.d.ts +2 -1
  85. package/dist/utils/deep-map-response.d.ts +17 -0
  86. package/dist/utils/deep-map-response.js +61 -0
  87. package/dist/utils/get-relation-info.d.ts +1 -2
  88. package/dist/utils/permissions-cacheable.d.ts +8 -0
  89. package/dist/utils/{permissions-cachable.js → permissions-cacheable.js} +8 -6
  90. package/dist/utils/transaction.d.ts +1 -1
  91. package/dist/utils/transaction.js +18 -2
  92. package/dist/utils/versioning/deep-map-with-schema.d.ts +23 -0
  93. package/dist/utils/versioning/deep-map-with-schema.js +81 -0
  94. package/dist/utils/versioning/handle-version.d.ts +3 -0
  95. package/dist/utils/versioning/handle-version.js +96 -0
  96. package/dist/utils/versioning/merge-version-data.d.ts +2 -0
  97. package/dist/utils/versioning/merge-version-data.js +10 -0
  98. package/dist/utils/versioning/split-recursive.d.ts +4 -0
  99. package/dist/utils/versioning/split-recursive.js +27 -0
  100. package/package.json +31 -30
  101. package/dist/middleware/merge-content-versions.d.ts +0 -2
  102. package/dist/middleware/merge-content-versions.js +0 -26
  103. package/dist/utils/merge-version-data.d.ts +0 -3
  104. package/dist/utils/merge-version-data.js +0 -134
  105. package/dist/utils/permissions-cachable.d.ts +0 -8
@@ -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,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,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
+ }
@@ -0,0 +1,3 @@
1
+ import type { 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<any>;
@@ -0,0 +1,96 @@
1
+ import { ForbiddenError } from '@directus/errors';
2
+ import { transaction } from '../transaction.js';
3
+ import { deepMapWithSchema } from './deep-map-with-schema.js';
4
+ import { splitRecursive } from './split-recursive.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 version = await versionsService.getVersionSave(queryWithKey.version, self.collection, key);
19
+ return Object.assign(originalData[0], version?.delta);
20
+ }
21
+ let result;
22
+ const versionsService = new VersionsService({
23
+ schema: self.schema,
24
+ accountability: self.accountability,
25
+ knex: self.knex,
26
+ });
27
+ const createdIDs = {};
28
+ const version = await versionsService.getVersionSave(queryWithKey.version, self.collection, key, false);
29
+ if (!version) {
30
+ throw new ForbiddenError();
31
+ }
32
+ const { delta } = version;
33
+ await transaction(self.knex, async (trx) => {
34
+ const itemsServiceAdmin = new ItemsService(self.collection, {
35
+ schema: self.schema,
36
+ accountability: {
37
+ admin: true,
38
+ },
39
+ knex: trx,
40
+ });
41
+ if (delta) {
42
+ const { rawDelta, defaultOverwrites } = splitRecursive(delta);
43
+ await itemsServiceAdmin.updateOne(key, rawDelta, {
44
+ emitEvents: false,
45
+ autoPurgeCache: false,
46
+ skipTracking: true,
47
+ overwriteDefaults: defaultOverwrites,
48
+ onItemCreate: (collection, pk) => {
49
+ if (collection in createdIDs === false)
50
+ createdIDs[collection] = [];
51
+ createdIDs[collection].push(pk);
52
+ },
53
+ });
54
+ }
55
+ const itemsServiceUser = new ItemsService(self.collection, {
56
+ schema: self.schema,
57
+ accountability: self.accountability,
58
+ knex: trx,
59
+ });
60
+ result = (await itemsServiceUser.readByQuery(queryWithKey, opts))[0];
61
+ await trx.rollback();
62
+ });
63
+ if (!result) {
64
+ throw new ForbiddenError();
65
+ }
66
+ return deepMapWithSchema(result, ([key, value], context) => {
67
+ if (context.relationType === 'm2o' || context.relationType === 'a2o') {
68
+ const ids = createdIDs[context.relation.related_collection];
69
+ const match = ids?.find((id) => String(id) === String(value));
70
+ if (match) {
71
+ return [key, null];
72
+ }
73
+ }
74
+ else if (context.relationType === 'o2m' && Array.isArray(value)) {
75
+ const ids = createdIDs[context.relation.collection];
76
+ return [
77
+ key,
78
+ value.map((val) => {
79
+ const match = ids?.find((id) => String(id) === String(val));
80
+ if (match) {
81
+ return null;
82
+ }
83
+ return val;
84
+ }),
85
+ ];
86
+ }
87
+ if (context.field.field === context.collection.primary) {
88
+ const ids = createdIDs[context.collection.collection];
89
+ const match = ids?.find((id) => String(id) === String(value));
90
+ if (match) {
91
+ return [key, null];
92
+ }
93
+ }
94
+ return [key, value];
95
+ }, { collection: self.collection, schema: self.schema });
96
+ }
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ export declare function splitRecursive(object: unknown): {
2
+ rawDelta: Record<string, any>;
3
+ defaultOverwrites: Record<string, any> | undefined;
4
+ };
@@ -0,0 +1,27 @@
1
+ import { isPlainObject } from 'lodash-es';
2
+ export function splitRecursive(object) {
3
+ if (isPlainObject(object) && typeof object === 'object' && object !== null) {
4
+ const { _user, _date, ...rest } = object;
5
+ const defaultOverwrites = { _user, _date };
6
+ for (const key in rest) {
7
+ const { rawDelta, defaultOverwrites: innerDefaultOverwrites } = splitRecursive(rest[key]);
8
+ rest[key] = rawDelta;
9
+ if (innerDefaultOverwrites)
10
+ defaultOverwrites[key] = innerDefaultOverwrites;
11
+ }
12
+ return { rawDelta: rest, defaultOverwrites };
13
+ }
14
+ else if (Array.isArray(object)) {
15
+ const rest = [];
16
+ const defaultOverwrites = [];
17
+ for (const key in object) {
18
+ const { rawDelta, defaultOverwrites: innerDefaultOverwrites } = splitRecursive(object[key]);
19
+ rest[key] = rawDelta;
20
+ if (innerDefaultOverwrites)
21
+ defaultOverwrites[key] = innerDefaultOverwrites;
22
+ }
23
+ object.map((value) => splitRecursive(value));
24
+ return { rawDelta: rest, defaultOverwrites };
25
+ }
26
+ return { rawDelta: object, defaultOverwrites: undefined };
27
+ }