@directus/api 35.2.0 → 36.0.0-rc.1

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 (192) hide show
  1. package/dist/ai/chat/models/chat-request.js +48 -48
  2. package/dist/ai/chat/models/object-request.js +6 -6
  3. package/dist/ai/chat/models/providers.js +14 -14
  4. package/dist/ai/chat/utils/parse-json-schema-7.js +22 -22
  5. package/dist/ai/mcp/server.js +44 -6
  6. package/dist/ai/mcp/utils.js +31 -0
  7. package/dist/ai/tools/assets/index.js +3 -3
  8. package/dist/ai/tools/collections/index.js +18 -18
  9. package/dist/ai/tools/fields/index.js +18 -18
  10. package/dist/ai/tools/files/index.js +18 -18
  11. package/dist/ai/tools/flows/index.js +16 -16
  12. package/dist/ai/tools/folders/index.js +18 -18
  13. package/dist/ai/tools/items/index.js +17 -17
  14. package/dist/ai/tools/operations/index.js +16 -16
  15. package/dist/ai/tools/relations/index.js +22 -22
  16. package/dist/ai/tools/schema/index.js +3 -3
  17. package/dist/ai/tools/schema.js +159 -159
  18. package/dist/ai/tools/system/index.js +3 -3
  19. package/dist/ai/tools/trigger-flow/index.js +3 -3
  20. package/dist/app.js +35 -11
  21. package/dist/auth/drivers/ldap.js +3 -1
  22. package/dist/auth/drivers/local.js +2 -0
  23. package/dist/auth/drivers/oauth2.js +3 -1
  24. package/dist/auth/drivers/openid.js +3 -1
  25. package/dist/auth/drivers/saml.js +2 -0
  26. package/dist/auth/utils/check-local-disabled.js +16 -0
  27. package/dist/auth/utils/check-sso-enabled.js +14 -0
  28. package/dist/auth.js +8 -5
  29. package/dist/cli/commands/bootstrap/index.js +3 -0
  30. package/dist/cli/commands/cache/clear.js +6 -1
  31. package/dist/cli/commands/roles/create.js +4 -1
  32. package/dist/cli/commands/users/create.js +3 -0
  33. package/dist/constants.js +8 -1
  34. package/dist/controllers/access.js +1 -1
  35. package/dist/controllers/activity.js +2 -1
  36. package/dist/controllers/assets.js +2 -0
  37. package/dist/controllers/auth.js +13 -5
  38. package/dist/controllers/collections.js +1 -1
  39. package/dist/controllers/comments.js +1 -1
  40. package/dist/controllers/dashboards.js +1 -1
  41. package/dist/controllers/fields.js +1 -1
  42. package/dist/controllers/files.js +3 -1
  43. package/dist/controllers/flows.js +6 -5
  44. package/dist/controllers/folders.js +1 -1
  45. package/dist/controllers/graphql.js +2 -0
  46. package/dist/controllers/items.js +3 -1
  47. package/dist/controllers/license.js +119 -0
  48. package/dist/controllers/mcp/index.js +38 -0
  49. package/dist/controllers/mcp/oauth-clients.js +68 -0
  50. package/dist/controllers/mcp/oauth-consent-page.js +316 -0
  51. package/dist/controllers/mcp/oauth.js +381 -0
  52. package/dist/controllers/mcp/templates/oauth-consent.liquid +62 -0
  53. package/dist/controllers/mcp/templates/oauth-error.liquid +28 -0
  54. package/dist/controllers/notifications.js +1 -1
  55. package/dist/controllers/operations.js +1 -1
  56. package/dist/controllers/panels.js +1 -1
  57. package/dist/controllers/permissions.js +1 -1
  58. package/dist/controllers/policies.js +1 -1
  59. package/dist/controllers/presets.js +1 -1
  60. package/dist/controllers/revisions.js +3 -2
  61. package/dist/controllers/roles.js +1 -1
  62. package/dist/controllers/server.js +38 -10
  63. package/dist/controllers/shares.js +1 -1
  64. package/dist/controllers/translations.js +1 -1
  65. package/dist/controllers/users.js +1 -1
  66. package/dist/controllers/utils.js +2 -2
  67. package/dist/controllers/versions.js +12 -5
  68. package/dist/database/get-ast-from-query/lib/convert-wildcards.js +10 -1
  69. package/dist/database/get-ast-from-query/lib/parse-fields.js +2 -1
  70. package/dist/database/helpers/fn/dialects/mysql.js +7 -12
  71. package/dist/database/helpers/fn/dialects/oracle.js +3 -4
  72. package/dist/database/helpers/fn/dialects/postgres.js +4 -26
  73. package/dist/database/helpers/fn/json/mysql-json-path.js +22 -0
  74. package/dist/database/helpers/fn/json/parse-function.js +14 -6
  75. package/dist/database/helpers/fn/json/postgres-json-path.js +54 -0
  76. package/dist/database/migrations/20260110A-add-ai-provider-settings.js +4 -4
  77. package/dist/database/migrations/20260217A-null-item-versions.js +14 -0
  78. package/dist/database/migrations/20260312A-add-ai-translation-settings.js +18 -0
  79. package/dist/database/migrations/20260507A-add-licensing.js +22 -0
  80. package/dist/database/migrations/20260512A-add-autosave-revision-interval.js +14 -0
  81. package/dist/database/migrations/20260512B-add-mcp-oauth.js +87 -0
  82. package/dist/database/run-ast/lib/apply-query/filter/operator.js +116 -33
  83. package/dist/database/run-ast/lib/apply-query/index.js +4 -1
  84. package/dist/database/run-ast/lib/apply-query/sort.js +17 -7
  85. package/dist/database/run-ast/lib/get-db-query.js +21 -9
  86. package/dist/database/run-ast/lib/parse-current-level.js +2 -1
  87. package/dist/database/run-ast/run-ast.js +2 -1
  88. package/dist/database/run-ast/utils/get-column.js +2 -1
  89. package/dist/database/run-ast/utils/merge-with-parent-items.js +5 -3
  90. package/dist/extensions/lib/installation/manager.js +1 -1
  91. package/dist/extensions/lib/sandbox/register/operation.js +1 -1
  92. package/dist/extensions/lib/sync/sync.js +1 -1
  93. package/dist/extensions/manager.js +3 -3
  94. package/dist/flows.js +5 -5
  95. package/dist/license/entitlements/lib/collections.js +37 -0
  96. package/dist/license/entitlements/lib/custom-llms-enabled.js +18 -0
  97. package/dist/license/entitlements/lib/custom-permission-rules-enabled.js +41 -0
  98. package/dist/license/entitlements/lib/flows.js +29 -0
  99. package/dist/license/entitlements/lib/seats.js +103 -0
  100. package/dist/license/entitlements/lib/sso-enabled.js +45 -0
  101. package/dist/license/entitlements/manager.js +256 -0
  102. package/dist/license/index.js +4 -0
  103. package/dist/license/manager.js +505 -0
  104. package/dist/license/utils/compute-license-status.js +27 -0
  105. package/dist/license/utils/get-core-grace-expires-at.js +38 -0
  106. package/dist/license/utils/get-license-key.js +23 -0
  107. package/dist/license/utils/get-license-token.js +23 -0
  108. package/dist/license/utils/handle-license-error.js +41 -0
  109. package/dist/license/utils/is-in-core-grace-period.js +11 -0
  110. package/dist/license/utils/is-sso-bypass-allowed.js +21 -0
  111. package/dist/license/utils/use-rpc.js +33 -0
  112. package/dist/middleware/cache.js +4 -1
  113. package/dist/middleware/error-handler.js +11 -0
  114. package/dist/middleware/extract-token.js +11 -2
  115. package/dist/middleware/is-admin.js +16 -0
  116. package/dist/middleware/is-locked.js +16 -0
  117. package/dist/middleware/mcp-oauth-guard.js +23 -0
  118. package/dist/middleware/request-counter.js +5 -2
  119. package/dist/packages/types/dist/index.js +117 -122
  120. package/dist/permissions/modules/process-ast/utils/extract-paths-from-query.js +10 -1
  121. package/dist/permissions/utils/get-unaliased-field-key.js +2 -1
  122. package/dist/request/is-denied-ip.js +2 -0
  123. package/dist/schedules/license.js +31 -0
  124. package/dist/schedules/oauth-cleanup.js +26 -0
  125. package/dist/schedules/retention.js +1 -1
  126. package/dist/schedules/telemetry.js +4 -1
  127. package/dist/schedules/tus.js +1 -1
  128. package/dist/schedules/utils/duration-to-cron.js +36 -0
  129. package/dist/services/activity.js +15 -0
  130. package/dist/services/authentication.js +12 -5
  131. package/dist/services/collections.js +40 -10
  132. package/dist/services/fields.js +6 -6
  133. package/dist/services/flows.js +12 -0
  134. package/dist/services/graphql/resolvers/system-admin.js +2 -2
  135. package/dist/services/graphql/resolvers/system-global.js +1 -1
  136. package/dist/services/graphql/resolvers/system.js +43 -27
  137. package/dist/services/graphql/schema/get-types.js +28 -7
  138. package/dist/services/graphql/schema/parse-query.js +8 -0
  139. package/dist/services/graphql/schema/read.js +12 -0
  140. package/dist/services/graphql/types/json-filter.js +30 -0
  141. package/dist/services/index.js +6 -6
  142. package/dist/services/items.js +32 -14
  143. package/dist/services/mcp-oauth/cimd.js +307 -0
  144. package/dist/services/mcp-oauth/index.js +1185 -0
  145. package/dist/services/mcp-oauth/types/error.js +22 -0
  146. package/dist/services/mcp-oauth/utils/cimd-egress.js +182 -0
  147. package/dist/services/mcp-oauth/utils/domain.js +21 -0
  148. package/dist/services/mcp-oauth/utils/loopback.js +11 -0
  149. package/dist/services/mcp-oauth/utils/redirect.js +84 -0
  150. package/dist/services/mcp-oauth/utils/registration-debug.js +131 -0
  151. package/dist/services/payload.js +2 -1
  152. package/dist/services/permissions.js +31 -9
  153. package/dist/services/revisions.js +15 -0
  154. package/dist/services/server.js +66 -68
  155. package/dist/services/settings.js +37 -3
  156. package/dist/services/users.js +23 -6
  157. package/dist/services/utils.js +6 -1
  158. package/dist/services/versions.js +160 -70
  159. package/dist/utils/calculate-field-depth.js +1 -0
  160. package/dist/utils/create-admin.js +3 -3
  161. package/dist/utils/deep-freeze.js +24 -0
  162. package/dist/utils/extract-function-name.js +13 -0
  163. package/dist/utils/generate-translations.js +5 -5
  164. package/dist/utils/get-accountability-for-token.js +13 -1
  165. package/dist/utils/get-cache-key.js +1 -1
  166. package/dist/utils/get-history-filter-query.js +22 -0
  167. package/dist/utils/get-schema.js +2 -2
  168. package/dist/utils/get-service.js +3 -3
  169. package/dist/utils/is-admin.js +9 -0
  170. package/dist/utils/is-unauthenticated.js +15 -0
  171. package/dist/utils/parse-oauth-scope.js +12 -0
  172. package/dist/utils/sanitize-query.js +2 -2
  173. package/dist/utils/split-field-path.js +29 -0
  174. package/dist/utils/store.js +1 -1
  175. package/dist/utils/transaction.js +2 -2
  176. package/dist/utils/translations-validation.js +2 -2
  177. package/dist/utils/validate-query.js +35 -4
  178. package/dist/utils/validate-user-count-integrity.js +28 -5
  179. package/dist/utils/verify-session-jwt.js +5 -2
  180. package/dist/utils/versioning/handle-version.js +131 -48
  181. package/dist/utils/versioning/remove-circular.js +17 -0
  182. package/dist/websocket/authenticate.js +2 -1
  183. package/dist/websocket/collab/collab.js +1 -1
  184. package/dist/websocket/collab/room.js +1 -1
  185. package/dist/websocket/controllers/base.js +12 -0
  186. package/dist/websocket/controllers/graphql.js +1 -1
  187. package/dist/websocket/handlers/subscribe.js +1 -1
  188. package/dist/websocket/messages.js +64 -64
  189. package/dist/websocket/utils/items.js +2 -2
  190. package/license +90 -80
  191. package/package.json +33 -32
  192. package/dist/controllers/mcp.js +0 -31
@@ -1,3 +1,4 @@
1
+ import "../packages/types/dist/index.js";
1
2
  import { getCache } from "../cache.js";
2
3
  import { getHelpers } from "../database/helpers/index.js";
3
4
  import emitter_default from "../emitter.js";
@@ -8,10 +9,10 @@ import { splitRecursive } from "../utils/versioning/split-recursive.js";
8
9
  import { ItemsService } from "./items.js";
9
10
  import { ActivityService } from "./activity.js";
10
11
  import { RevisionsService } from "./revisions.js";
11
- import { ForbiddenError, InvalidPayloadError, UnprocessableContentError } from "@directus/errors";
12
+ import { ForbiddenError, InvalidPayloadError, UnprocessableContentError, VersionHashMismatchError } from "@directus/errors";
12
13
  import { deepMapWithSchema } from "@directus/utils";
13
- import { assign, get, isEqual, isPlainObject, pick } from "lodash-es";
14
- import { Action } from "@directus/constants";
14
+ import { assign, get, isEqual, isNil, isPlainObject, pick } from "lodash-es";
15
+ import { Action, VERSION_KEY_DRAFT, isPublishedVersionKey } from "@directus/constants";
15
16
  import hash from "object-hash";
16
17
  import Joi from "joi";
17
18
 
@@ -21,16 +22,23 @@ var VersionsService = class VersionsService extends ItemsService {
21
22
  super("directus_versions", options);
22
23
  }
23
24
  async validateCreateData(data) {
24
- const { error } = Joi.object({
25
+ const versionCreateSchema = Joi.object({
25
26
  key: Joi.string().required(),
26
27
  name: Joi.string().allow(null),
27
28
  collection: Joi.string().required(),
28
- item: Joi.string().required()
29
- }).validate(data);
29
+ item: Joi.string().allow(null)
30
+ });
31
+ const itemLess = isNil(data["item"]);
32
+ const { error } = versionCreateSchema.validate(data);
30
33
  if (error) throw new InvalidPayloadError({ reason: error.message });
31
- if (data["key"] === "main") throw new InvalidPayloadError({ reason: `"main" is a reserved version key` });
34
+ if (isPublishedVersionKey(data["key"])) throw new InvalidPayloadError({ reason: `"${data["key"]}" is a reserved version key` });
35
+ if (itemLess && data["key"] !== VERSION_KEY_DRAFT) throw new InvalidPayloadError({ reason: `"key" must be "${VERSION_KEY_DRAFT}" for versions not linked to an item` });
32
36
  if (this.accountability) try {
33
- await validateAccess({
37
+ await validateAccess(itemLess ? {
38
+ accountability: this.accountability,
39
+ action: "read",
40
+ collection: data["collection"]
41
+ } : {
34
42
  accountability: this.accountability,
35
43
  action: "read",
36
44
  collection: data["collection"],
@@ -47,6 +55,9 @@ var VersionsService = class VersionsService extends ItemsService {
47
55
  knex: this.knex,
48
56
  schema: this.schema
49
57
  }).readOne(data["collection"])).meta?.versioning) throw new UnprocessableContentError({ reason: `Content Versioning is not enabled for collection "${data["collection"]}"` });
58
+ const isSingleton = !!this.schema.collections[data["collection"]]?.singleton;
59
+ if (itemLess) if (isSingleton) await this.assertSingletonEmpty(data["collection"]);
60
+ else return;
50
61
  if ((await new VersionsService({
51
62
  knex: this.knex,
52
63
  schema: this.schema
@@ -55,9 +66,9 @@ var VersionsService = class VersionsService extends ItemsService {
55
66
  filter: {
56
67
  key: { _eq: data["key"] },
57
68
  collection: { _eq: data["collection"] },
58
- item: { _eq: data["item"] }
69
+ item: itemLess ? { _null: true } : { _eq: data["item"] }
59
70
  }
60
- }))[0]["count"] > 0) throw new UnprocessableContentError({ reason: `Version "${data["key"]}" already exists for item "${data["item"]}" in collection "${data["collection"]}"` });
71
+ }))[0]["count"] > 0) throw new UnprocessableContentError({ reason: itemLess ? `Singleton collection "${data["collection"]}" already has an item-less version` : `Version "${data["key"]}" already exists for item "${data["item"]}" in collection "${data["collection"]}"` });
61
72
  }
62
73
  async getMainItem(collection, item, query) {
63
74
  return await new ItemsService(collection, {
@@ -73,18 +84,27 @@ var VersionsService = class VersionsService extends ItemsService {
73
84
  mainHash
74
85
  };
75
86
  }
76
- async getVersionSave(key, collection, item, mapDelta = true) {
77
- const version = (await this.readByQuery({ filter: {
78
- key: { _eq: key },
79
- collection: { _eq: collection },
80
- item: { _eq: item }
81
- } }))[0];
82
- if (mapDelta && version?.delta) version.delta = this.mapDelta(version);
83
- return version;
87
+ async getVersionSaves(key, collection, item, mapDelta = true) {
88
+ let itemFilter = {};
89
+ if (item) itemFilter = { item: { _eq: item } };
90
+ let versions = await this.readByQuery({
91
+ filter: {
92
+ key: { _eq: key },
93
+ collection: { _eq: collection },
94
+ ...itemFilter
95
+ },
96
+ limit: -1
97
+ });
98
+ if (mapDelta) versions = versions.map((version) => {
99
+ if (version.delta) version.delta = this.mapDelta(version);
100
+ return version;
101
+ });
102
+ return versions;
84
103
  }
85
104
  async createOne(data, opts) {
86
105
  await this.validateCreateData(data);
87
- data["hash"] = hash(await this.getMainItem(data["collection"], data["item"]));
106
+ if (data["item"]) data["hash"] = hash(await this.getMainItem(data["collection"], data["item"]));
107
+ else data["hash"] = null;
88
108
  return super.createOne(data, opts);
89
109
  }
90
110
  async readOne(key, query = {}, opts) {
@@ -96,6 +116,7 @@ var VersionsService = class VersionsService extends ItemsService {
96
116
  if (!Array.isArray(data)) throw new InvalidPayloadError({ reason: "Input should be an array of items" });
97
117
  const keyCombos = /* @__PURE__ */ new Set();
98
118
  for (const item of data) {
119
+ if (isNil(item["item"])) continue;
99
120
  const keyCombo = `${item["key"]}-${item["collection"]}-${item["item"]}`;
100
121
  if (keyCombos.has(keyCombo)) throw new UnprocessableContentError({ reason: `Cannot create multiple versions on "${item["item"]}" in collection "${item["collection"]}" with the same key "${item["key"]}"` });
101
122
  keyCombos.add(keyCombo);
@@ -105,31 +126,52 @@ var VersionsService = class VersionsService extends ItemsService {
105
126
  async updateMany(keys, data, opts) {
106
127
  const { error } = Joi.object({
107
128
  key: Joi.string(),
108
- name: Joi.string().allow(null)
129
+ name: Joi.string().allow(null),
130
+ item: Joi.string().allow(null)
109
131
  }).validate(data);
110
132
  if (error) throw new InvalidPayloadError({ reason: error.message });
111
- if ("key" in data) {
112
- if (data["key"] === "main") throw new InvalidPayloadError({ reason: `"main" is a reserved version key` });
113
- const keyCombos = /* @__PURE__ */ new Set();
114
- for (const pk of keys) {
115
- const { collection, item } = await this.readOne(pk, { fields: ["collection", "item"] });
116
- const keyCombo = `${data["key"]}-${collection}-${item}`;
117
- if (keyCombos.has(keyCombo)) throw new UnprocessableContentError({ reason: `Cannot update multiple versions on "${item}" in collection "${collection}" to the same key "${data["key"]}"` });
118
- keyCombos.add(keyCombo);
119
- if ((await super.readByQuery({
120
- aggregate: { count: ["*"] },
121
- filter: {
122
- id: { _neq: pk },
123
- key: { _eq: data["key"] },
124
- collection: { _eq: collection },
125
- item: { _eq: item }
126
- }
127
- }))[0]["count"] > 0) throw new UnprocessableContentError({ reason: `Version "${data["key"]}" already exists for item "${item}" in collection "${collection}"` });
133
+ if (isPublishedVersionKey(data["key"])) throw new InvalidPayloadError({ reason: `"${data["key"]}" is a reserved version key` });
134
+ const keyCombos = /* @__PURE__ */ new Set();
135
+ for (const pk of keys) {
136
+ const existingVersion = await this.readOne(pk, { fields: [
137
+ "collection",
138
+ "item",
139
+ "key"
140
+ ] });
141
+ const collection = existingVersion.collection;
142
+ const item = "item" in data ? data["item"] : existingVersion.item;
143
+ const key = "key" in data ? data["key"] : existingVersion.key;
144
+ if (key !== VERSION_KEY_DRAFT && item === null) throw new InvalidPayloadError({ reason: `"key" must be "${VERSION_KEY_DRAFT}" for versions not linked to an item` });
145
+ if (item === null) {
146
+ if (this.schema.collections[collection]?.singleton) {
147
+ await this.assertSingletonEmpty(collection);
148
+ if ((await super.readByQuery({
149
+ aggregate: { count: ["*"] },
150
+ filter: {
151
+ id: { _neq: pk },
152
+ collection: { _eq: collection },
153
+ item: { _null: true }
154
+ }
155
+ }))[0]["count"] > 0) throw new UnprocessableContentError({ reason: `Singleton collection "${collection}" already has an item-less version` });
156
+ }
157
+ continue;
128
158
  }
159
+ const keyCombo = `${key}-${collection}-${item}`;
160
+ if (keyCombos.has(keyCombo)) throw new UnprocessableContentError({ reason: `Cannot update multiple versions on "${item}" in collection "${collection}" to the same key "${key}"` });
161
+ keyCombos.add(keyCombo);
162
+ if ((await super.readByQuery({
163
+ aggregate: { count: ["*"] },
164
+ filter: {
165
+ id: { _neq: pk },
166
+ key: { _eq: key },
167
+ collection: { _eq: collection },
168
+ item: { _eq: item }
169
+ }
170
+ }))[0]["count"] > 0) throw new UnprocessableContentError({ reason: `Version "${key}" already exists for item "${item}" in collection "${collection}"` });
129
171
  }
130
172
  return super.updateMany(keys, data, opts);
131
173
  }
132
- async save(key, delta) {
174
+ async save(key, delta, opts) {
133
175
  const version = await super.readOne(key);
134
176
  const payloadService = new PayloadService(this.collection, {
135
177
  accountability: this.accountability,
@@ -137,35 +179,65 @@ var VersionsService = class VersionsService extends ItemsService {
137
179
  schema: this.schema
138
180
  });
139
181
  const { item, collection, delta: existingDelta } = version;
140
- const helpers = getHelpers(this.knex);
141
182
  let revisionDelta = await payloadService.prepareDelta(delta);
142
- const trackingAccountability = this.schema.collections[collection]?.accountability ?? null;
143
- if (trackingAccountability !== null) {
144
- const activity = await new ActivityService({
145
- knex: this.knex,
146
- schema: this.schema
147
- }).createOne({
148
- action: Action.VERSION_SAVE,
149
- user: this.accountability?.user ?? null,
150
- collection,
151
- ip: this.accountability?.ip ?? null,
152
- user_agent: this.accountability?.userAgent ?? null,
153
- origin: this.accountability?.origin ?? null,
154
- item
155
- });
156
- if (trackingAccountability === "all") await new RevisionsService({
157
- knex: this.knex,
158
- schema: this.schema
159
- }).createOne({
160
- activity,
161
- version: key,
162
- collection,
163
- item,
164
- data: revisionDelta,
165
- delta: revisionDelta
166
- });
183
+ if (item) {
184
+ const trackingAccountability = this.schema.collections[collection]?.accountability ?? null;
185
+ if (trackingAccountability !== null) {
186
+ const revisionsService = new RevisionsService({
187
+ knex: this.knex,
188
+ schema: this.schema
189
+ });
190
+ let patchedExistingRevision = false;
191
+ if (opts?.patchRevision && trackingAccountability === "all") {
192
+ const [latestRevision] = await revisionsService.readByQuery({
193
+ filter: { version: { _eq: key } },
194
+ sort: ["-activity.timestamp"],
195
+ limit: 1,
196
+ fields: [
197
+ "id",
198
+ "data",
199
+ "delta",
200
+ "activity.user"
201
+ ]
202
+ });
203
+ const currentUser = this.accountability?.user ?? null;
204
+ const latestRevisionUser = (latestRevision?.["activity"])?.user ?? null;
205
+ if (latestRevision && latestRevisionUser === currentUser) {
206
+ const mergedRevisionData = assign({}, latestRevision["data"], revisionDelta);
207
+ const mergedRevisionDelta = assign({}, latestRevision["delta"], revisionDelta);
208
+ await revisionsService.updateOne(latestRevision["id"], {
209
+ data: mergedRevisionData,
210
+ delta: mergedRevisionDelta
211
+ });
212
+ patchedExistingRevision = true;
213
+ }
214
+ }
215
+ if (!patchedExistingRevision) {
216
+ const activity = await new ActivityService({
217
+ knex: this.knex,
218
+ schema: this.schema
219
+ }).createOne({
220
+ action: Action.VERSION_SAVE,
221
+ user: this.accountability?.user ?? null,
222
+ collection,
223
+ ip: this.accountability?.ip ?? null,
224
+ user_agent: this.accountability?.userAgent ?? null,
225
+ origin: this.accountability?.origin ?? null,
226
+ item
227
+ });
228
+ if (trackingAccountability === "all") await revisionsService.createOne({
229
+ activity,
230
+ version: key,
231
+ collection,
232
+ item,
233
+ data: revisionDelta,
234
+ delta: revisionDelta
235
+ });
236
+ }
237
+ }
167
238
  }
168
239
  revisionDelta = revisionDelta ? revisionDelta : null;
240
+ const helpers = getHelpers(this.knex);
169
241
  const date = new Date(helpers.date.writeTimestamp((/* @__PURE__ */ new Date()).toISOString()));
170
242
  deepMapObjects(revisionDelta, (object, path) => {
171
243
  const existing = get(existingDelta, path);
@@ -186,22 +258,29 @@ var VersionsService = class VersionsService extends ItemsService {
186
258
  if (shouldClearCache(cache, void 0, collection)) cache.clear();
187
259
  return finalVersionDelta;
188
260
  }
189
- async promote(version, mainHash, fields) {
261
+ async promote(version, opts) {
190
262
  const { collection, item, delta } = await super.readOne(version);
191
- if (this.accountability) await validateAccess({
263
+ if (item && typeof opts?.mainHash !== "string") throw new InvalidPayloadError({ reason: `"mainHash" field is required` });
264
+ if (this.accountability) await validateAccess(item ? {
192
265
  accountability: this.accountability,
193
266
  action: "update",
194
267
  collection,
195
268
  primaryKeys: [item]
269
+ } : {
270
+ accountability: this.accountability,
271
+ action: "create",
272
+ collection
196
273
  }, {
197
274
  schema: this.schema,
198
275
  knex: this.knex
199
276
  });
200
277
  if (!delta) throw new UnprocessableContentError({ reason: `No changes to promote` });
201
- const { outdated } = await this.verifyHash(collection, item, mainHash);
202
- if (outdated) throw new UnprocessableContentError({ reason: `Main item has changed since this version was last updated` });
278
+ if (item) {
279
+ const { outdated, mainHash } = await this.verifyHash(collection, item, opts?.mainHash);
280
+ if (outdated) throw new VersionHashMismatchError({ mainHash });
281
+ }
203
282
  const { rawDelta, defaultOverwrites } = splitRecursive(delta);
204
- const payloadToUpdate = fields ? pick(rawDelta, fields) : rawDelta;
283
+ const payloadToUpdate = opts?.fields ? pick(rawDelta, opts.fields) : rawDelta;
205
284
  const itemsService = new ItemsService(collection, {
206
285
  accountability: this.accountability,
207
286
  knex: this.knex,
@@ -216,7 +295,13 @@ var VersionsService = class VersionsService extends ItemsService {
216
295
  schema: this.schema,
217
296
  accountability: this.accountability
218
297
  });
219
- const updatedItemKey = await itemsService.updateOne(item, payloadAfterHooks, { overwriteDefaults: defaultOverwrites });
298
+ let updatedItemKey;
299
+ if (item) updatedItemKey = await itemsService.updateOne(item, payloadAfterHooks, { overwriteDefaults: defaultOverwrites });
300
+ else {
301
+ await this.assertSingletonEmpty(collection);
302
+ updatedItemKey = await itemsService.createOne(payloadAfterHooks, { overwriteDefaults: defaultOverwrites });
303
+ await this.updateOne(version, { item: String(updatedItemKey) });
304
+ }
220
305
  emitter_default.emitAction(["items.promote", `${collection}.items.promote`], {
221
306
  payload: payloadAfterHooks,
222
307
  collection,
@@ -229,6 +314,11 @@ var VersionsService = class VersionsService extends ItemsService {
229
314
  });
230
315
  return updatedItemKey;
231
316
  }
317
+ async assertSingletonEmpty(collection) {
318
+ const collectionMeta = this.schema.collections[collection];
319
+ if (!collectionMeta?.singleton) return;
320
+ if (await this.knex(collection).first(collectionMeta.primary)) throw new UnprocessableContentError({ reason: `Singleton collection "${collection}" already contains an item` });
321
+ }
232
322
  mapDelta(version) {
233
323
  const delta = version.delta ?? {};
234
324
  delta[this.schema.collections[version.collection].primary] = version.item;
@@ -39,6 +39,7 @@ function calculateFieldDepth(obj, dotNotationKeys = []) {
39
39
  const keys = Object.keys(obj);
40
40
  for (const key of keys) {
41
41
  const nestedValue = obj[key];
42
+ if (key === "_json") continue;
42
43
  if (dotNotationKeys.includes(key) && nestedValue) {
43
44
  let sortDepth = 0;
44
45
  for (const sortKey of nestedValue) if (sortKey) sortDepth = Math.max(sortKey.split(".").length, sortDepth);
@@ -27,6 +27,9 @@ const defaultAdminPolicy = {
27
27
  async function createAdmin(schema, admin) {
28
28
  const logger = useLogger();
29
29
  const env = useEnv();
30
+ const adminEmail = admin?.email ?? env["ADMIN_EMAIL"];
31
+ const adminPassword = admin?.password ?? env["ADMIN_PASSWORD"];
32
+ if (!adminEmail || !adminPassword) return;
30
33
  logger.info("Setting up first admin role...");
31
34
  const accessService = new AccessService({ schema });
32
35
  const policiesService = new PoliciesService({ schema });
@@ -37,9 +40,6 @@ async function createAdmin(schema, admin) {
37
40
  role
38
41
  });
39
42
  const usersService = new UsersService({ schema });
40
- const adminEmail = admin?.email ?? env["ADMIN_EMAIL"];
41
- const adminPassword = admin?.password ?? env["ADMIN_PASSWORD"];
42
- if (!adminEmail || !adminPassword) return;
43
43
  const token = env["ADMIN_TOKEN"] ?? null;
44
44
  logger.info("Adding first admin user...");
45
45
  await usersService.createOne({
@@ -0,0 +1,24 @@
1
+ import { isPlainObject } from "lodash-es";
2
+
3
+ //#region src/utils/deep-freeze.ts
4
+ /**
5
+ * Recursively freezes arrays and plain objects so the entire structure is immutable.
6
+ *
7
+ * @example
8
+ * const frozen = deepFreeze({ a: { b: 1 } });
9
+ * frozen.a.b = 2; // throws in strict mode
10
+ */
11
+ function deepFreeze(value) {
12
+ if (Array.isArray(value)) {
13
+ for (const item of value) deepFreeze(item);
14
+ return Object.freeze(value);
15
+ }
16
+ if (isPlainObject(value)) {
17
+ for (const item of Object.values(value)) deepFreeze(item);
18
+ return Object.freeze(value);
19
+ }
20
+ return value;
21
+ }
22
+
23
+ //#endregion
24
+ export { deepFreeze };
@@ -0,0 +1,13 @@
1
+ //#region src/utils/extract-function-name.ts
2
+ /**
3
+ * Extracts the function name from a function call string, e.g. `year(date_created)` → `'year'`.
4
+ * Returns null if the string is not a recognized function call.
5
+ */
6
+ function extractFunctionName(str) {
7
+ const trimmed = str.trim();
8
+ if (!trimmed.includes("(") || !trimmed.endsWith(")")) return null;
9
+ return trimmed.split("(")[0] ?? null;
10
+ }
11
+
12
+ //#endregion
13
+ export { extractFunctionName };
@@ -10,7 +10,7 @@ import { cloneFields, validateFieldsEligibility } from "./translations-shared.js
10
10
  import { dbSafeIdentifierSchema } from "./translations-validation.js";
11
11
  import { InvalidPayloadError } from "@directus/errors";
12
12
  import { fromZodError } from "zod-validation-error";
13
- import { z } from "zod";
13
+ import { z as z$1 } from "zod";
14
14
 
15
15
  //#region src/utils/generate-translations.ts
16
16
  const logger = useLogger();
@@ -31,15 +31,15 @@ function buildTranslationsAliasField(languagesFields) {
31
31
  }
32
32
  };
33
33
  }
34
- const GenerateTranslationsInput = z.object({
34
+ const GenerateTranslationsInput = z$1.object({
35
35
  collection: dbSafeIdentifierSchema,
36
- fields: z.array(dbSafeIdentifierSchema).min(1),
36
+ fields: z$1.array(dbSafeIdentifierSchema).min(1),
37
37
  translationsCollection: dbSafeIdentifierSchema.optional(),
38
38
  languagesCollection: dbSafeIdentifierSchema.optional(),
39
39
  parentFkField: dbSafeIdentifierSchema.optional(),
40
40
  languageFkField: dbSafeIdentifierSchema.optional(),
41
- createLanguagesCollection: z.boolean().optional().default(true),
42
- seedLanguages: z.boolean().optional().default(true)
41
+ createLanguagesCollection: z$1.boolean().optional().default(true),
42
+ seedLanguages: z$1.boolean().optional().default(true)
43
43
  });
44
44
  async function generateTranslations(input, options) {
45
45
  const parseResult = GenerateTranslationsInput.safeParse(input);
@@ -5,6 +5,7 @@ import { getSecret } from "./get-secret.js";
5
5
  import { createDefaultAccountability } from "../permissions/utils/create-default-accountability.js";
6
6
  import { verifyAccessJWT } from "./jwt.js";
7
7
  import isDirectusJWT from "./is-directus-jwt.js";
8
+ import { parseOAuthScope } from "./parse-oauth-scope.js";
8
9
  import { verifySessionJWT } from "./verify-session-jwt.js";
9
10
  import { InvalidCredentialsError } from "@directus/errors";
10
11
 
@@ -15,8 +16,19 @@ async function getAccountabilityForToken(token, accountability) {
15
16
  if (token) if (isDirectusJWT(token)) {
16
17
  const payload = verifyAccessJWT(token, getSecret());
17
18
  if ("session" in payload) {
18
- await verifySessionJWT(payload);
19
+ const { oauth_client } = await verifySessionJWT(payload);
19
20
  accountability.session = payload.session;
21
+ if (oauth_client !== null) {
22
+ let aud;
23
+ if (Array.isArray(payload.aud)) aud = payload.aud;
24
+ else if (payload.aud) aud = [String(payload.aud)];
25
+ else aud = [];
26
+ accountability.oauth = {
27
+ client: oauth_client,
28
+ scopes: parseOAuthScope(payload.scope),
29
+ aud
30
+ };
31
+ }
20
32
  }
21
33
  if (payload.share) accountability.share = payload.share;
22
34
  if (payload.id) accountability.user = payload.id;
@@ -1,7 +1,7 @@
1
1
  import database_default from "../database/index.js";
2
+ import { getFlowManager } from "../flows.js";
2
3
  import { fetchPoliciesIpAccess } from "../permissions/modules/fetch-policies-ip-access/fetch-policies-ip-access.js";
3
4
  import { getGraphqlQueryAndVariables } from "./get-graphql-query-and-variables.js";
4
- import { getFlowManager } from "../flows.js";
5
5
  import { toArray } from "@directus/utils";
6
6
  import { isEmpty, pick } from "lodash-es";
7
7
  import { ipInNetworks } from "@directus/utils/node";
@@ -0,0 +1,22 @@
1
+ import { getEntitlementManager } from "../license/entitlements/manager.js";
2
+ import "../license/index.js";
3
+ import { mergeFilters } from "@directus/utils";
4
+
5
+ //#region src/utils/get-history-filter-query.ts
6
+ function getHistoryFilterQuery(query, entitlement, buildFilter) {
7
+ const limit = getEntitlementManager().getEntitlementLimit(entitlement);
8
+ if (limit === null || !Number.isFinite(limit) || limit < 0) return query;
9
+ if (limit === 0) return {
10
+ ...query,
11
+ limit: 0
12
+ };
13
+ const filter = mergeFilters(buildFilter(/* @__PURE__ */ new Date(Date.now() - limit * 1e3)), query.filter ?? null, "and");
14
+ if (!filter) return query;
15
+ return {
16
+ ...query,
17
+ filter
18
+ };
19
+ }
20
+
21
+ //#endregion
22
+ export { getHistoryFilterQuery };
@@ -12,7 +12,7 @@ import getDefaultValue from "./get-default-value.js";
12
12
  import { getSystemFieldRowsWithAuthProviders } from "./get-field-system-rows.js";
13
13
  import { useEnv } from "@directus/env";
14
14
  import { parseJSON, toArray, toBoolean } from "@directus/utils";
15
- import { mapValues } from "lodash-es";
15
+ import { mapValues, pick } from "lodash-es";
16
16
  import { createInspector } from "@directus/schema";
17
17
  import { systemCollectionRows } from "@directus/system-data";
18
18
 
@@ -74,7 +74,7 @@ async function getDatabaseSchema(database, schemaInspector) {
74
74
  };
75
75
  const systemFieldRows$1 = getSystemFieldRowsWithAuthProviders();
76
76
  const schemaOverview = await schemaInspector.overview();
77
- const collections = [...await database.select("collection", "singleton", "note", "sort_field", "accountability").from("directus_collections"), ...systemCollectionRows];
77
+ const collections = [...(await database.select("*").from("directus_collections")).filter((c) => !("status" in c) || c["status"] === "active").map((c) => pick(c, "collection", "singleton", "note", "sort_field", "accountability")), ...systemCollectionRows];
78
78
  for (const [collection, info] of Object.entries(schemaOverview)) {
79
79
  if (toArray(env["DB_EXCLUDE_TABLES"]).includes(collection)) {
80
80
  logger.trace(`Collection "${collection}" is configured to be excluded and will be ignored`);
@@ -1,12 +1,13 @@
1
1
  import { ItemsService } from "../services/items.js";
2
2
  import { FilesService } from "../services/files.js";
3
3
  import { FoldersService } from "../services/folders.js";
4
- import { ActivityService } from "../services/activity.js";
5
4
  import { AccessService } from "../services/access.js";
5
+ import { ActivityService } from "../services/activity.js";
6
+ import { FlowsService } from "../services/flows.js";
7
+ import { RevisionsService } from "../services/revisions.js";
6
8
  import { SettingsService } from "../services/settings.js";
7
9
  import { UsersService } from "../services/users.js";
8
10
  import { NotificationsService } from "../services/notifications.js";
9
- import { RevisionsService } from "../services/revisions.js";
10
11
  import { CommentsService } from "../services/comments.js";
11
12
  import { DashboardsService } from "../services/dashboards.js";
12
13
  import { DeploymentProjectsService } from "../services/deployment-projects.js";
@@ -22,7 +23,6 @@ import { SharesService } from "../services/shares.js";
22
23
  import { TranslationsService } from "../services/translations.js";
23
24
  import { VersionsService } from "../services/versions.js";
24
25
  import "../services/index.js";
25
- import { FlowsService } from "../services/flows.js";
26
26
  import { ForbiddenError } from "@directus/errors";
27
27
 
28
28
  //#region src/utils/get-service.ts
@@ -0,0 +1,9 @@
1
+ //#region src/utils/is-admin.ts
2
+ function isAdmin(accountability) {
3
+ if (accountability === null) return true;
4
+ if (accountability?.admin === true) return true;
5
+ return false;
6
+ }
7
+
8
+ //#endregion
9
+ export { isAdmin };
@@ -0,0 +1,15 @@
1
+ //#region src/utils/is-unauthenticated.ts
2
+ /**
3
+ * Checks if the given accountability is unauthenticated
4
+ *
5
+ * @param accountability
6
+ * @returns True if the user is unauthenticated, false otherwise.
7
+ */
8
+ function isUnauthenticated(accountability) {
9
+ if (accountability === null) return false;
10
+ if (accountability === void 0) return true;
11
+ return accountability?.role === null && accountability?.user === null;
12
+ }
13
+
14
+ //#endregion
15
+ export { isUnauthenticated };
@@ -0,0 +1,12 @@
1
+ //#region src/utils/parse-oauth-scope.ts
2
+ /**
3
+ * Parse an OAuth scope string into a deduplicated array of scope tokens.
4
+ * Per RFC 6749 Section 3.3, scope is a space-separated list of case-sensitive strings.
5
+ */
6
+ function parseOAuthScope(scope) {
7
+ if (typeof scope !== "string" || scope.trim() === "") return [];
8
+ return [...new Set(scope.trim().split(/\s+/))];
9
+ }
10
+
11
+ //#endregion
12
+ export { parseOAuthScope };
@@ -61,7 +61,7 @@ function sanitizeFields(rawFields) {
61
61
  }
62
62
  function sanitizeSort(rawSort) {
63
63
  let fields = [];
64
- if (typeof rawSort === "string") fields = rawSort.split(",");
64
+ if (typeof rawSort === "string") fields = splitFields(rawSort);
65
65
  else if (Array.isArray(rawSort)) fields = rawSort;
66
66
  fields = fields.map((field) => field.trim());
67
67
  return fields;
@@ -143,7 +143,7 @@ async function sanitizeDeep(deep, schema, accountability) {
143
143
  for (const [key, value] of Object.entries(level)) {
144
144
  if (!key) break;
145
145
  if (key.startsWith("_")) subQuery[key.substring(1)] = value;
146
- else if (isPlainObject(value)) parse(value, [...path, key]);
146
+ else if (isPlainObject(value)) await parse(value, [...path, key]);
147
147
  }
148
148
  if (Object.keys(subQuery).length > 0) {
149
149
  const parsedSubQuery = await sanitizeQuery(subQuery, schema, accountability);
@@ -0,0 +1,29 @@
1
+ //#region src/utils/split-field-path.ts
2
+ /**
3
+ * Splits a field path on dots that are outside parentheses.
4
+ *
5
+ * A plain `.split('.')` breaks when a path segment contains a function call
6
+ * whose arguments include dots (e.g. `category_id.json(metadata, settings.theme)`).
7
+ * This helper only splits on dots at paren-depth 0.
8
+ *
9
+ * @example
10
+ * splitFieldPath('a.b.c') // ['a', 'b', 'c']
11
+ * splitFieldPath('category_id.json(metadata, color)') // ['category_id', 'json(metadata, color)']
12
+ * splitFieldPath('a.json(meta, x.y.z)') // ['a', 'json(meta, x.y.z)']
13
+ */
14
+ function splitFieldPath(path) {
15
+ const parts = [];
16
+ let depth = 0;
17
+ let start = 0;
18
+ for (let i = 0; i < path.length; i++) if (path[i] === "(") depth++;
19
+ else if (path[i] === ")") depth--;
20
+ else if (path[i] === "." && depth === 0) {
21
+ parts.push(path.slice(start, i));
22
+ start = i + 1;
23
+ }
24
+ parts.push(path.slice(start));
25
+ return parts;
26
+ }
27
+
28
+ //#endregion
29
+ export { splitFieldPath };
@@ -13,7 +13,7 @@ function useStore(namespace, options) {
13
13
  namespace,
14
14
  redis: useRedis()
15
15
  };
16
- if (config.type === "redis" && options?.ttl) config.ttl = options?.ttl;
16
+ if (options?.ttl) config.ttl = options?.ttl;
17
17
  const store = createCache(config);
18
18
  return (callback) => store.usingLock(`lock`, async () => {
19
19
  return await callback({