@directus/api 35.2.0 → 36.0.0-rc.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 (188) 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 +33 -9
  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 -9
  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/extensions/lib/installation/manager.js +1 -1
  90. package/dist/extensions/lib/sandbox/register/operation.js +1 -1
  91. package/dist/extensions/lib/sync/sync.js +1 -1
  92. package/dist/extensions/manager.js +3 -3
  93. package/dist/flows.js +5 -5
  94. package/dist/license/entitlements/lib/collections.js +37 -0
  95. package/dist/license/entitlements/lib/custom-llms-enabled.js +18 -0
  96. package/dist/license/entitlements/lib/custom-permission-rules-enabled.js +41 -0
  97. package/dist/license/entitlements/lib/flows.js +29 -0
  98. package/dist/license/entitlements/lib/seats.js +103 -0
  99. package/dist/license/entitlements/lib/sso-enabled.js +45 -0
  100. package/dist/license/entitlements/manager.js +256 -0
  101. package/dist/license/index.js +4 -0
  102. package/dist/license/manager.js +505 -0
  103. package/dist/license/utils/compute-license-status.js +27 -0
  104. package/dist/license/utils/get-core-grace-expires-at.js +38 -0
  105. package/dist/license/utils/get-license-key.js +23 -0
  106. package/dist/license/utils/get-license-token.js +23 -0
  107. package/dist/license/utils/handle-license-error.js +41 -0
  108. package/dist/license/utils/is-in-core-grace-period.js +11 -0
  109. package/dist/license/utils/is-sso-bypass-allowed.js +21 -0
  110. package/dist/license/utils/use-rpc.js +33 -0
  111. package/dist/middleware/cache.js +4 -1
  112. package/dist/middleware/error-handler.js +11 -0
  113. package/dist/middleware/extract-token.js +11 -2
  114. package/dist/middleware/is-admin.js +16 -0
  115. package/dist/middleware/is-locked.js +16 -0
  116. package/dist/middleware/mcp-oauth-guard.js +23 -0
  117. package/dist/middleware/request-counter.js +5 -2
  118. package/dist/packages/types/dist/index.js +117 -122
  119. package/dist/permissions/modules/process-ast/utils/extract-paths-from-query.js +10 -1
  120. package/dist/permissions/utils/get-unaliased-field-key.js +2 -1
  121. package/dist/request/is-denied-ip.js +2 -0
  122. package/dist/schedules/license.js +31 -0
  123. package/dist/schedules/oauth-cleanup.js +26 -0
  124. package/dist/schedules/retention.js +1 -1
  125. package/dist/schedules/telemetry.js +4 -1
  126. package/dist/schedules/tus.js +1 -1
  127. package/dist/schedules/utils/duration-to-cron.js +36 -0
  128. package/dist/services/activity.js +15 -0
  129. package/dist/services/authentication.js +12 -5
  130. package/dist/services/collections.js +40 -10
  131. package/dist/services/fields.js +6 -6
  132. package/dist/services/flows.js +12 -0
  133. package/dist/services/graphql/resolvers/system-admin.js +2 -2
  134. package/dist/services/graphql/resolvers/system-global.js +1 -1
  135. package/dist/services/graphql/resolvers/system.js +34 -18
  136. package/dist/services/graphql/schema/get-types.js +23 -2
  137. package/dist/services/graphql/schema/parse-query.js +8 -0
  138. package/dist/services/graphql/schema/read.js +12 -0
  139. package/dist/services/graphql/types/json-filter.js +30 -0
  140. package/dist/services/index.js +6 -6
  141. package/dist/services/items.js +32 -14
  142. package/dist/services/mcp-oauth/cimd.js +307 -0
  143. package/dist/services/mcp-oauth/index.js +1185 -0
  144. package/dist/services/mcp-oauth/types/error.js +22 -0
  145. package/dist/services/mcp-oauth/utils/cimd-egress.js +182 -0
  146. package/dist/services/mcp-oauth/utils/domain.js +21 -0
  147. package/dist/services/mcp-oauth/utils/loopback.js +11 -0
  148. package/dist/services/mcp-oauth/utils/redirect.js +84 -0
  149. package/dist/services/mcp-oauth/utils/registration-debug.js +131 -0
  150. package/dist/services/payload.js +2 -1
  151. package/dist/services/permissions.js +31 -9
  152. package/dist/services/revisions.js +15 -0
  153. package/dist/services/server.js +21 -4
  154. package/dist/services/settings.js +37 -3
  155. package/dist/services/users.js +13 -6
  156. package/dist/services/utils.js +6 -1
  157. package/dist/services/versions.js +137 -69
  158. package/dist/utils/calculate-field-depth.js +1 -0
  159. package/dist/utils/deep-freeze.js +24 -0
  160. package/dist/utils/extract-function-name.js +13 -0
  161. package/dist/utils/generate-translations.js +5 -5
  162. package/dist/utils/get-accountability-for-token.js +13 -1
  163. package/dist/utils/get-cache-key.js +1 -1
  164. package/dist/utils/get-history-filter-query.js +22 -0
  165. package/dist/utils/get-schema.js +2 -2
  166. package/dist/utils/get-service.js +3 -3
  167. package/dist/utils/is-admin.js +9 -0
  168. package/dist/utils/parse-oauth-scope.js +12 -0
  169. package/dist/utils/sanitize-query.js +1 -1
  170. package/dist/utils/split-field-path.js +29 -0
  171. package/dist/utils/transaction.js +2 -2
  172. package/dist/utils/translations-validation.js +2 -2
  173. package/dist/utils/validate-query.js +35 -4
  174. package/dist/utils/validate-user-count-integrity.js +28 -5
  175. package/dist/utils/verify-session-jwt.js +5 -2
  176. package/dist/utils/versioning/handle-version.js +130 -48
  177. package/dist/utils/versioning/remove-circular.js +17 -0
  178. package/dist/websocket/authenticate.js +2 -1
  179. package/dist/websocket/collab/collab.js +1 -1
  180. package/dist/websocket/collab/room.js +1 -1
  181. package/dist/websocket/controllers/base.js +12 -0
  182. package/dist/websocket/controllers/graphql.js +1 -1
  183. package/dist/websocket/handlers/subscribe.js +1 -1
  184. package/dist/websocket/messages.js +64 -64
  185. package/dist/websocket/utils/items.js +2 -2
  186. package/license +90 -80
  187. package/package.json +33 -32
  188. package/dist/controllers/mcp.js +0 -31
@@ -1,6 +1,10 @@
1
+ import { CUSTOM_LLM_FIELDS } from "../constants.js";
1
2
  import { ItemsService } from "./items.js";
3
+ import { getEntitlementManager } from "../license/entitlements/manager.js";
2
4
  import { sendReport } from "../telemetry/lib/send-report.js";
3
5
  import "../telemetry/index.js";
6
+ import "../license/index.js";
7
+ import { InvalidPayloadError, ResourceRestrictedError } from "@directus/errors";
4
8
  import { version } from "directus/version";
5
9
 
6
10
  //#region src/services/settings.ts
@@ -8,6 +12,37 @@ var SettingsService = class extends ItemsService {
8
12
  constructor(options) {
9
13
  super("directus_settings", options);
10
14
  }
15
+ async createOne(data, opts) {
16
+ if (this.accountability !== null) {
17
+ if ("license_key" in data) throw new InvalidPayloadError({ reason: `You can't change the "license_key" value manually` });
18
+ if ("license_token" in data) throw new InvalidPayloadError({ reason: `You can't change the "license_token" value manually` });
19
+ }
20
+ const entitlementManager = getEntitlementManager();
21
+ const changesLLM = CUSTOM_LLM_FIELDS.some((field) => field in data && data[field] !== null);
22
+ if (!entitlementManager.isEntitled("custom_llms_enabled") && changesLLM) throw new ResourceRestrictedError({ category: "custom_llms_enabled" });
23
+ const result = await super.createOne(data, opts);
24
+ if (changesLLM) await getEntitlementManager().clearCache("custom_llms_enabled");
25
+ return result;
26
+ }
27
+ async updateMany(keys, data, opts) {
28
+ if (this.accountability !== null) {
29
+ if ("license_key" in data) throw new InvalidPayloadError({ reason: `You can't change the "license_key" value manually` });
30
+ if ("license_token" in data) throw new InvalidPayloadError({ reason: `You can't change the "license_token" value manually` });
31
+ }
32
+ const entitlementManager = getEntitlementManager();
33
+ const changesLLM = CUSTOM_LLM_FIELDS.some((field) => field in data && data[field] !== null);
34
+ if (!entitlementManager.isEntitled("custom_llms_enabled") && changesLLM) throw new ResourceRestrictedError({ category: "custom_llms_enabled" });
35
+ const result = await super.updateMany(keys, data, opts);
36
+ if (changesLLM) await entitlementManager.clearCache("custom_llms_enabled");
37
+ return result;
38
+ }
39
+ async readByQuery(query, opts) {
40
+ const data = await super.readByQuery(query, opts);
41
+ if (!getEntitlementManager().isEntitled("custom_llms_enabled") && this.accountability !== null) {
42
+ for (const record of data) for (const field of CUSTOM_LLM_FIELDS) if (record[field]) record[field] = null;
43
+ }
44
+ return data;
45
+ }
11
46
  async setOwner(data) {
12
47
  const { project_id } = await this.knex.select("project_id").from("directus_settings").first();
13
48
  sendReport({
@@ -19,10 +54,9 @@ var SettingsService = class extends ItemsService {
19
54
  });
20
55
  return await this.upsertSingleton({
21
56
  project_owner: data.project_owner,
22
- project_usage: data.project_usage,
23
- org_name: data.org_name,
24
57
  product_updates: data.product_updates,
25
- project_status: null
58
+ project_usage: data.project_usage,
59
+ org_name: data.org_name
26
60
  });
27
61
  }
28
62
  };
@@ -12,11 +12,14 @@ import { createDefaultAccountability } from "../permissions/utils/create-default
12
12
  import isUrlAllowed from "../utils/is-url-allowed.js";
13
13
  import { verifyJWT } from "../utils/jwt.js";
14
14
  import { stall } from "../utils/stall.js";
15
+ import { getEntitlementManager } from "../license/entitlements/manager.js";
15
16
  import { SettingsService } from "./settings.js";
17
+ import "../license/index.js";
16
18
  import { useEnv } from "@directus/env";
17
19
  import { ForbiddenError, InvalidInviteError, InvalidPayloadError, RecordNotUniqueError } from "@directus/errors";
18
20
  import { getSimpleHash, toArray, validatePayload } from "@directus/utils";
19
21
  import { isEmpty } from "lodash-es";
22
+ import { USER_INACTIVE_LICENSE_STATUS } from "@directus/constants";
20
23
  import { performance } from "perf_hooks";
21
24
  import Joi from "joi";
22
25
  import { FailedValidationError, joiValidationErrorItemToErrorExtensions } from "@directus/validation";
@@ -187,7 +190,7 @@ var UsersService = class UsersService extends ItemsService {
187
190
  }
188
191
  if ("role" in data) opts.userIntegrityCheckFlags = UserIntegrityCheckFlag.All;
189
192
  if ("status" in data) if (data["status"] === "active") opts.userIntegrityCheckFlags = (opts.userIntegrityCheckFlags ?? UserIntegrityCheckFlag.None) | UserIntegrityCheckFlag.UserLimits;
190
- else opts.userIntegrityCheckFlags = UserIntegrityCheckFlag.All;
193
+ else opts.userIntegrityCheckFlags = (opts.userIntegrityCheckFlags ?? UserIntegrityCheckFlag.None) | UserIntegrityCheckFlag.RemainingAdmins;
191
194
  if (opts.userIntegrityCheckFlags) opts.onRequireUserIntegrityCheck?.(opts.userIntegrityCheckFlags);
192
195
  const result = await super.updateMany(keys, data, opts);
193
196
  if (data["status"] !== void 0 && data["status"] !== "active") await this.clearUserSessions(keys);
@@ -213,6 +216,7 @@ var UsersService = class UsersService extends ItemsService {
213
216
  await this.knex("directus_versions").update({ user_updated: null }).whereIn("user_updated", keys);
214
217
  await super.deleteMany(keys, opts);
215
218
  await this.clearUserSessions(keys);
219
+ await getEntitlementManager().clearCache("seats", "sso_enabled");
216
220
  return keys;
217
221
  }
218
222
  async inviteUser(email, role, url, subject) {
@@ -258,12 +262,15 @@ var UsersService = class UsersService extends ItemsService {
258
262
  if (scope !== "invite") throw new ForbiddenError();
259
263
  const user = await this.getUserByEmail(email);
260
264
  if (user?.status !== "invited") throw new InvalidInviteError();
261
- await new UsersService({
265
+ const service = new UsersService({
262
266
  knex: this.knex,
263
267
  schema: this.schema
264
- }).updateOne(user.id, {
268
+ });
269
+ const { allowed: isWithinLicenseLimits } = await getEntitlementManager().check("seats");
270
+ const status = isWithinLicenseLimits ? "active" : USER_INACTIVE_LICENSE_STATUS;
271
+ await service.updateOne(user.id, {
265
272
  password,
266
- status: "active"
273
+ status
267
274
  });
268
275
  }
269
276
  async registerUser(input) {
@@ -310,7 +317,7 @@ var UsersService = class UsersService extends ItemsService {
310
317
  email: input.email,
311
318
  scope: "pending-registration"
312
319
  };
313
- const token = jwt.sign(payload, env["SECRET"], {
320
+ const token = jwt.sign(payload, getSecret(), {
314
321
  expiresIn: env["EMAIL_VERIFICATION_TOKEN_TTL"],
315
322
  issuer: "directus"
316
323
  });
@@ -334,7 +341,7 @@ var UsersService = class UsersService extends ItemsService {
334
341
  await stall(STALL_TIME, timeStart);
335
342
  }
336
343
  async verifyRegistration(token) {
337
- const { email, scope } = verifyJWT(token, env["SECRET"]);
344
+ const { email, scope } = verifyJWT(token, getSecret());
338
345
  if (scope !== "pending-registration") throw new ForbiddenError();
339
346
  const user = await this.getUserByEmail(email);
340
347
  if (user?.status !== "unverified") throw new InvalidPayloadError({ reason: "Invalid verification code" });
@@ -4,6 +4,8 @@ import { fetchAllowedFields } from "../permissions/modules/fetch-allowed-fields/
4
4
  import emitter_default from "../emitter.js";
5
5
  import { validateAccess } from "../permissions/modules/validate-access/validate-access.js";
6
6
  import { shouldClearCache } from "../utils/should-clear-cache.js";
7
+ import { getEntitlementManager } from "../license/entitlements/manager.js";
8
+ import "../license/index.js";
7
9
  import { ForbiddenError, InvalidPayloadError } from "@directus/errors";
8
10
  import { systemCollectionRows } from "@directus/system-data";
9
11
 
@@ -74,7 +76,10 @@ var UtilsService = class {
74
76
  async clearCache({ system }) {
75
77
  if (this.accountability?.admin !== true) throw new ForbiddenError();
76
78
  const { cache } = getCache();
77
- if (system) await clearSystemCache({ forced: true });
79
+ if (system) {
80
+ await clearSystemCache({ forced: true });
81
+ await getEntitlementManager().clearCache();
82
+ }
78
83
  return cache?.clear();
79
84
  }
80
85
  };
@@ -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,7 @@ 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
+ if (itemLess) return;
50
59
  if ((await new VersionsService({
51
60
  knex: this.knex,
52
61
  schema: this.schema
@@ -73,18 +82,27 @@ var VersionsService = class VersionsService extends ItemsService {
73
82
  mainHash
74
83
  };
75
84
  }
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;
85
+ async getVersionSaves(key, collection, item, mapDelta = true) {
86
+ let itemFilter = {};
87
+ if (item) itemFilter = { item: { _eq: item } };
88
+ let versions = await this.readByQuery({
89
+ filter: {
90
+ key: { _eq: key },
91
+ collection: { _eq: collection },
92
+ ...itemFilter
93
+ },
94
+ limit: -1
95
+ });
96
+ if (mapDelta) versions = versions.map((version) => {
97
+ if (version.delta) version.delta = this.mapDelta(version);
98
+ return version;
99
+ });
100
+ return versions;
84
101
  }
85
102
  async createOne(data, opts) {
86
103
  await this.validateCreateData(data);
87
- data["hash"] = hash(await this.getMainItem(data["collection"], data["item"]));
104
+ if (data["item"]) data["hash"] = hash(await this.getMainItem(data["collection"], data["item"]));
105
+ else data["hash"] = null;
88
106
  return super.createOne(data, opts);
89
107
  }
90
108
  async readOne(key, query = {}, opts) {
@@ -105,31 +123,39 @@ var VersionsService = class VersionsService extends ItemsService {
105
123
  async updateMany(keys, data, opts) {
106
124
  const { error } = Joi.object({
107
125
  key: Joi.string(),
108
- name: Joi.string().allow(null)
126
+ name: Joi.string().allow(null),
127
+ item: Joi.string().allow(null)
109
128
  }).validate(data);
110
129
  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}"` });
128
- }
130
+ if (isPublishedVersionKey(data["key"])) throw new InvalidPayloadError({ reason: `"${data["key"]}" is a reserved version key` });
131
+ const keyCombos = /* @__PURE__ */ new Set();
132
+ for (const pk of keys) {
133
+ const existingVersion = await this.readOne(pk, { fields: [
134
+ "collection",
135
+ "item",
136
+ "key"
137
+ ] });
138
+ const collection = existingVersion.collection;
139
+ const item = "item" in data ? data["item"] : existingVersion.item;
140
+ const key = "key" in data ? data["key"] : existingVersion.key;
141
+ if (key !== VERSION_KEY_DRAFT && item === null) throw new InvalidPayloadError({ reason: `"key" must be "${VERSION_KEY_DRAFT}" for versions not linked to an item` });
142
+ if (item === null) continue;
143
+ const keyCombo = `${key}-${collection}-${item}`;
144
+ if (keyCombos.has(keyCombo)) throw new UnprocessableContentError({ reason: `Cannot update multiple versions on "${item}" in collection "${collection}" to the same key "${key}"` });
145
+ keyCombos.add(keyCombo);
146
+ if ((await super.readByQuery({
147
+ aggregate: { count: ["*"] },
148
+ filter: {
149
+ id: { _neq: pk },
150
+ key: { _eq: key },
151
+ collection: { _eq: collection },
152
+ item: { _eq: item }
153
+ }
154
+ }))[0]["count"] > 0) throw new UnprocessableContentError({ reason: `Version "${key}" already exists for item "${item}" in collection "${collection}"` });
129
155
  }
130
156
  return super.updateMany(keys, data, opts);
131
157
  }
132
- async save(key, delta) {
158
+ async save(key, delta, opts) {
133
159
  const version = await super.readOne(key);
134
160
  const payloadService = new PayloadService(this.collection, {
135
161
  accountability: this.accountability,
@@ -137,35 +163,65 @@ var VersionsService = class VersionsService extends ItemsService {
137
163
  schema: this.schema
138
164
  });
139
165
  const { item, collection, delta: existingDelta } = version;
140
- const helpers = getHelpers(this.knex);
141
166
  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
- });
167
+ if (item) {
168
+ const trackingAccountability = this.schema.collections[collection]?.accountability ?? null;
169
+ if (trackingAccountability !== null) {
170
+ const revisionsService = new RevisionsService({
171
+ knex: this.knex,
172
+ schema: this.schema
173
+ });
174
+ let patchedExistingRevision = false;
175
+ if (opts?.patchRevision && trackingAccountability === "all") {
176
+ const [latestRevision] = await revisionsService.readByQuery({
177
+ filter: { version: { _eq: key } },
178
+ sort: ["-activity.timestamp"],
179
+ limit: 1,
180
+ fields: [
181
+ "id",
182
+ "data",
183
+ "delta",
184
+ "activity.user"
185
+ ]
186
+ });
187
+ const currentUser = this.accountability?.user ?? null;
188
+ const latestRevisionUser = (latestRevision?.["activity"])?.user ?? null;
189
+ if (latestRevision && latestRevisionUser === currentUser) {
190
+ const mergedRevisionData = assign({}, latestRevision["data"], revisionDelta);
191
+ const mergedRevisionDelta = assign({}, latestRevision["delta"], revisionDelta);
192
+ await revisionsService.updateOne(latestRevision["id"], {
193
+ data: mergedRevisionData,
194
+ delta: mergedRevisionDelta
195
+ });
196
+ patchedExistingRevision = true;
197
+ }
198
+ }
199
+ if (!patchedExistingRevision) {
200
+ const activity = await new ActivityService({
201
+ knex: this.knex,
202
+ schema: this.schema
203
+ }).createOne({
204
+ action: Action.VERSION_SAVE,
205
+ user: this.accountability?.user ?? null,
206
+ collection,
207
+ ip: this.accountability?.ip ?? null,
208
+ user_agent: this.accountability?.userAgent ?? null,
209
+ origin: this.accountability?.origin ?? null,
210
+ item
211
+ });
212
+ if (trackingAccountability === "all") await revisionsService.createOne({
213
+ activity,
214
+ version: key,
215
+ collection,
216
+ item,
217
+ data: revisionDelta,
218
+ delta: revisionDelta
219
+ });
220
+ }
221
+ }
167
222
  }
168
223
  revisionDelta = revisionDelta ? revisionDelta : null;
224
+ const helpers = getHelpers(this.knex);
169
225
  const date = new Date(helpers.date.writeTimestamp((/* @__PURE__ */ new Date()).toISOString()));
170
226
  deepMapObjects(revisionDelta, (object, path) => {
171
227
  const existing = get(existingDelta, path);
@@ -186,22 +242,29 @@ var VersionsService = class VersionsService extends ItemsService {
186
242
  if (shouldClearCache(cache, void 0, collection)) cache.clear();
187
243
  return finalVersionDelta;
188
244
  }
189
- async promote(version, mainHash, fields) {
245
+ async promote(version, opts) {
190
246
  const { collection, item, delta } = await super.readOne(version);
191
- if (this.accountability) await validateAccess({
247
+ if (item && typeof opts?.mainHash !== "string") throw new InvalidPayloadError({ reason: `"mainHash" field is required` });
248
+ if (this.accountability) await validateAccess(item ? {
192
249
  accountability: this.accountability,
193
250
  action: "update",
194
251
  collection,
195
252
  primaryKeys: [item]
253
+ } : {
254
+ accountability: this.accountability,
255
+ action: "create",
256
+ collection
196
257
  }, {
197
258
  schema: this.schema,
198
259
  knex: this.knex
199
260
  });
200
261
  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` });
262
+ if (item) {
263
+ const { outdated, mainHash } = await this.verifyHash(collection, item, opts?.mainHash);
264
+ if (outdated) throw new VersionHashMismatchError({ mainHash });
265
+ }
203
266
  const { rawDelta, defaultOverwrites } = splitRecursive(delta);
204
- const payloadToUpdate = fields ? pick(rawDelta, fields) : rawDelta;
267
+ const payloadToUpdate = opts?.fields ? pick(rawDelta, opts.fields) : rawDelta;
205
268
  const itemsService = new ItemsService(collection, {
206
269
  accountability: this.accountability,
207
270
  knex: this.knex,
@@ -216,7 +279,12 @@ var VersionsService = class VersionsService extends ItemsService {
216
279
  schema: this.schema,
217
280
  accountability: this.accountability
218
281
  });
219
- const updatedItemKey = await itemsService.updateOne(item, payloadAfterHooks, { overwriteDefaults: defaultOverwrites });
282
+ let updatedItemKey;
283
+ if (item) updatedItemKey = await itemsService.updateOne(item, payloadAfterHooks, { overwriteDefaults: defaultOverwrites });
284
+ else {
285
+ updatedItemKey = await itemsService.createOne(payloadAfterHooks, { overwriteDefaults: defaultOverwrites });
286
+ await this.updateOne(version, { item: String(updatedItemKey) });
287
+ }
220
288
  emitter_default.emitAction(["items.promote", `${collection}.items.promote`], {
221
289
  payload: payloadAfterHooks,
222
290
  collection,
@@ -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);
@@ -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,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 };