@directus/api 30.0.0 → 32.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/dist/app.js +7 -0
  2. package/dist/auth/auth.d.ts +2 -1
  3. package/dist/auth/auth.js +7 -2
  4. package/dist/auth/drivers/ldap.d.ts +0 -2
  5. package/dist/auth/drivers/ldap.js +9 -7
  6. package/dist/auth/drivers/oauth2.d.ts +0 -2
  7. package/dist/auth/drivers/oauth2.js +28 -11
  8. package/dist/auth/drivers/openid.d.ts +0 -2
  9. package/dist/auth/drivers/openid.js +28 -11
  10. package/dist/auth/drivers/saml.d.ts +0 -2
  11. package/dist/auth/drivers/saml.js +5 -5
  12. package/dist/auth.js +1 -2
  13. package/dist/cli/commands/bootstrap/index.js +12 -33
  14. package/dist/cli/commands/init/index.js +1 -1
  15. package/dist/cli/commands/schema/apply.d.ts +4 -0
  16. package/dist/cli/commands/schema/apply.js +26 -3
  17. package/dist/controllers/collections.js +7 -2
  18. package/dist/controllers/fields.js +31 -8
  19. package/dist/controllers/mcp.d.ts +2 -0
  20. package/dist/controllers/mcp.js +33 -0
  21. package/dist/controllers/server.js +26 -1
  22. package/dist/controllers/settings.js +9 -2
  23. package/dist/controllers/users.js +17 -7
  24. package/dist/controllers/versions.js +3 -2
  25. package/dist/database/errors/dialects/mssql.d.ts +1 -1
  26. package/dist/database/errors/dialects/mssql.js +18 -10
  27. package/dist/database/helpers/fn/types.js +3 -3
  28. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +2 -1
  29. package/dist/database/helpers/schema/dialects/cockroachdb.js +13 -0
  30. package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -1
  31. package/dist/database/helpers/schema/dialects/mssql.js +23 -0
  32. package/dist/database/helpers/schema/dialects/mysql.d.ts +2 -1
  33. package/dist/database/helpers/schema/dialects/mysql.js +25 -0
  34. package/dist/database/helpers/schema/dialects/oracle.d.ts +2 -1
  35. package/dist/database/helpers/schema/dialects/oracle.js +13 -0
  36. package/dist/database/helpers/schema/dialects/postgres.d.ts +2 -1
  37. package/dist/database/helpers/schema/dialects/postgres.js +13 -0
  38. package/dist/database/helpers/schema/types.d.ts +5 -0
  39. package/dist/database/helpers/schema/types.js +6 -0
  40. package/dist/database/migrations/20250813A-add-mcp.d.ts +3 -0
  41. package/dist/database/migrations/20250813A-add-mcp.js +18 -0
  42. package/dist/database/migrations/20251012A-add-field-searchable.d.ts +3 -0
  43. package/dist/database/migrations/20251012A-add-field-searchable.js +10 -0
  44. package/dist/database/migrations/20251014A-add-project-owner.d.ts +3 -0
  45. package/dist/database/migrations/20251014A-add-project-owner.js +37 -0
  46. package/dist/database/migrations/20251028A-add-retention-indexes.d.ts +3 -0
  47. package/dist/database/migrations/20251028A-add-retention-indexes.js +42 -0
  48. package/dist/database/run-ast/README.md +46 -0
  49. package/dist/database/run-ast/lib/apply-query/add-join.js +2 -2
  50. package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
  51. package/dist/database/run-ast/lib/apply-query/index.d.ts +0 -1
  52. package/dist/database/run-ast/lib/apply-query/index.js +4 -6
  53. package/dist/database/run-ast/lib/apply-query/search.js +2 -0
  54. package/dist/database/run-ast/lib/get-db-query.js +7 -6
  55. package/dist/database/run-ast/utils/generate-alias.d.ts +6 -0
  56. package/dist/database/run-ast/utils/generate-alias.js +57 -0
  57. package/dist/flows.js +1 -0
  58. package/dist/mcp/define.d.ts +2 -0
  59. package/dist/mcp/define.js +3 -0
  60. package/dist/mcp/index.d.ts +1 -0
  61. package/dist/mcp/index.js +1 -0
  62. package/dist/mcp/schema.d.ts +485 -0
  63. package/dist/mcp/schema.js +219 -0
  64. package/dist/mcp/server.d.ts +103 -0
  65. package/dist/mcp/server.js +310 -0
  66. package/dist/mcp/tools/assets.d.ts +3 -0
  67. package/dist/mcp/tools/assets.js +54 -0
  68. package/dist/mcp/tools/collections.d.ts +84 -0
  69. package/dist/mcp/tools/collections.js +90 -0
  70. package/dist/mcp/tools/fields.d.ts +101 -0
  71. package/dist/mcp/tools/fields.js +157 -0
  72. package/dist/mcp/tools/files.d.ts +235 -0
  73. package/dist/mcp/tools/files.js +103 -0
  74. package/dist/mcp/tools/flows.d.ts +323 -0
  75. package/dist/mcp/tools/flows.js +85 -0
  76. package/dist/mcp/tools/folders.d.ts +95 -0
  77. package/dist/mcp/tools/folders.js +96 -0
  78. package/dist/mcp/tools/index.d.ts +15 -0
  79. package/dist/mcp/tools/index.js +29 -0
  80. package/dist/mcp/tools/items.d.ts +87 -0
  81. package/dist/mcp/tools/items.js +141 -0
  82. package/dist/mcp/tools/operations.d.ts +171 -0
  83. package/dist/mcp/tools/operations.js +77 -0
  84. package/dist/mcp/tools/prompts/assets.md +8 -0
  85. package/dist/mcp/tools/prompts/collections.md +336 -0
  86. package/dist/mcp/tools/prompts/fields.md +521 -0
  87. package/dist/mcp/tools/prompts/files.md +180 -0
  88. package/dist/mcp/tools/prompts/flows.md +495 -0
  89. package/dist/mcp/tools/prompts/folders.md +34 -0
  90. package/dist/mcp/tools/prompts/index.d.ts +16 -0
  91. package/dist/mcp/tools/prompts/index.js +19 -0
  92. package/dist/mcp/tools/prompts/items.md +317 -0
  93. package/dist/mcp/tools/prompts/operations.md +721 -0
  94. package/dist/mcp/tools/prompts/relations.md +386 -0
  95. package/dist/mcp/tools/prompts/schema.md +130 -0
  96. package/dist/mcp/tools/prompts/system-prompt-description.md +1 -0
  97. package/dist/mcp/tools/prompts/system-prompt.md +44 -0
  98. package/dist/mcp/tools/prompts/trigger-flow.md +214 -0
  99. package/dist/mcp/tools/relations.d.ts +73 -0
  100. package/dist/mcp/tools/relations.js +93 -0
  101. package/dist/mcp/tools/schema.d.ts +54 -0
  102. package/dist/mcp/tools/schema.js +317 -0
  103. package/dist/mcp/tools/system.d.ts +3 -0
  104. package/dist/mcp/tools/system.js +22 -0
  105. package/dist/mcp/tools/trigger-flow.d.ts +8 -0
  106. package/dist/mcp/tools/trigger-flow.js +48 -0
  107. package/dist/mcp/transport.d.ts +13 -0
  108. package/dist/mcp/transport.js +18 -0
  109. package/dist/mcp/types.d.ts +56 -0
  110. package/dist/mcp/types.js +1 -0
  111. package/dist/metrics/lib/create-metrics.js +16 -25
  112. package/dist/middleware/collection-exists.js +2 -2
  113. package/dist/operations/mail/index.js +3 -1
  114. package/dist/operations/mail/rate-limiter.d.ts +1 -0
  115. package/dist/operations/mail/rate-limiter.js +29 -0
  116. package/dist/permissions/modules/process-payload/process-payload.js +3 -10
  117. package/dist/permissions/modules/validate-access/validate-access.js +2 -3
  118. package/dist/schedules/metrics.js +6 -2
  119. package/dist/schedules/project.d.ts +4 -0
  120. package/dist/schedules/project.js +27 -0
  121. package/dist/services/authentication.js +36 -0
  122. package/dist/services/collections.d.ts +3 -3
  123. package/dist/services/collections.js +16 -1
  124. package/dist/services/fields.d.ts +21 -5
  125. package/dist/services/fields.js +109 -32
  126. package/dist/services/graphql/resolvers/query.js +1 -1
  127. package/dist/services/graphql/resolvers/system-admin.js +49 -5
  128. package/dist/services/graphql/schema/parse-query.js +8 -8
  129. package/dist/services/graphql/utils/aggregate-query.d.ts +1 -1
  130. package/dist/services/graphql/utils/aggregate-query.js +5 -1
  131. package/dist/services/graphql/utils/filter-replace-m2a.js +2 -1
  132. package/dist/services/import-export.d.ts +9 -1
  133. package/dist/services/import-export.js +287 -101
  134. package/dist/services/items.d.ts +1 -1
  135. package/dist/services/items.js +50 -24
  136. package/dist/services/mail/index.js +2 -0
  137. package/dist/services/mail/rate-limiter.d.ts +1 -0
  138. package/dist/services/mail/rate-limiter.js +29 -0
  139. package/dist/services/meta.js +28 -24
  140. package/dist/services/payload.d.ts +7 -3
  141. package/dist/services/payload.js +26 -12
  142. package/dist/services/schema.js +4 -1
  143. package/dist/services/server.d.ts +1 -0
  144. package/dist/services/server.js +15 -18
  145. package/dist/services/settings.d.ts +2 -1
  146. package/dist/services/settings.js +15 -0
  147. package/dist/services/tfa.d.ts +1 -1
  148. package/dist/services/tfa.js +20 -5
  149. package/dist/services/tus/server.js +14 -9
  150. package/dist/services/versions.d.ts +6 -4
  151. package/dist/services/versions.js +84 -25
  152. package/dist/telemetry/lib/get-report.js +4 -4
  153. package/dist/telemetry/lib/send-report.d.ts +6 -1
  154. package/dist/telemetry/lib/send-report.js +3 -1
  155. package/dist/telemetry/types/report.d.ts +17 -1
  156. package/dist/telemetry/utils/get-settings.d.ts +9 -0
  157. package/dist/telemetry/utils/get-settings.js +14 -0
  158. package/dist/test-utils/README.md +760 -0
  159. package/dist/test-utils/cache.d.ts +51 -0
  160. package/dist/test-utils/cache.js +59 -0
  161. package/dist/test-utils/database.d.ts +48 -0
  162. package/dist/test-utils/database.js +52 -0
  163. package/dist/test-utils/emitter.d.ts +35 -0
  164. package/dist/test-utils/emitter.js +38 -0
  165. package/dist/test-utils/fields-service.d.ts +28 -0
  166. package/dist/test-utils/fields-service.js +36 -0
  167. package/dist/test-utils/items-service.d.ts +23 -0
  168. package/dist/test-utils/items-service.js +37 -0
  169. package/dist/test-utils/knex.d.ts +164 -0
  170. package/dist/test-utils/knex.js +268 -0
  171. package/dist/test-utils/schema.d.ts +26 -0
  172. package/dist/test-utils/schema.js +35 -0
  173. package/dist/types/auth.d.ts +2 -3
  174. package/dist/utils/apply-diff.js +15 -0
  175. package/dist/utils/create-admin.d.ts +11 -0
  176. package/dist/utils/create-admin.js +50 -0
  177. package/dist/utils/get-schema.js +5 -3
  178. package/dist/utils/get-snapshot-diff.js +49 -5
  179. package/dist/utils/get-snapshot.js +13 -7
  180. package/dist/utils/sanitize-schema.d.ts +11 -4
  181. package/dist/utils/sanitize-schema.js +9 -6
  182. package/dist/utils/schedule.js +15 -19
  183. package/dist/utils/validate-diff.js +31 -0
  184. package/dist/utils/validate-snapshot.js +7 -0
  185. package/dist/utils/versioning/deep-map-with-schema.d.ts +23 -0
  186. package/dist/utils/versioning/deep-map-with-schema.js +81 -0
  187. package/dist/utils/versioning/handle-version.d.ts +2 -2
  188. package/dist/utils/versioning/handle-version.js +47 -43
  189. package/dist/utils/versioning/split-recursive.d.ts +4 -0
  190. package/dist/utils/versioning/split-recursive.js +27 -0
  191. package/dist/websocket/controllers/hooks.js +12 -20
  192. package/dist/websocket/messages.d.ts +3 -3
  193. package/package.json +65 -66
  194. package/dist/cli/utils/defaults.d.ts +0 -4
  195. package/dist/cli/utils/defaults.js +0 -17
  196. package/dist/telemetry/utils/get-project-id.d.ts +0 -2
  197. package/dist/telemetry/utils/get-project-id.js +0 -4
@@ -6,8 +6,6 @@ import { pick } from 'lodash-es';
6
6
  * @returns sanitized collection
7
7
  */
8
8
  export function sanitizeCollection(collection) {
9
- if (!collection)
10
- return collection;
11
9
  return pick(collection, ['collection', 'fields', 'meta', 'schema.name']);
12
10
  }
13
11
  /**
@@ -18,8 +16,6 @@ export function sanitizeCollection(collection) {
18
16
  * @returns sanitized field
19
17
  */
20
18
  export function sanitizeField(field, sanitizeAllSchema = false) {
21
- if (!field)
22
- return field;
23
19
  const defaultPaths = ['collection', 'field', 'type', 'meta', 'name', 'children'];
24
20
  const pickedPaths = sanitizeAllSchema
25
21
  ? defaultPaths
@@ -71,8 +67,6 @@ export function sanitizeColumn(column) {
71
67
  * @returns sanitized relation
72
68
  */
73
69
  export function sanitizeRelation(relation) {
74
- if (!relation)
75
- return relation;
76
70
  return pick(relation, [
77
71
  'collection',
78
72
  'field',
@@ -87,3 +81,12 @@ export function sanitizeRelation(relation) {
87
81
  'schema.on_delete',
88
82
  ]);
89
83
  }
84
+ /**
85
+ * Pick certain specific system field properties that should be compared when performing diff
86
+ *
87
+ * @param field field to sanitize
88
+ * @returns sanitized system field
89
+ */
90
+ export function sanitizeSystemField(field) {
91
+ return pick(field, ['collection', 'field', 'schema.is_indexed']);
92
+ }
@@ -1,29 +1,25 @@
1
- import cron from 'cron-parser';
2
- import schedule from 'node-schedule';
1
+ import { CronJob, validateCronExpression } from 'cron';
3
2
  import { SynchronizedClock } from '../synchronization.js';
4
3
  export function validateCron(rule) {
5
- try {
6
- cron.parseExpression(rule);
7
- }
8
- catch {
9
- return false;
10
- }
11
- return true;
4
+ return validateCronExpression(rule).valid;
12
5
  }
13
6
  export function scheduleSynchronizedJob(id, rule, cb) {
14
7
  const clock = new SynchronizedClock(`${id}:${rule}`);
15
- const job = schedule.scheduleJob(rule, async (fireDate) => {
16
- const nextInvocation = job.nextInvocation();
17
- if (!nextInvocation)
18
- return;
19
- const nextTimestamp = nextInvocation.getTime();
20
- const wasSet = await clock.set(nextTimestamp);
21
- if (wasSet) {
22
- await cb(fireDate);
23
- }
8
+ const job = CronJob.from({
9
+ cronTime: rule,
10
+ onTick: async (fireDate) => {
11
+ // Get next execution time for synchronization
12
+ const nextDate = job.nextDate();
13
+ const nextTimestamp = nextDate.toMillis();
14
+ const wasSet = await clock.set(nextTimestamp);
15
+ if (wasSet) {
16
+ await cb(fireDate);
17
+ }
18
+ },
19
+ start: true,
24
20
  });
25
21
  const stop = async () => {
26
- job.cancel();
22
+ job.stop();
27
23
  await clock.reset();
28
24
  };
29
25
  return { stop };
@@ -31,6 +31,13 @@ const applyJoiSchema = Joi.object({
31
31
  diff: Joi.array().items(deepDiffSchema).required(),
32
32
  }))
33
33
  .required(),
34
+ systemFields: Joi.array()
35
+ .items(Joi.object({
36
+ collection: Joi.string().required(),
37
+ field: Joi.string().required(),
38
+ diff: Joi.array().items(deepDiffSchema).required(),
39
+ }))
40
+ .required(),
34
41
  relations: Joi.array()
35
42
  .items(Joi.object({
36
43
  collection: Joi.string().required(),
@@ -53,6 +60,7 @@ export function validateApplyDiff(applyDiff, currentSnapshotWithHash) {
53
60
  // No changes to apply
54
61
  if (applyDiff.diff.collections.length === 0 &&
55
62
  applyDiff.diff.fields.length === 0 &&
63
+ applyDiff.diff.systemFields.length === 0 &&
56
64
  applyDiff.diff.relations.length === 0) {
57
65
  return false;
58
66
  }
@@ -97,6 +105,29 @@ export function validateApplyDiff(applyDiff, currentSnapshotWithHash) {
97
105
  }
98
106
  }
99
107
  }
108
+ for (const diffSystemField of applyDiff.diff.systemFields) {
109
+ const systemField = `${diffSystemField.collection}.${diffSystemField.field}`;
110
+ if (!diffSystemField.diff[0])
111
+ continue;
112
+ if (diffSystemField.diff[0].kind !== DiffKind.EDIT) {
113
+ let action = 'update array'; // DiffKind.ARRAY
114
+ if (diffSystemField.diff[0].kind === DiffKind.NEW) {
115
+ action = 'create';
116
+ }
117
+ else if (diffSystemField.diff[0].kind === DiffKind.DELETE) {
118
+ action = 'delete';
119
+ }
120
+ throw new InvalidPayloadError({
121
+ reason: `Provided diff is trying to ${action} field "${systemField}" but this action is not supported. Please generate a new diff and try again`,
122
+ });
123
+ }
124
+ const pathString = diffSystemField.diff[0].path?.join('.') ?? '';
125
+ if (pathString.length === 0 || pathString !== 'schema.is_indexed') {
126
+ throw new InvalidPayloadError({
127
+ reason: `Provided diff is trying to alter property "${pathString}" on "${systemField}" but currently only "schema.is_indexed" is supported. Please generate a new diff and try again`,
128
+ });
129
+ }
130
+ }
100
131
  for (const diffRelation of applyDiff.diff.relations) {
101
132
  let relation = `${diffRelation.collection}.${diffRelation.field}`;
102
133
  if (diffRelation.related_collection)
@@ -33,6 +33,13 @@ const snapshotJoiSchema = Joi.object({
33
33
  .valid(...TYPES, ...ALIAS_TYPES)
34
34
  .allow(null),
35
35
  })),
36
+ systemFields: Joi.array().items(Joi.object({
37
+ collection: Joi.string(),
38
+ field: Joi.string(),
39
+ schema: Joi.object({
40
+ is_indexed: Joi.bool(),
41
+ }),
42
+ })),
36
43
  relations: Joi.array().items(Joi.object({
37
44
  collection: Joi.string(),
38
45
  field: Joi.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>;
@@ -1,7 +1,7 @@
1
1
  import { ForbiddenError } from '@directus/errors';
2
- import { deepMapResponse } from '../deep-map-response.js';
3
2
  import { transaction } from '../transaction.js';
4
- import { mergeVersionsRaw } from './merge-version-data.js';
3
+ import { deepMapWithSchema } from './deep-map-with-schema.js';
4
+ import { splitRecursive } from './split-recursive.js';
5
5
  export async function handleVersion(self, key, queryWithKey, opts) {
6
6
  const { VersionsService } = await import('../../services/versions.js');
7
7
  const { ItemsService } = await import('../../services/items.js');
@@ -15,19 +15,21 @@ export async function handleVersion(self, key, queryWithKey, opts) {
15
15
  accountability: self.accountability,
16
16
  knex: self.knex,
17
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)];
18
+ const version = await versionsService.getVersionSave(queryWithKey.version, self.collection, key);
19
+ return Object.assign(originalData[0], version?.delta);
22
20
  }
23
- let results = [];
21
+ let result;
24
22
  const versionsService = new VersionsService({
25
23
  schema: self.schema,
26
24
  accountability: self.accountability,
27
25
  knex: self.knex,
28
26
  });
29
27
  const createdIDs = {};
30
- const versionData = await versionsService.getVersionSaves(queryWithKey.version, self.collection, key);
28
+ const version = await versionsService.getVersionSave(queryWithKey.version, self.collection, key, false);
29
+ if (!version) {
30
+ throw new ForbiddenError();
31
+ }
32
+ const { delta } = version;
31
33
  await transaction(self.knex, async (trx) => {
32
34
  const itemsServiceAdmin = new ItemsService(self.collection, {
33
35
  schema: self.schema,
@@ -36,57 +38,59 @@ export async function handleVersion(self, key, queryWithKey, opts) {
36
38
  },
37
39
  knex: trx,
38
40
  });
39
- await Promise.all((versionData ?? []).map((data) => {
40
- return itemsServiceAdmin.updateOne(key, data, {
41
+ if (delta) {
42
+ const { rawDelta, defaultOverwrites } = splitRecursive(delta);
43
+ await itemsServiceAdmin.updateOne(key, rawDelta, {
41
44
  emitEvents: false,
42
45
  autoPurgeCache: false,
43
46
  skipTracking: true,
47
+ overwriteDefaults: defaultOverwrites,
44
48
  onItemCreate: (collection, pk) => {
45
49
  if (collection in createdIDs === false)
46
50
  createdIDs[collection] = [];
47
51
  createdIDs[collection].push(pk);
48
52
  },
49
53
  });
50
- }));
54
+ }
51
55
  const itemsServiceUser = new ItemsService(self.collection, {
52
56
  schema: self.schema,
53
57
  accountability: self.accountability,
54
58
  knex: trx,
55
59
  });
56
- results = await itemsServiceUser.readByQuery(queryWithKey, opts);
60
+ result = (await itemsServiceUser.readByQuery(queryWithKey, opts))[0];
57
61
  await trx.rollback();
58
62
  });
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
- ];
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];
80
72
  }
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
- }
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];
87
92
  }
88
- return [key, value];
89
- }, { collection: self.collection, schema: self.schema });
90
- });
91
- return results;
93
+ }
94
+ return [key, value];
95
+ }, { collection: self.collection, schema: self.schema });
92
96
  }
@@ -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
+ }
@@ -1,3 +1,4 @@
1
+ import { systemCollectionNames } from '@directus/system-data';
1
2
  import { useBus } from '../../bus/index.js';
2
3
  import emitter from '../../emitter.js';
3
4
  let actionsRegistered = false;
@@ -7,26 +8,17 @@ export function registerWebSocketEvents() {
7
8
  actionsRegistered = true;
8
9
  registerActionHooks([
9
10
  'items',
10
- 'access',
11
- 'activity',
12
- 'collections',
13
- 'dashboards',
14
- 'flows',
15
- 'folders',
16
- 'notifications',
17
- 'operations',
18
- 'panels',
19
- 'permissions',
20
- 'policies',
21
- 'presets',
22
- 'revisions',
23
- 'roles',
24
- 'settings',
25
- 'shares',
26
- 'translations',
27
- 'users',
28
- 'versions',
29
- 'webhooks',
11
+ ...systemCollectionNames
12
+ .filter((collection) => ![
13
+ 'directus_migrations',
14
+ // manually registered below
15
+ 'directus_fields',
16
+ 'directus_relations',
17
+ 'directus_files',
18
+ // excluded for security reasons
19
+ 'directus_sessions',
20
+ ].includes(collection))
21
+ .map((collection) => collection.replace('directus_', '')),
30
22
  ]);
31
23
  registerFieldsHooks();
32
24
  registerFilesHooks();
@@ -12,7 +12,7 @@ export declare const WebSocketResponse: z.ZodDiscriminatedUnion<[z.ZodObject<{
12
12
  code: z.ZodString;
13
13
  message: z.ZodString;
14
14
  }, z.core.$loose>;
15
- }, z.core.$loose>]>;
15
+ }, z.core.$loose>], "status">;
16
16
  export type WebSocketResponse = z.infer<typeof WebSocketResponse>;
17
17
  export declare const ConnectionParams: z.ZodObject<{
18
18
  access_token: z.ZodOptional<z.ZodString>;
@@ -49,7 +49,7 @@ export declare const WebSocketSubscribeMessage: z.ZodDiscriminatedUnion<[z.ZodOb
49
49
  }, z.core.$loose>, z.ZodObject<{
50
50
  uid: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
51
51
  type: z.ZodLiteral<"unsubscribe">;
52
- }, z.core.$loose>]>;
52
+ }, z.core.$loose>], "type">;
53
53
  export type WebSocketSubscribeMessage = z.infer<typeof WebSocketSubscribeMessage>;
54
54
  export declare const WebSocketLogsMessage: z.ZodUnion<readonly [z.ZodObject<{
55
55
  type: z.ZodLiteral<"subscribe">;
@@ -108,7 +108,7 @@ export declare const WebSocketEvent: z.ZodDiscriminatedUnion<[z.ZodObject<{
108
108
  collection: z.ZodString;
109
109
  payload: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
110
110
  keys: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
111
- }, z.core.$strip>]>;
111
+ }, z.core.$strip>], "action">;
112
112
  export type WebSocketEvent = z.infer<typeof WebSocketEvent>;
113
113
  export declare const AuthMode: z.ZodUnion<readonly [z.ZodLiteral<"public">, z.ZodLiteral<"handshake">, z.ZodLiteral<"strict">]>;
114
114
  export type AuthMode = z.infer<typeof AuthMode>;