@directus/api 35.1.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 (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 +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 +45 -1
  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/schema.js +2 -2
  63. package/dist/controllers/server.js +38 -9
  64. package/dist/controllers/shares.js +1 -1
  65. package/dist/controllers/translations.js +1 -1
  66. package/dist/controllers/users.js +1 -1
  67. package/dist/controllers/utils.js +2 -2
  68. package/dist/controllers/versions.js +12 -5
  69. package/dist/database/get-ast-from-query/lib/convert-wildcards.js +10 -1
  70. package/dist/database/get-ast-from-query/lib/parse-fields.js +2 -1
  71. package/dist/database/helpers/fn/dialects/mysql.js +7 -12
  72. package/dist/database/helpers/fn/dialects/oracle.js +3 -4
  73. package/dist/database/helpers/fn/dialects/postgres.js +4 -26
  74. package/dist/database/helpers/fn/json/mysql-json-path.js +22 -0
  75. package/dist/database/helpers/fn/json/parse-function.js +14 -6
  76. package/dist/database/helpers/fn/json/postgres-json-path.js +54 -0
  77. package/dist/database/migrations/20260110A-add-ai-provider-settings.js +4 -4
  78. package/dist/database/migrations/20260217A-null-item-versions.js +14 -0
  79. package/dist/database/migrations/20260312A-add-ai-translation-settings.js +18 -0
  80. package/dist/database/migrations/20260507A-add-licensing.js +22 -0
  81. package/dist/database/migrations/20260512A-add-autosave-revision-interval.js +14 -0
  82. package/dist/database/migrations/20260512B-add-mcp-oauth.js +87 -0
  83. package/dist/database/run-ast/lib/apply-query/filter/operator.js +116 -33
  84. package/dist/database/run-ast/lib/apply-query/index.js +4 -1
  85. package/dist/database/run-ast/lib/apply-query/sort.js +17 -7
  86. package/dist/database/run-ast/lib/get-db-query.js +21 -9
  87. package/dist/database/run-ast/lib/parse-current-level.js +2 -1
  88. package/dist/database/run-ast/run-ast.js +2 -1
  89. package/dist/database/run-ast/utils/get-column.js +2 -1
  90. package/dist/extensions/lib/installation/manager.js +3 -3
  91. package/dist/extensions/lib/sandbox/register/operation.js +1 -1
  92. package/dist/extensions/lib/sync/sync.js +2 -2
  93. package/dist/extensions/manager.js +5 -5
  94. package/dist/flows.js +12 -10
  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 +41 -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 +34 -18
  137. package/dist/services/graphql/schema/get-types.js +23 -2
  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/schema.js +2 -2
  155. package/dist/services/server.js +21 -4
  156. package/dist/services/settings.js +37 -3
  157. package/dist/services/users.js +13 -6
  158. package/dist/services/utils.js +6 -1
  159. package/dist/services/versions.js +138 -69
  160. package/dist/utils/calculate-field-depth.js +1 -0
  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/parse-oauth-scope.js +12 -0
  171. package/dist/utils/sanitize-query.js +1 -1
  172. package/dist/utils/split-field-path.js +29 -0
  173. package/dist/utils/transaction.js +2 -2
  174. package/dist/utils/translations-validation.js +2 -2
  175. package/dist/utils/validate-diff.js +7 -3
  176. package/dist/utils/validate-query.js +35 -4
  177. package/dist/utils/validate-user-count-integrity.js +28 -5
  178. package/dist/utils/verify-session-jwt.js +5 -2
  179. package/dist/utils/versioning/handle-version.js +130 -48
  180. package/dist/utils/versioning/remove-circular.js +17 -0
  181. package/dist/websocket/authenticate.js +2 -1
  182. package/dist/websocket/collab/collab.js +1 -1
  183. package/dist/websocket/collab/room.js +1 -1
  184. package/dist/websocket/controllers/base.js +12 -0
  185. package/dist/websocket/controllers/graphql.js +1 -1
  186. package/dist/websocket/handlers/subscribe.js +1 -1
  187. package/dist/websocket/messages.js +64 -64
  188. package/dist/websocket/utils/items.js +2 -2
  189. package/license +90 -80
  190. package/package.json +33 -33
  191. package/dist/controllers/mcp.js +0 -31
  192. package/dist/utils/job-queue.js +0 -24
@@ -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";
@@ -47,6 +49,11 @@ var CollectionsService = class CollectionsService {
47
49
  if (!("collection" in payload)) throw new InvalidPayloadError({ reason: `"collection" is required` });
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_"` });
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
+ });
50
57
  payload.collection = await this.helpers.schema.parseCollectionName(payload.collection);
51
58
  const nestedActionEvents = [];
52
59
  try {
@@ -125,7 +132,10 @@ var CollectionsService = class CollectionsService {
125
132
  return payload.collection;
126
133
  } finally {
127
134
  if (shouldClearCache(this.cache, opts)) await this.cache.clear();
128
- 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
+ }
129
139
  if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
130
140
  const updatedSchema = await getSchema();
131
141
  for (const nestedActionEvent of nestedActionEvents) {
@@ -161,7 +171,10 @@ var CollectionsService = class CollectionsService {
161
171
  });
162
172
  } finally {
163
173
  if (shouldClearCache(this.cache, opts)) await this.cache.clear();
164
- 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
+ }
165
178
  if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
166
179
  const updatedSchema = await getSchema();
167
180
  for (const nestedActionEvent of nestedActionEvents) {
@@ -252,7 +265,7 @@ var CollectionsService = class CollectionsService {
252
265
  /**
253
266
  * Update a single collection by name
254
267
  */
255
- async updateOne(collectionKey, data, opts) {
268
+ async updateOne(collectionKey, payload, opts) {
256
269
  if (this.accountability && this.accountability.admin !== true) throw new ForbiddenError();
257
270
  const nestedActionEvents = [];
258
271
  try {
@@ -261,8 +274,11 @@ var CollectionsService = class CollectionsService {
261
274
  accountability: this.accountability,
262
275
  schema: this.schema
263
276
  });
264
- const payload = data;
265
277
  if (!payload.meta) return collectionKey;
278
+ if (payload.meta?.status === "active") await getEntitlementManager().assert("collections", {
279
+ adding: 1,
280
+ knex: this.knex
281
+ });
266
282
  if (!!await this.knex.select("collection").from("directus_collections").where({ collection: collectionKey }).first()) await collectionsItemsService.updateOne(collectionKey, payload.meta, {
267
283
  ...opts,
268
284
  bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params)
@@ -277,7 +293,10 @@ var CollectionsService = class CollectionsService {
277
293
  return collectionKey;
278
294
  } finally {
279
295
  if (shouldClearCache(this.cache, opts)) await this.cache.clear();
280
- 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
+ }
281
300
  if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
282
301
  const updatedSchema = await getSchema();
283
302
  for (const nestedActionEvent of nestedActionEvents) {
@@ -315,7 +334,10 @@ var CollectionsService = class CollectionsService {
315
334
  });
316
335
  } finally {
317
336
  if (shouldClearCache(this.cache, opts)) await this.cache.clear();
318
- 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
+ }
319
341
  if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
320
342
  const updatedSchema = await getSchema();
321
343
  for (const nestedActionEvent of nestedActionEvents) {
@@ -348,7 +370,10 @@ var CollectionsService = class CollectionsService {
348
370
  return collectionKeys;
349
371
  } finally {
350
372
  if (shouldClearCache(this.cache, opts)) await this.cache.clear();
351
- 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
+ }
352
377
  if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
353
378
  const updatedSchema = await getSchema();
354
379
  for (const nestedActionEvent of nestedActionEvents) {
@@ -433,7 +458,10 @@ var CollectionsService = class CollectionsService {
433
458
  return collectionKey;
434
459
  } finally {
435
460
  if (shouldClearCache(this.cache, opts)) await this.cache.clear();
436
- 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
+ }
437
465
  if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
438
466
  const updatedSchema = await getSchema();
439
467
  for (const nestedActionEvent of nestedActionEvents) {
@@ -465,7 +493,10 @@ var CollectionsService = class CollectionsService {
465
493
  return collectionKeys;
466
494
  } finally {
467
495
  if (shouldClearCache(this.cache, opts)) await this.cache.clear();
468
- 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
+ }
469
500
  if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
470
501
  const updatedSchema = await getSchema();
471
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({
@@ -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
  }, {})
@@ -65,6 +65,14 @@ async function getQuery(rawQuery, schema, selections, variableValues, accountabi
65
65
  for (const subSelection of selection.selectionSet.selections) {
66
66
  if (subSelection.kind !== "Field") continue;
67
67
  if (subSelection.name.value.startsWith("__")) continue;
68
+ if (subSelection.name.value === "json" && subSelection.arguments?.length) {
69
+ const pathArg = subSelection.arguments.find((a) => a.name.value === "path");
70
+ if (pathArg) {
71
+ const pathValue = parseArgs([pathArg], variableValues).path;
72
+ children.push(`json(${rootField}, ${pathValue})`);
73
+ continue;
74
+ }
75
+ }
68
76
  children.push(`${subSelection.name.value}(${rootField})`);
69
77
  }
70
78
  } else children = await parseFields(selection.selectionSet.selections, currentAlias ?? current, childCollection, selection.kind === "Field" ? selection.name.value : void 0);
@@ -6,6 +6,7 @@ import { GraphQLGeoJSON } from "../types/geojson.js";
6
6
  import { GraphQLHash } from "../types/hash.js";
7
7
  import { getGraphQLType } from "../../../utils/get-graphql-type.js";
8
8
  import { resolveQuery } from "../resolvers/query.js";
9
+ import { GraphQLJsonFilter } from "../types/json-filter.js";
9
10
  import { GraphQLStringOrFloat } from "../types/string-or-float.js";
10
11
  import { getTypes } from "./get-types.js";
11
12
  import { SYSTEM_DENY_LIST } from "./index.js";
@@ -154,6 +155,14 @@ async function getReadableTypes(gql, schemaComposer, schema, inconsistentFields)
154
155
  _nempty: { type: GraphQLBoolean }
155
156
  }
156
157
  });
158
+ const JsonFilterOperators = schemaComposer.createInputTC({
159
+ name: "json_filter_operators",
160
+ fields: {
161
+ _json: { type: GraphQLJsonFilter },
162
+ _null: { type: GraphQLBoolean },
163
+ _nnull: { type: GraphQLBoolean }
164
+ }
165
+ });
157
166
  const CountFunctionFilterOperators = schemaComposer.createInputTC({
158
167
  name: "count_function_filter_operators",
159
168
  fields: { count: { type: NumberFilterOperators } }
@@ -213,6 +222,9 @@ async function getReadableTypes(gql, schemaComposer, schema, inconsistentFields)
213
222
  case GraphQLDate:
214
223
  filterOperatorType = DateFilterOperators;
215
224
  break;
225
+ case GraphQLJSON:
226
+ filterOperatorType = JsonFilterOperators;
227
+ break;
216
228
  case GraphQLGeoJSON:
217
229
  filterOperatorType = GeometryFilterOperators;
218
230
  break;