@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
@@ -0,0 +1,31 @@
1
+ import { scheduleSynchronizedJob, validateCron } from "../utils/schedule.js";
2
+ import { durationToCron } from "./utils/duration-to-cron.js";
3
+ import { getLicenseManager } from "../license/manager.js";
4
+
5
+ //#region src/schedules/license.ts
6
+ let job = null;
7
+ async function stopLicenseCheck() {
8
+ await job?.stop();
9
+ }
10
+ /**
11
+ * Schedule a license check at the license's validation_interval.
12
+ */
13
+ async function schedule() {
14
+ await job?.stop();
15
+ const licenseManager = getLicenseManager();
16
+ const license = await licenseManager.getLicense();
17
+ if (license.meta.validation_interval === -1) return false;
18
+ const cron = durationToCron(license.meta.validation_interval);
19
+ if (!validateCron(cron)) return false;
20
+ job = scheduleSynchronizedJob("license-check", cron, async () => {
21
+ const jobLicense = await licenseManager.getLicense();
22
+ if (jobLicense.meta.validation_interval !== license.meta.validation_interval) {
23
+ await job?.stop();
24
+ if (jobLicense.meta.validation_interval !== -1) await schedule();
25
+ } else await licenseManager.refresh();
26
+ });
27
+ return true;
28
+ }
29
+
30
+ //#endregion
31
+ export { schedule as default, stopLicenseCheck };
@@ -0,0 +1,26 @@
1
+ import { useLogger } from "../logger/index.js";
2
+ import { scheduleSynchronizedJob, validateCron } from "../utils/schedule.js";
3
+ import { getSchema } from "../utils/get-schema.js";
4
+ import { McpOAuthService } from "../services/mcp-oauth/index.js";
5
+ import { useEnv } from "@directus/env";
6
+
7
+ //#region src/schedules/oauth-cleanup.ts
8
+ async function scheduleOAuthCleanup() {
9
+ const env = useEnv();
10
+ const schedule = String(env["MCP_OAUTH_CLEANUP_SCHEDULE"]);
11
+ if (!validateCron(schedule)) {
12
+ useLogger().error(`Invalid MCP_OAUTH_CLEANUP_SCHEDULE: "${schedule}". OAuth cleanup disabled.`);
13
+ return false;
14
+ }
15
+ scheduleSynchronizedJob("oauth-cleanup", schedule, async () => {
16
+ try {
17
+ await new McpOAuthService({ schema: await getSchema() }).cleanup();
18
+ } catch (error) {
19
+ useLogger().error(error, "MCP OAuth cleanup failed");
20
+ }
21
+ });
22
+ return true;
23
+ }
24
+
25
+ //#endregion
26
+ export { scheduleOAuthCleanup as default };
@@ -2,9 +2,9 @@ import { useLogger } from "../logger/index.js";
2
2
  import { getMilliseconds } from "../utils/get-milliseconds.js";
3
3
  import { getHelpers } from "../database/helpers/index.js";
4
4
  import database_default from "../database/index.js";
5
+ import { scheduleSynchronizedJob, validateCron } from "../utils/schedule.js";
5
6
  import { useLock } from "../lock/lib/use-lock.js";
6
7
  import "../lock/index.js";
7
- import { scheduleSynchronizedJob, validateCron } from "../utils/schedule.js";
8
8
  import { useEnv } from "@directus/env";
9
9
  import { toBoolean } from "@directus/utils";
10
10
  import { Action } from "@directus/constants";
@@ -1,7 +1,9 @@
1
1
  import { getCache } from "../cache.js";
2
2
  import { scheduleSynchronizedJob } from "../utils/schedule.js";
3
+ import { getEntitlementManager } from "../license/entitlements/manager.js";
3
4
  import { track } from "../telemetry/lib/track.js";
4
5
  import "../telemetry/index.js";
6
+ import "../license/index.js";
5
7
  import { useEnv } from "@directus/env";
6
8
  import { toBoolean } from "@directus/utils";
7
9
 
@@ -19,7 +21,8 @@ const jobCallback = () => {
19
21
  * @returns Whether or not telemetry has been initialized
20
22
  */
21
23
  async function schedule() {
22
- if (toBoolean(useEnv()["TELEMETRY"]) === false) return false;
24
+ const env = useEnv();
25
+ if (!getEntitlementManager().isEntitled("telemetry_required") && !toBoolean(env["TELEMETRY"])) return false;
23
26
  scheduleSynchronizedJob("telemetry", "0 */6 * * *", jobCallback);
24
27
  const { lockCache } = getCache();
25
28
  if (!await lockCache.get("telemetry-lock")) {
@@ -1,6 +1,6 @@
1
1
  import { RESUMABLE_UPLOADS } from "../constants.js";
2
- import { getSchema } from "../utils/get-schema.js";
3
2
  import { scheduleSynchronizedJob, validateCron } from "../utils/schedule.js";
3
+ import { getSchema } from "../utils/get-schema.js";
4
4
  import { createTusServer } from "../services/tus/server.js";
5
5
  import "../services/tus/index.js";
6
6
 
@@ -0,0 +1,36 @@
1
+ import { random } from "lodash-es";
2
+
3
+ //#region src/schedules/utils/duration-to-cron.ts
4
+ const HOURS_IN_SECONDS = 3600;
5
+ const ALLOWED_HOURS = new Set([
6
+ 1,
7
+ 2,
8
+ 3,
9
+ 4,
10
+ 6,
11
+ 8,
12
+ 12
13
+ ]);
14
+ /**
15
+ * Convert a duration in seconds into a cron expression
16
+ *
17
+ * - Honored intervals (hours): 1, 2, 3, 4, 6, 8, 12.
18
+ * - Random hour offset within [0, hours) spreads load across phase groups.
19
+ * - Any duration outside interval falls back to daily.
20
+ *
21
+ * 3600 (1h) → fires every hour at random minute and second
22
+ * 7200 (2h) → fires every 2h, phase offset 0 or 1
23
+ * 25200 (7h) → daily fallback
24
+ */
25
+ function durationToCron(duration) {
26
+ const second = random(0, 59);
27
+ const minute = random(0, 59);
28
+ if (duration > 0 && duration % HOURS_IN_SECONDS === 0) {
29
+ const hours = duration / HOURS_IN_SECONDS;
30
+ if (ALLOWED_HOURS.has(hours)) return `${second} ${minute} ${hours === 1 ? "*" : random(0, hours - 1)}/${hours} * * *`;
31
+ }
32
+ return `${second} ${minute} ${random(0, 23)} * * *`;
33
+ }
34
+
35
+ //#endregion
36
+ export { durationToCron };
@@ -1,10 +1,25 @@
1
1
  import { ItemsService } from "./items.js";
2
+ import { getHistoryFilterQuery } from "../utils/get-history-filter-query.js";
2
3
 
3
4
  //#region src/services/activity.ts
4
5
  var ActivityService = class extends ItemsService {
6
+ queryCache = /* @__PURE__ */ new WeakMap();
5
7
  constructor(options) {
6
8
  super("directus_activity", options);
7
9
  }
10
+ async readByQuery(query, opts) {
11
+ if (this.accountability === null) return super.readByQuery(query, opts);
12
+ const historyQuery = this.getLimitedHistoryQuery(query);
13
+ return super.readByQuery(historyQuery, opts);
14
+ }
15
+ getLimitedHistoryQuery(query) {
16
+ let cachedQuery = this.queryCache.get(query);
17
+ if (!cachedQuery) {
18
+ cachedQuery = getHistoryFilterQuery(query, "activity_historical_timeframe", (sinceDate) => ({ timestamp: { _gte: sinceDate.toISOString() } }));
19
+ this.queryCache.set(query, cachedQuery);
20
+ }
21
+ return cachedQuery;
22
+ }
8
23
  };
9
24
 
10
25
  //#endregion
@@ -9,12 +9,15 @@ import { PayloadService } from "./payload.js";
9
9
  import { ActivityService } from "./activity.js";
10
10
  import { RateLimiterRes, createRateLimiter } from "../rate-limiter.js";
11
11
  import { stall } from "../utils/stall.js";
12
+ import { getEntitlementManager } from "../license/entitlements/manager.js";
13
+ import { RevisionsService } from "./revisions.js";
12
14
  import { SettingsService } from "./settings.js";
13
15
  import { getAuthProvider } from "../auth.js";
14
- import { RevisionsService } from "./revisions.js";
15
16
  import { TFAService } from "./tfa.js";
17
+ import { getLicenseManager } from "../license/manager.js";
18
+ import "../license/index.js";
16
19
  import { useEnv } from "@directus/env";
17
- import { InvalidCredentialsError, InvalidOtpError, ServiceUnavailableError, UserSuspendedError } from "@directus/errors";
20
+ import { InvalidCredentialsError, InvalidOtpError, ResourceRestrictedError, ServiceUnavailableError, UserSuspendedError } from "@directus/errors";
18
21
  import { clone, cloneDeep } from "lodash-es";
19
22
  import { Action } from "@directus/constants";
20
23
  import { performance } from "perf_hooks";
@@ -96,6 +99,7 @@ var AuthenticationService = class {
96
99
  } catch (error) {
97
100
  if (error instanceof RateLimiterRes && error.remainingPoints === 0) {
98
101
  await this.knex("directus_users").update({ status: "suspended" }).where({ id: user.id });
102
+ await getEntitlementManager().clearCache("sso_enabled", "seats");
99
103
  if (this.accountability) {
100
104
  const activity = await this.activityService.createOne({
101
105
  action: Action.UPDATE,
@@ -160,6 +164,7 @@ var AuthenticationService = class {
160
164
  user: user.id,
161
165
  ip: this.accountability?.ip ?? null
162
166
  }, { knex: this.knex });
167
+ if (await getLicenseManager().isLocked() && globalAccess.admin === false) throw new ResourceRestrictedError({ category: "login" });
163
168
  const tokenPayload = {
164
169
  id: user.id,
165
170
  role: user.role,
@@ -237,7 +242,7 @@ var AuthenticationService = class {
237
242
  share_id: "d.id",
238
243
  share_start: "d.date_start",
239
244
  share_end: "d.date_end"
240
- }).from("directus_sessions AS s").leftJoin("directus_users AS u", "s.user", "u.id").leftJoin("directus_shares AS d", "s.share", "d.id").where("s.token", refreshToken).andWhere("s.expires", ">=", /* @__PURE__ */ new Date()).andWhere((subQuery) => {
245
+ }).from("directus_sessions AS s").leftJoin("directus_users AS u", "s.user", "u.id").leftJoin("directus_shares AS d", "s.share", "d.id").where("s.token", refreshToken).andWhere("s.expires", ">=", /* @__PURE__ */ new Date()).andWhere("s.oauth_client", null).andWhere((subQuery) => {
241
246
  subQuery.whereNull("d.date_end").orWhere("d.date_end", ">=", /* @__PURE__ */ new Date());
242
247
  }).andWhere((subQuery) => {
243
248
  subQuery.whereNull("d.date_start").orWhere("d.date_start", "<=", /* @__PURE__ */ new Date());
@@ -259,6 +264,7 @@ var AuthenticationService = class {
259
264
  roles,
260
265
  ip: this.accountability?.ip ?? null
261
266
  }, { knex: this.knex });
267
+ if (await getLicenseManager().isLocked() && globalAccess.admin === false) throw new ResourceRestrictedError({ category: "login" });
262
268
  if (record.user_id) await getAuthProvider(record.user_provider).refresh({
263
269
  id: record.user_id,
264
270
  first_name: record.user_first_name,
@@ -346,12 +352,13 @@ var AuthenticationService = class {
346
352
  expires: sessionExpiration,
347
353
  ip: this.accountability?.ip,
348
354
  user_agent: this.accountability?.userAgent,
349
- origin: this.accountability?.origin
355
+ origin: this.accountability?.origin,
356
+ oauth_client: sessionRecord["oauth_client"]
350
357
  });
351
358
  return newSessionToken;
352
359
  }
353
360
  async logout(refreshToken) {
354
- const record = await this.knex.select("u.id", "u.first_name", "u.last_name", "u.email", "u.password", "u.status", "u.role", "u.provider", "u.external_identifier", "u.auth_data").from("directus_sessions as s").innerJoin("directus_users as u", "s.user", "u.id").where("s.token", refreshToken).first();
361
+ const record = await this.knex.select("u.id", "u.first_name", "u.last_name", "u.email", "u.password", "u.status", "u.role", "u.provider", "u.external_identifier", "u.auth_data").from("directus_sessions as s").innerJoin("directus_users as u", "s.user", "u.id").where("s.token", refreshToken).andWhere("s.oauth_client", null).first();
355
362
  if (record) {
356
363
  const user = record;
357
364
  await getAuthProvider(user.provider).logout(clone(user));
@@ -7,12 +7,14 @@ import { validateAccess } from "../permissions/modules/validate-access/validate-
7
7
  import { transaction } from "../utils/transaction.js";
8
8
  import { shouldClearCache } from "../utils/should-clear-cache.js";
9
9
  import { ItemsService } from "./items.js";
10
- import { fetchAllowedCollections } from "../permissions/modules/fetch-allowed-collections/fetch-allowed-collections.js";
11
10
  import { getSchema } from "../utils/get-schema.js";
11
+ import { getEntitlementManager } from "../license/entitlements/manager.js";
12
12
  import { buildCollectionAndFieldRelations } from "./fields/build-collection-and-field-relations.js";
13
13
  import { getCollectionMetaUpdates } from "./fields/get-collection-meta-updates.js";
14
14
  import { getCollectionRelationList } from "./fields/get-collection-relation-list.js";
15
15
  import { FieldsService } from "./fields.js";
16
+ import "../license/index.js";
17
+ import { fetchAllowedCollections } from "../permissions/modules/fetch-allowed-collections/fetch-allowed-collections.js";
16
18
  import { useEnv } from "@directus/env";
17
19
  import { ForbiddenError, InvalidPayloadError } from "@directus/errors";
18
20
  import { addFieldFlag } from "@directus/utils";
@@ -48,6 +50,10 @@ var CollectionsService = class CollectionsService {
48
50
  if (typeof payload.collection !== "string" || payload.collection === "") throw new InvalidPayloadError({ reason: `"collection" must be a non-empty string` });
49
51
  if (payload.collection.startsWith("directus_")) throw new InvalidPayloadError({ reason: `Collections can't start with "directus_"` });
50
52
  if (payload.collection.includes("/")) throw new InvalidPayloadError({ reason: `Collection name can't contain "/"` });
53
+ if (payload.schema && payload.meta && (!("status" in payload.meta) || payload.meta.status === "active")) await getEntitlementManager().assert("collections", {
54
+ adding: 1,
55
+ knex: this.knex
56
+ });
51
57
  payload.collection = await this.helpers.schema.parseCollectionName(payload.collection);
52
58
  const nestedActionEvents = [];
53
59
  try {
@@ -126,7 +132,10 @@ var CollectionsService = class CollectionsService {
126
132
  return payload.collection;
127
133
  } finally {
128
134
  if (shouldClearCache(this.cache, opts)) await this.cache.clear();
129
- if (opts?.autoPurgeSystemCache !== false) await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache });
135
+ if (opts?.autoPurgeSystemCache !== false) {
136
+ await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache });
137
+ await getEntitlementManager().clearCache("collections");
138
+ }
130
139
  if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
131
140
  const updatedSchema = await getSchema();
132
141
  for (const nestedActionEvent of nestedActionEvents) {
@@ -162,7 +171,10 @@ var CollectionsService = class CollectionsService {
162
171
  });
163
172
  } finally {
164
173
  if (shouldClearCache(this.cache, opts)) await this.cache.clear();
165
- if (opts?.autoPurgeSystemCache !== false) await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache });
174
+ if (opts?.autoPurgeSystemCache !== false) {
175
+ await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache });
176
+ await getEntitlementManager().clearCache("collections");
177
+ }
166
178
  if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
167
179
  const updatedSchema = await getSchema();
168
180
  for (const nestedActionEvent of nestedActionEvents) {
@@ -253,7 +265,7 @@ var CollectionsService = class CollectionsService {
253
265
  /**
254
266
  * Update a single collection by name
255
267
  */
256
- async updateOne(collectionKey, data, opts) {
268
+ async updateOne(collectionKey, payload, opts) {
257
269
  if (this.accountability && this.accountability.admin !== true) throw new ForbiddenError();
258
270
  const nestedActionEvents = [];
259
271
  try {
@@ -262,8 +274,11 @@ var CollectionsService = class CollectionsService {
262
274
  accountability: this.accountability,
263
275
  schema: this.schema
264
276
  });
265
- const payload = data;
266
277
  if (!payload.meta) return collectionKey;
278
+ if (payload.meta?.status === "active") await getEntitlementManager().assert("collections", {
279
+ adding: 1,
280
+ knex: this.knex
281
+ });
267
282
  if (!!await this.knex.select("collection").from("directus_collections").where({ collection: collectionKey }).first()) await collectionsItemsService.updateOne(collectionKey, payload.meta, {
268
283
  ...opts,
269
284
  bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params)
@@ -278,7 +293,10 @@ var CollectionsService = class CollectionsService {
278
293
  return collectionKey;
279
294
  } finally {
280
295
  if (shouldClearCache(this.cache, opts)) await this.cache.clear();
281
- if (opts?.autoPurgeSystemCache !== false) await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache });
296
+ if (opts?.autoPurgeSystemCache !== false) {
297
+ await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache });
298
+ await getEntitlementManager().clearCache("collections");
299
+ }
282
300
  if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
283
301
  const updatedSchema = await getSchema();
284
302
  for (const nestedActionEvent of nestedActionEvents) {
@@ -316,7 +334,10 @@ var CollectionsService = class CollectionsService {
316
334
  });
317
335
  } finally {
318
336
  if (shouldClearCache(this.cache, opts)) await this.cache.clear();
319
- if (opts?.autoPurgeSystemCache !== false) await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache });
337
+ if (opts?.autoPurgeSystemCache !== false) {
338
+ await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache });
339
+ await getEntitlementManager().clearCache("collections");
340
+ }
320
341
  if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
321
342
  const updatedSchema = await getSchema();
322
343
  for (const nestedActionEvent of nestedActionEvents) {
@@ -349,7 +370,10 @@ var CollectionsService = class CollectionsService {
349
370
  return collectionKeys;
350
371
  } finally {
351
372
  if (shouldClearCache(this.cache, opts)) await this.cache.clear();
352
- if (opts?.autoPurgeSystemCache !== false) await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache });
373
+ if (opts?.autoPurgeSystemCache !== false) {
374
+ await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache });
375
+ await getEntitlementManager().clearCache("collections");
376
+ }
353
377
  if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
354
378
  const updatedSchema = await getSchema();
355
379
  for (const nestedActionEvent of nestedActionEvents) {
@@ -434,7 +458,10 @@ var CollectionsService = class CollectionsService {
434
458
  return collectionKey;
435
459
  } finally {
436
460
  if (shouldClearCache(this.cache, opts)) await this.cache.clear();
437
- if (opts?.autoPurgeSystemCache !== false) await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache });
461
+ if (opts?.autoPurgeSystemCache !== false) {
462
+ await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache });
463
+ await getEntitlementManager().clearCache("collections");
464
+ }
438
465
  if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
439
466
  const updatedSchema = await getSchema();
440
467
  for (const nestedActionEvent of nestedActionEvents) {
@@ -466,7 +493,10 @@ var CollectionsService = class CollectionsService {
466
493
  return collectionKeys;
467
494
  } finally {
468
495
  if (shouldClearCache(this.cache, opts)) await this.cache.clear();
469
- if (opts?.autoPurgeSystemCache !== false) await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache });
496
+ if (opts?.autoPurgeSystemCache !== false) {
497
+ await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache });
498
+ await getEntitlementManager().clearCache("collections");
499
+ }
470
500
  if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
471
501
  const updatedSchema = await getSchema();
472
502
  for (const nestedActionEvent of nestedActionEvents) {
@@ -16,15 +16,15 @@ import getLocalType from "../utils/get-local-type.js";
16
16
  import getDefaultValue from "../utils/get-default-value.js";
17
17
  import { getSystemFieldRowsWithAuthProviders } from "../utils/get-field-system-rows.js";
18
18
  import { getSchema } from "../utils/get-schema.js";
19
+ import { sanitizeColumn } from "../utils/sanitize-schema.js";
19
20
  import { buildCollectionAndFieldRelations } from "./fields/build-collection-and-field-relations.js";
20
21
  import { getCollectionMetaUpdates } from "./fields/get-collection-meta-updates.js";
21
22
  import { getCollectionRelationList } from "./fields/get-collection-relation-list.js";
22
- import { sanitizeColumn } from "../utils/sanitize-schema.js";
23
23
  import { useEnv } from "@directus/env";
24
24
  import { ForbiddenError, InvalidPayloadError } from "@directus/errors";
25
25
  import { addFieldFlag, getRelations, toArray } from "@directus/utils";
26
26
  import { isEqual, isNil, merge } from "lodash-es";
27
- import { z } from "zod";
27
+ import { z as z$1 } from "zod";
28
28
  import { DEFAULT_NUMERIC_PRECISION, DEFAULT_NUMERIC_SCALE, KNEX_TYPES, REGEX_BETWEEN_PARENS } from "@directus/constants";
29
29
  import { createInspector } from "@directus/schema";
30
30
  import { isSystemField } from "@directus/system-data";
@@ -32,10 +32,10 @@ import { isSystemField } from "@directus/system-data";
32
32
  //#region src/services/fields.ts
33
33
  const systemFieldRows$1 = getSystemFieldRowsWithAuthProviders();
34
34
  const env = useEnv();
35
- const systemFieldUpdateSchema = z.object({
36
- collection: z.string().optional(),
37
- field: z.string().optional(),
38
- schema: z.object({ is_indexed: z.boolean().optional() }).strict()
35
+ const systemFieldUpdateSchema = z$1.object({
36
+ collection: z$1.string().optional(),
37
+ field: z$1.string().optional(),
38
+ schema: z$1.object({ is_indexed: z$1.boolean().optional() }).strict()
39
39
  }).strict();
40
40
  var FieldsService = class FieldsService {
41
41
  knex;
@@ -1,4 +1,5 @@
1
1
  import { ItemsService } from "./items.js";
2
+ import { getEntitlementManager } from "../license/entitlements/manager.js";
2
3
  import { getFlowManager } from "../flows.js";
3
4
 
4
5
  //#region src/services/flows.ts
@@ -7,12 +8,22 @@ var FlowsService = class extends ItemsService {
7
8
  super("directus_flows", options);
8
9
  }
9
10
  async createOne(data, opts) {
11
+ if (!("status" in data) || data["status"] === "active") await getEntitlementManager().assert("flows", {
12
+ adding: 1,
13
+ knex: this.knex
14
+ });
10
15
  const result = await super.createOne(data, opts);
16
+ await getEntitlementManager().clearCache("flows");
11
17
  await getFlowManager().reload();
12
18
  return result;
13
19
  }
14
20
  async updateMany(keys, data, opts) {
21
+ if ("status" in data && data["status"] === "active") await getEntitlementManager().assert("flows", {
22
+ adding: keys.length,
23
+ knex: this.knex
24
+ });
15
25
  const result = await super.updateMany(keys, data, opts);
26
+ await getEntitlementManager().clearCache("flows");
16
27
  await getFlowManager().reload();
17
28
  return result;
18
29
  }
@@ -22,6 +33,7 @@ var FlowsService = class extends ItemsService {
22
33
  reject: null
23
34
  }).whereIn("flow", keys);
24
35
  const result = await super.deleteMany(keys, opts);
36
+ await getEntitlementManager().clearCache("flows");
25
37
  await getFlowManager().reload();
26
38
  return result;
27
39
  }
@@ -1,11 +1,11 @@
1
1
  import { RelationsService } from "../../relations.js";
2
- import { FieldsService, systemFieldUpdateSchema } from "../../fields.js";
3
- import { CollectionsService } from "../../collections.js";
4
2
  import { ExtensionsService } from "../../extensions.js";
3
+ import { FieldsService, systemFieldUpdateSchema } from "../../fields.js";
5
4
  import { getCollectionType } from "./get-collection-type.js";
6
5
  import { getFieldType } from "./get-field-type.js";
7
6
  import { getRelationType } from "./get-relation-type.js";
8
7
  import "../index.js";
8
+ import { CollectionsService } from "../../collections.js";
9
9
  import { InvalidPayloadError } from "@directus/errors";
10
10
  import { isSystemField } from "@directus/system-data";
11
11
  import { GraphQLBoolean, GraphQLID, GraphQLList, GraphQLNonNull, GraphQLString } from "graphql";
@@ -4,9 +4,9 @@ import { generateHash } from "../../../utils/generate-hash.js";
4
4
  import { getSecret } from "../../../utils/get-secret.js";
5
5
  import { createDefaultAccountability } from "../../../permissions/utils/create-default-accountability.js";
6
6
  import { verifyAccessJWT } from "../../../utils/jwt.js";
7
+ import { RevisionsService } from "../../revisions.js";
7
8
  import { UsersService } from "../../users.js";
8
9
  import { getIPFromReq } from "../../../utils/get-ip-from-req.js";
9
- import { RevisionsService } from "../../revisions.js";
10
10
  import { TFAService } from "../../tfa.js";
11
11
  import { AuthenticationService } from "../../authentication.js";
12
12
  import isDirectusJWT from "../../../utils/is-directus-jwt.js";
@@ -1,9 +1,8 @@
1
1
  import database_default from "../../../database/index.js";
2
2
  import { FilesService } from "../../files.js";
3
3
  import { RelationsService } from "../../relations.js";
4
- import { FieldsService } from "../../fields.js";
5
- import { CollectionsService } from "../../collections.js";
6
4
  import { UsersService } from "../../users.js";
5
+ import { FieldsService } from "../../fields.js";
7
6
  import { fetchAccountabilityCollectionAccess } from "../../../permissions/modules/fetch-accountability-collection-access/fetch-accountability-collection-access.js";
8
7
  import { fetchAccountabilityPolicyGlobals } from "../../../permissions/modules/fetch-accountability-policy-globals/fetch-accountability-policy-globals.js";
9
8
  import { RolesService } from "../../roles.js";
@@ -19,6 +18,7 @@ import { resolveSystemAdmin } from "./system-admin.js";
19
18
  import { globalResolvers } from "./system-global.js";
20
19
  import { generateSchema } from "../schema/index.js";
21
20
  import { GraphQLService } from "../index.js";
21
+ import { CollectionsService } from "../../collections.js";
22
22
  import { useEnv } from "@directus/env";
23
23
  import { toBoolean } from "@directus/utils";
24
24
  import { GraphQLBoolean, GraphQLEnumType, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLString } from "graphql";
@@ -30,22 +30,38 @@ function injectSystemResolvers(gql, schemaComposer, { CreateCollectionTypes, Rea
30
30
  globalResolvers(gql, schemaComposer);
31
31
  const ServerInfo = schemaComposer.createObjectTC({
32
32
  name: "server_info",
33
- fields: { project: { type: new GraphQLObjectType({
34
- name: "server_info_project",
35
- fields: {
36
- project_name: { type: GraphQLString },
37
- project_descriptor: { type: GraphQLString },
38
- project_logo: { type: GraphQLString },
39
- project_color: { type: GraphQLString },
40
- default_language: { type: GraphQLString },
41
- public_foreground: { type: GraphQLString },
42
- public_background: { type: GraphQLString },
43
- public_note: { type: GraphQLString },
44
- custom_css: { type: GraphQLString },
45
- public_registration: { type: GraphQLBoolean },
46
- public_registration_verify_email: { type: GraphQLBoolean }
47
- }
48
- }) } }
33
+ fields: {
34
+ project: { type: new GraphQLObjectType({
35
+ name: "server_info_project",
36
+ fields: {
37
+ project_name: { type: GraphQLString },
38
+ project_descriptor: { type: GraphQLString },
39
+ project_logo: { type: GraphQLString },
40
+ project_color: { type: GraphQLString },
41
+ default_language: { type: GraphQLString },
42
+ public_foreground: { type: GraphQLString },
43
+ public_background: { type: GraphQLString },
44
+ public_note: { type: GraphQLString },
45
+ custom_css: { type: GraphQLString },
46
+ public_registration: { type: GraphQLBoolean },
47
+ public_registration_verify_email: { type: GraphQLBoolean }
48
+ }
49
+ }) },
50
+ license: { type: new GraphQLObjectType({
51
+ name: "server_info_license",
52
+ fields: {
53
+ source: { type: GraphQLString },
54
+ entitlements: { type: new GraphQLObjectType({
55
+ name: "server_info_license_entitlements",
56
+ fields: {
57
+ production_enabled: { type: new GraphQLNonNull(GraphQLBoolean) },
58
+ ai_translations_enabled: { type: new GraphQLNonNull(GraphQLBoolean) },
59
+ display_powered_by: { type: new GraphQLNonNull(GraphQLString) }
60
+ }
61
+ }) }
62
+ }
63
+ }) }
64
+ }
49
65
  });
50
66
  if (gql.accountability?.user) ServerInfo.addFields({
51
67
  rateLimit: env["RATE_LIMITER_ENABLED"] ? { type: new GraphQLObjectType({
@@ -144,17 +160,17 @@ function injectSystemResolvers(gql, schemaComposer, { CreateCollectionTypes, Rea
144
160
  schema: gql.schema
145
161
  }).serverInfo();
146
162
  }, "server_info")
147
- },
148
- server_health: {
149
- type: GraphQLJSON,
150
- resolve: dedupeResolver(async () => {
151
- return await new ServerService({
152
- accountability: gql.accountability,
153
- schema: gql.schema
154
- }).health();
155
- }, "server_health")
156
163
  }
157
164
  });
165
+ if (toBoolean(env["HEALTHCHECK_ENABLED"]) !== false) schemaComposer.Query.addFields({ server_health: {
166
+ type: GraphQLJSON,
167
+ resolve: dedupeResolver(async () => {
168
+ return await new ServerService({
169
+ accountability: gql.accountability,
170
+ schema: gql.schema
171
+ }).health();
172
+ }, "server_health")
173
+ } });
158
174
  if ("directus_collections" in schema.read.collections) {
159
175
  const Collection = getCollectionType(schemaComposer, schema, "read");
160
176
  schemaComposer.Query.addFields({
@@ -1,8 +1,9 @@
1
+ import { applyFunctionToColumnName } from "../../../database/run-ast/utils/apply-function-to-column-name.js";
1
2
  import { getGraphQLType } from "../../../utils/get-graphql-type.js";
2
3
  import { SYSTEM_DENY_LIST } from "./index.js";
3
4
  import { mapKeys, pick } from "lodash-es";
4
5
  import { GENERATE_SPECIAL } from "@directus/constants";
5
- import { GraphQLID, GraphQLInt, GraphQLNonNull, GraphQLScalarType, GraphQLUnionType } from "graphql";
6
+ import { GraphQLID, GraphQLInt, GraphQLNonNull, GraphQLScalarType, GraphQLString, GraphQLUnionType } from "graphql";
6
7
  import { GraphQLJSON } from "graphql-compose";
7
8
 
8
9
  //#region src/services/graphql/schema/get-types.ts
@@ -81,12 +82,32 @@ function getTypes(schemaComposer, scope, schema, inconsistentFields, action) {
81
82
  return mapKeys(pick(obj, Object.keys(DateTimeFunctions.getFields()).map((key) => `${field.field}_${key}`)), (_value, key) => key.substring(field.field.length + 1));
82
83
  }
83
84
  };
84
- if (field.type === "json" || field.type === "alias") acc[`${field.field}_func`] = {
85
+ if (field.type === "alias") acc[`${field.field}_func`] = {
85
86
  type: CountFunctions,
86
87
  resolve: (obj) => {
87
88
  return mapKeys(pick(obj, Object.keys(CountFunctions.getFields()).map((key) => `${field.field}_${key}`)), (_value, key) => key.substring(field.field.length + 1));
88
89
  }
89
90
  };
91
+ if (field.type === "json") {
92
+ const JsonFieldFunctions = schemaComposer.createObjectTC({
93
+ name: `${collection.collection}_${field.field}_func`,
94
+ fields: {
95
+ count: {
96
+ type: GraphQLInt,
97
+ resolve: (obj) => obj[`${field.field}_count`]
98
+ },
99
+ json: {
100
+ type: GraphQLJSON,
101
+ args: { path: { type: new GraphQLNonNull(GraphQLString) } },
102
+ resolve: (obj, { path }) => obj[applyFunctionToColumnName(`json(${field.field}, ${path})`)]
103
+ }
104
+ }
105
+ });
106
+ acc[`${field.field}_func`] = {
107
+ type: JsonFieldFunctions,
108
+ resolve: (obj) => obj
109
+ };
110
+ }
90
111
  }
91
112
  return acc;
92
113
  }, {})
@@ -98,26 +119,26 @@ function getTypes(schemaComposer, scope, schema, inconsistentFields, action) {
98
119
  CollectionTypes[relation.collection]?.addFields({ [relation.field]: {
99
120
  type: CollectionTypes[relation.related_collection],
100
121
  resolve: (obj, _, __, info) => {
101
- return obj[info?.path?.key ?? relation.field];
122
+ return obj[info?.path?.key] ?? obj[info?.fieldName ?? relation.field];
102
123
  }
103
124
  } });
104
125
  VersionTypes[relation.collection]?.addFields({ [relation.field]: {
105
126
  type: GraphQLJSON,
106
127
  resolve: (obj, _, __, info) => {
107
- return obj[info?.path?.key ?? relation.field];
128
+ return obj[info?.path?.key] ?? obj[info?.fieldName ?? relation.field];
108
129
  }
109
130
  } });
110
131
  if (relation.meta?.one_field) {
111
132
  CollectionTypes[relation.related_collection]?.addFields({ [relation.meta.one_field]: {
112
133
  type: [CollectionTypes[relation.collection]],
113
134
  resolve: (obj, _, __, info) => {
114
- return obj[info?.path?.key ?? relation.meta.one_field];
135
+ return obj[info?.path?.key] ?? obj[info?.fieldName ?? relation.meta.one_field];
115
136
  }
116
137
  } });
117
138
  if (scope === "items") VersionTypes[relation.related_collection]?.addFields({ [relation.meta.one_field]: {
118
139
  type: GraphQLJSON,
119
140
  resolve: (obj, _, __, info) => {
120
- return obj[info?.path?.key ?? relation.meta.one_field];
141
+ return obj[info?.path?.key] ?? obj[info?.fieldName ?? relation.meta.one_field];
121
142
  }
122
143
  } });
123
144
  }
@@ -139,7 +160,7 @@ function getTypes(schemaComposer, scope, schema, inconsistentFields, action) {
139
160
  }
140
161
  }),
141
162
  resolve: (obj, _, __, info) => {
142
- return obj[info?.path?.key ?? relation.field];
163
+ return obj[info?.path?.key] ?? obj[info?.fieldName ?? relation.field];
143
164
  }
144
165
  } });
145
166
  return {