@directus/api 35.2.0 → 36.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/dist/ai/chat/models/chat-request.js +48 -48
  2. package/dist/ai/chat/models/object-request.js +6 -6
  3. package/dist/ai/chat/models/providers.js +14 -14
  4. package/dist/ai/chat/utils/parse-json-schema-7.js +22 -22
  5. package/dist/ai/mcp/server.js +44 -6
  6. package/dist/ai/mcp/utils.js +31 -0
  7. package/dist/ai/tools/assets/index.js +3 -3
  8. package/dist/ai/tools/collections/index.js +18 -18
  9. package/dist/ai/tools/fields/index.js +18 -18
  10. package/dist/ai/tools/files/index.js +18 -18
  11. package/dist/ai/tools/flows/index.js +16 -16
  12. package/dist/ai/tools/folders/index.js +18 -18
  13. package/dist/ai/tools/items/index.js +17 -17
  14. package/dist/ai/tools/operations/index.js +16 -16
  15. package/dist/ai/tools/relations/index.js +22 -22
  16. package/dist/ai/tools/schema/index.js +3 -3
  17. package/dist/ai/tools/schema.js +159 -159
  18. package/dist/ai/tools/system/index.js +3 -3
  19. package/dist/ai/tools/trigger-flow/index.js +3 -3
  20. package/dist/app.js +35 -11
  21. package/dist/auth/drivers/ldap.js +3 -1
  22. package/dist/auth/drivers/local.js +2 -0
  23. package/dist/auth/drivers/oauth2.js +3 -1
  24. package/dist/auth/drivers/openid.js +3 -1
  25. package/dist/auth/drivers/saml.js +2 -0
  26. package/dist/auth/utils/check-local-disabled.js +16 -0
  27. package/dist/auth/utils/check-sso-enabled.js +14 -0
  28. package/dist/auth.js +8 -5
  29. package/dist/cli/commands/bootstrap/index.js +3 -0
  30. package/dist/cli/commands/cache/clear.js +6 -1
  31. package/dist/cli/commands/roles/create.js +4 -1
  32. package/dist/cli/commands/users/create.js +3 -0
  33. package/dist/constants.js +8 -1
  34. package/dist/controllers/access.js +1 -1
  35. package/dist/controllers/activity.js +2 -1
  36. package/dist/controllers/assets.js +2 -0
  37. package/dist/controllers/auth.js +13 -5
  38. package/dist/controllers/collections.js +1 -1
  39. package/dist/controllers/comments.js +1 -1
  40. package/dist/controllers/dashboards.js +1 -1
  41. package/dist/controllers/fields.js +1 -1
  42. package/dist/controllers/files.js +3 -1
  43. package/dist/controllers/flows.js +6 -5
  44. package/dist/controllers/folders.js +1 -1
  45. package/dist/controllers/graphql.js +2 -0
  46. package/dist/controllers/items.js +3 -1
  47. package/dist/controllers/license.js +119 -0
  48. package/dist/controllers/mcp/index.js +38 -0
  49. package/dist/controllers/mcp/oauth-clients.js +68 -0
  50. package/dist/controllers/mcp/oauth-consent-page.js +316 -0
  51. package/dist/controllers/mcp/oauth.js +381 -0
  52. package/dist/controllers/mcp/templates/oauth-consent.liquid +62 -0
  53. package/dist/controllers/mcp/templates/oauth-error.liquid +28 -0
  54. package/dist/controllers/notifications.js +1 -1
  55. package/dist/controllers/operations.js +1 -1
  56. package/dist/controllers/panels.js +1 -1
  57. package/dist/controllers/permissions.js +1 -1
  58. package/dist/controllers/policies.js +1 -1
  59. package/dist/controllers/presets.js +1 -1
  60. package/dist/controllers/revisions.js +3 -2
  61. package/dist/controllers/roles.js +1 -1
  62. package/dist/controllers/server.js +38 -10
  63. package/dist/controllers/shares.js +1 -1
  64. package/dist/controllers/translations.js +1 -1
  65. package/dist/controllers/users.js +1 -1
  66. package/dist/controllers/utils.js +2 -2
  67. package/dist/controllers/versions.js +12 -5
  68. package/dist/database/get-ast-from-query/lib/convert-wildcards.js +10 -1
  69. package/dist/database/get-ast-from-query/lib/parse-fields.js +2 -1
  70. package/dist/database/helpers/fn/dialects/mysql.js +7 -12
  71. package/dist/database/helpers/fn/dialects/oracle.js +3 -4
  72. package/dist/database/helpers/fn/dialects/postgres.js +4 -26
  73. package/dist/database/helpers/fn/json/mysql-json-path.js +22 -0
  74. package/dist/database/helpers/fn/json/parse-function.js +14 -6
  75. package/dist/database/helpers/fn/json/postgres-json-path.js +54 -0
  76. package/dist/database/migrations/20260110A-add-ai-provider-settings.js +4 -4
  77. package/dist/database/migrations/20260217A-null-item-versions.js +14 -0
  78. package/dist/database/migrations/20260312A-add-ai-translation-settings.js +18 -0
  79. package/dist/database/migrations/20260507A-add-licensing.js +22 -0
  80. package/dist/database/migrations/20260512A-add-autosave-revision-interval.js +14 -0
  81. package/dist/database/migrations/20260512B-add-mcp-oauth.js +87 -0
  82. package/dist/database/run-ast/lib/apply-query/filter/operator.js +116 -33
  83. package/dist/database/run-ast/lib/apply-query/index.js +4 -1
  84. package/dist/database/run-ast/lib/apply-query/sort.js +17 -7
  85. package/dist/database/run-ast/lib/get-db-query.js +21 -9
  86. package/dist/database/run-ast/lib/parse-current-level.js +2 -1
  87. package/dist/database/run-ast/run-ast.js +2 -1
  88. package/dist/database/run-ast/utils/get-column.js +2 -1
  89. package/dist/database/run-ast/utils/merge-with-parent-items.js +5 -3
  90. package/dist/extensions/lib/installation/manager.js +1 -1
  91. package/dist/extensions/lib/sandbox/register/operation.js +1 -1
  92. package/dist/extensions/lib/sync/sync.js +1 -1
  93. package/dist/extensions/manager.js +3 -3
  94. package/dist/flows.js +5 -5
  95. package/dist/license/entitlements/lib/collections.js +37 -0
  96. package/dist/license/entitlements/lib/custom-llms-enabled.js +18 -0
  97. package/dist/license/entitlements/lib/custom-permission-rules-enabled.js +41 -0
  98. package/dist/license/entitlements/lib/flows.js +29 -0
  99. package/dist/license/entitlements/lib/seats.js +103 -0
  100. package/dist/license/entitlements/lib/sso-enabled.js +45 -0
  101. package/dist/license/entitlements/manager.js +256 -0
  102. package/dist/license/index.js +4 -0
  103. package/dist/license/manager.js +505 -0
  104. package/dist/license/utils/compute-license-status.js +27 -0
  105. package/dist/license/utils/get-core-grace-expires-at.js +38 -0
  106. package/dist/license/utils/get-license-key.js +23 -0
  107. package/dist/license/utils/get-license-token.js +23 -0
  108. package/dist/license/utils/handle-license-error.js +41 -0
  109. package/dist/license/utils/is-in-core-grace-period.js +11 -0
  110. package/dist/license/utils/is-sso-bypass-allowed.js +21 -0
  111. package/dist/license/utils/use-rpc.js +33 -0
  112. package/dist/middleware/cache.js +4 -1
  113. package/dist/middleware/error-handler.js +11 -0
  114. package/dist/middleware/extract-token.js +11 -2
  115. package/dist/middleware/is-admin.js +16 -0
  116. package/dist/middleware/is-locked.js +16 -0
  117. package/dist/middleware/mcp-oauth-guard.js +23 -0
  118. package/dist/middleware/request-counter.js +5 -2
  119. package/dist/packages/types/dist/index.js +117 -122
  120. package/dist/permissions/modules/process-ast/utils/extract-paths-from-query.js +10 -1
  121. package/dist/permissions/utils/get-unaliased-field-key.js +2 -1
  122. package/dist/request/is-denied-ip.js +2 -0
  123. package/dist/schedules/license.js +31 -0
  124. package/dist/schedules/oauth-cleanup.js +26 -0
  125. package/dist/schedules/retention.js +1 -1
  126. package/dist/schedules/telemetry.js +4 -1
  127. package/dist/schedules/tus.js +1 -1
  128. package/dist/schedules/utils/duration-to-cron.js +36 -0
  129. package/dist/services/activity.js +15 -0
  130. package/dist/services/authentication.js +12 -5
  131. package/dist/services/collections.js +40 -10
  132. package/dist/services/fields.js +6 -6
  133. package/dist/services/flows.js +12 -0
  134. package/dist/services/graphql/resolvers/system-admin.js +2 -2
  135. package/dist/services/graphql/resolvers/system-global.js +1 -1
  136. package/dist/services/graphql/resolvers/system.js +43 -27
  137. package/dist/services/graphql/schema/get-types.js +28 -7
  138. package/dist/services/graphql/schema/parse-query.js +8 -0
  139. package/dist/services/graphql/schema/read.js +12 -0
  140. package/dist/services/graphql/types/json-filter.js +30 -0
  141. package/dist/services/index.js +6 -6
  142. package/dist/services/items.js +32 -14
  143. package/dist/services/mcp-oauth/cimd.js +307 -0
  144. package/dist/services/mcp-oauth/index.js +1185 -0
  145. package/dist/services/mcp-oauth/types/error.js +22 -0
  146. package/dist/services/mcp-oauth/utils/cimd-egress.js +182 -0
  147. package/dist/services/mcp-oauth/utils/domain.js +21 -0
  148. package/dist/services/mcp-oauth/utils/loopback.js +11 -0
  149. package/dist/services/mcp-oauth/utils/redirect.js +84 -0
  150. package/dist/services/mcp-oauth/utils/registration-debug.js +131 -0
  151. package/dist/services/payload.js +2 -1
  152. package/dist/services/permissions.js +31 -9
  153. package/dist/services/revisions.js +15 -0
  154. package/dist/services/server.js +66 -68
  155. package/dist/services/settings.js +37 -3
  156. package/dist/services/users.js +23 -6
  157. package/dist/services/utils.js +6 -1
  158. package/dist/services/versions.js +160 -70
  159. package/dist/utils/calculate-field-depth.js +1 -0
  160. package/dist/utils/create-admin.js +3 -3
  161. package/dist/utils/deep-freeze.js +24 -0
  162. package/dist/utils/extract-function-name.js +13 -0
  163. package/dist/utils/generate-translations.js +5 -5
  164. package/dist/utils/get-accountability-for-token.js +13 -1
  165. package/dist/utils/get-cache-key.js +1 -1
  166. package/dist/utils/get-history-filter-query.js +22 -0
  167. package/dist/utils/get-schema.js +2 -2
  168. package/dist/utils/get-service.js +3 -3
  169. package/dist/utils/is-admin.js +9 -0
  170. package/dist/utils/is-unauthenticated.js +15 -0
  171. package/dist/utils/parse-oauth-scope.js +12 -0
  172. package/dist/utils/sanitize-query.js +2 -2
  173. package/dist/utils/split-field-path.js +29 -0
  174. package/dist/utils/store.js +1 -1
  175. package/dist/utils/transaction.js +2 -2
  176. package/dist/utils/translations-validation.js +2 -2
  177. package/dist/utils/validate-query.js +35 -4
  178. package/dist/utils/validate-user-count-integrity.js +28 -5
  179. package/dist/utils/verify-session-jwt.js +5 -2
  180. package/dist/utils/versioning/handle-version.js +131 -48
  181. package/dist/utils/versioning/remove-circular.js +17 -0
  182. package/dist/websocket/authenticate.js +2 -1
  183. package/dist/websocket/collab/collab.js +1 -1
  184. package/dist/websocket/collab/room.js +1 -1
  185. package/dist/websocket/controllers/base.js +12 -0
  186. package/dist/websocket/controllers/graphql.js +1 -1
  187. package/dist/websocket/handlers/subscribe.js +1 -1
  188. package/dist/websocket/messages.js +64 -64
  189. package/dist/websocket/utils/items.js +2 -2
  190. package/license +90 -80
  191. package/package.json +33 -32
  192. package/dist/controllers/mcp.js +0 -31
@@ -1,11 +1,11 @@
1
1
  import async_handler_default from "../utils/async-handler.js";
2
2
  import { DEFAULT_AUTH_PROVIDER } from "../constants.js";
3
3
  import { getDatabase } from "../database/index.js";
4
- import { sanitizeQuery } from "../utils/sanitize-query.js";
5
4
  import { UsersService } from "../services/users.js";
6
5
  import { respond } from "../middleware/respond.js";
7
6
  import { TFAService } from "../services/tfa.js";
8
7
  import { AuthenticationService } from "../services/authentication.js";
8
+ import { sanitizeQuery } from "../utils/sanitize-query.js";
9
9
  import rate_limiter_registration_default from "../middleware/rate-limiter-registration.js";
10
10
  import { MetaService } from "../services/meta.js";
11
11
  import use_collection_default from "../middleware/use-collection.js";
@@ -3,11 +3,11 @@ import { useLogger } from "../logger/index.js";
3
3
  import { clearSystemCache } from "../cache.js";
4
4
  import { getDatabase } from "../database/index.js";
5
5
  import { generateHash } from "../utils/generate-hash.js";
6
- import { sanitizeQuery } from "../utils/sanitize-query.js";
6
+ import { RevisionsService } from "../services/revisions.js";
7
7
  import { ExportService, ImportService } from "../services/import-export.js";
8
8
  import { respond } from "../middleware/respond.js";
9
9
  import { resolveLoginRedirect } from "../auth/utils/resolve-login-redirect.js";
10
- import { RevisionsService } from "../services/revisions.js";
10
+ import { sanitizeQuery } from "../utils/sanitize-query.js";
11
11
  import { UtilsService } from "../services/utils.js";
12
12
  import collection_exists_default from "../middleware/collection-exists.js";
13
13
  import { generateTranslations } from "../utils/generate-translations.js";
@@ -1,11 +1,11 @@
1
1
  import async_handler_default from "../utils/async-handler.js";
2
- import { sanitizeQuery } from "../utils/sanitize-query.js";
3
2
  import { respond } from "../middleware/respond.js";
3
+ import { sanitizeQuery } from "../utils/sanitize-query.js";
4
4
  import { MetaService } from "../services/meta.js";
5
5
  import { VersionsService } from "../services/versions.js";
6
6
  import use_collection_default from "../middleware/use-collection.js";
7
7
  import { validateBatch } from "../middleware/validate-batch.js";
8
- import { ErrorCode, InvalidPayloadError, isDirectusError } from "@directus/errors";
8
+ import { ErrorCode, ForbiddenError, isDirectusError } from "@directus/errors";
9
9
  import express from "express";
10
10
  import { assign } from "lodash-es";
11
11
 
@@ -131,6 +131,7 @@ router.get("/:pk/compare", async_handler_default(async (req, res, next) => {
131
131
  schema: req.schema
132
132
  });
133
133
  const version = await service.readOne(req.params["pk"]);
134
+ if (!version.item) throw new ForbiddenError({ reason: `Version with key ${req.params["pk"]} does not have an associated item` });
134
135
  const { outdated, mainHash } = await service.verifyHash(version["collection"], version["item"], version["hash"]);
135
136
  const delta = version.delta ?? {};
136
137
  delta[req.schema.collections[version.collection].primary] = version.item;
@@ -149,16 +150,22 @@ router.post("/:pk/save", async_handler_default(async (req, res, next) => {
149
150
  schema: req.schema
150
151
  });
151
152
  const version = await service.readOne(req.params["pk"]);
152
- const result = assign(await service.getMainItem(version["collection"], version["item"]), await service.save(req.params["pk"], req.body));
153
+ let mainItem = {};
154
+ if (version.item) mainItem = await service.getMainItem(version["collection"], version["item"]);
155
+ const patchRevision = req.query["patchRevision"] !== void 0 && req.query["patchRevision"] !== "false";
156
+ const updatedVersion = await service.save(req.params["pk"], req.body, { patchRevision });
157
+ const result = assign(mainItem, updatedVersion);
153
158
  res.locals["payload"] = { data: result || null };
154
159
  return next();
155
160
  }), respond);
156
161
  router.post("/:pk/promote", async_handler_default(async (req, res, next) => {
157
- if (typeof req.body.mainHash !== "string") throw new InvalidPayloadError({ reason: `"mainHash" field is required` });
158
162
  const updatedItemKey = await new VersionsService({
159
163
  accountability: req.accountability,
160
164
  schema: req.schema
161
- }).promote(req.params["pk"], req.body.mainHash, req.body?.["fields"]);
165
+ }).promote(req.params["pk"], {
166
+ mainHash: req.body?.mainHash,
167
+ fields: req.body?.["fields"]
168
+ });
162
169
  res.locals["payload"] = { data: updatedItemKey || null };
163
170
  return next();
164
171
  }), respond);
@@ -1,3 +1,5 @@
1
+ import { parseJsonFunction } from "../../helpers/fn/json/parse-function.js";
2
+ import { extractFunctionName } from "../../../utils/extract-function-name.js";
1
3
  import { parseFilterKey } from "../../../utils/parse-filter-key.js";
2
4
  import { fetchAllowedFields } from "../../../permissions/modules/fetch-allowed-fields/fetch-allowed-fields.js";
3
5
  import { getRelation } from "@directus/utils";
@@ -23,7 +25,14 @@ async function convertWildcards(options, context) {
23
25
  if (allowedFields.includes("*")) fields.splice(index, 1, ...fieldsInCollection, ...aliases);
24
26
  else {
25
27
  const allowedAliases = aliases.filter((fieldKey$1) => {
26
- const { fieldName } = parseFilterKey(options.alias[fieldKey$1]);
28
+ const aliasValue = options.alias[fieldKey$1];
29
+ if (extractFunctionName(aliasValue) === "json") try {
30
+ const { field } = parseJsonFunction(aliasValue);
31
+ return allowedFields.includes(field);
32
+ } catch {
33
+ return false;
34
+ }
35
+ const { fieldName } = parseFilterKey(aliasValue);
27
36
  return allowedFields.includes(fieldName);
28
37
  });
29
38
  fields.splice(index, 1, ...allowedFields, ...allowedAliases);
@@ -1,3 +1,4 @@
1
+ import { splitFieldPath } from "../../../utils/split-field-path.js";
1
2
  import { fetchPolicies } from "../../../permissions/lib/fetch-policies.js";
2
3
  import { fetchPermissions } from "../../../permissions/lib/fetch-permissions.js";
3
4
  import { getAllowedSort } from "../utils/get-allowed-sort.js";
@@ -68,7 +69,7 @@ async function parseFields(options, context) {
68
69
  }
69
70
  const isRelationalFunctionCall = isFunctionCall && name.includes(".") && name.indexOf(".") < name.indexOf("(");
70
71
  if ((!isFunctionCall || isRelationalFunctionCall) && (name.includes(".") || !!context.schema.relations.find((relation) => relation.related_collection === options.parentCollection && relation.meta?.one_field === name))) {
71
- const parts = (isFunctionCall ? name : fieldKey).split(".");
72
+ const parts = splitFieldPath(isFunctionCall ? name : fieldKey);
72
73
  let rootField = parts[0];
73
74
  let collectionScope = null;
74
75
  if (rootField.includes(":")) {
@@ -1,6 +1,6 @@
1
1
  import { FnHelper } from "../types.js";
2
+ import { convertToMySQLPath } from "../json/mysql-json-path.js";
2
3
  import { InvalidQueryError } from "@directus/errors";
3
- import { toPath } from "lodash-es";
4
4
 
5
5
  //#region src/database/helpers/fn/dialects/mysql.ts
6
6
  var FnHelperMySQL = class extends FnHelper {
@@ -40,6 +40,11 @@ var FnHelperMySQL = class extends FnHelper {
40
40
  const fieldSchema = this.schema.collections?.[collectionName]?.fields?.[column];
41
41
  if (!fieldSchema || fieldSchema.type !== "json" || !options?.jsonPath) throw new InvalidQueryError({ reason: `${collectionName}.${column} is not a JSON field` });
42
42
  const jsonPath = convertToMySQLPath(options.jsonPath);
43
+ if (options?.jsonReturnType === "numeric") return this.knex.raw(`JSON_EXTRACT(??.??, ?)`, [
44
+ table,
45
+ column,
46
+ jsonPath
47
+ ]);
43
48
  return this.knex.raw(`JSON_UNQUOTE(JSON_EXTRACT(??.??, ?))`, [
44
49
  table,
45
50
  column,
@@ -47,16 +52,6 @@ var FnHelperMySQL = class extends FnHelper {
47
52
  ]);
48
53
  }
49
54
  };
50
- function convertToMySQLPath(path) {
51
- const parts = toPath(path.startsWith(".") ? path.slice(1) : path);
52
- let result = "$";
53
- for (const part of parts) {
54
- const num = Number(part);
55
- if (Number.isInteger(num) && num >= 0 && String(num) === part) result += `[${part}]`;
56
- else result += `.${part}`;
57
- }
58
- return result;
59
- }
60
55
 
61
56
  //#endregion
62
- export { FnHelperMySQL, convertToMySQLPath };
57
+ export { FnHelperMySQL };
@@ -43,13 +43,12 @@ var FnHelperOracle = class extends FnHelper {
43
43
  const fieldSchema = this.schema.collections?.[collectionName]?.fields?.[column];
44
44
  if (!fieldSchema || fieldSchema.type !== "json" || !options?.jsonPath) throw new InvalidQueryError({ reason: `${collectionName}.${column} is not a JSON field` });
45
45
  const jsonPath = "$" + options.jsonPath;
46
- return this.knex.raw(`COALESCE(JSON_QUERY(??.??, ?), JSON_VALUE(??.??, ?))`, [
46
+ if (options?.jsonReturnType === "numeric") return this.knex.raw(`JSON_VALUE(??.??, '${jsonPath}' RETURNING NUMBER)`, [table, column]);
47
+ return this.knex.raw(`COALESCE(JSON_QUERY(??.??, '${jsonPath}'), JSON_VALUE(??.??, '${jsonPath}'))`, [
47
48
  table,
48
49
  column,
49
- jsonPath,
50
50
  table,
51
- column,
52
- jsonPath
51
+ column
53
52
  ]);
54
53
  }
55
54
  };
@@ -1,6 +1,6 @@
1
+ import { buildPostgresJsonPath } from "../json/postgres-json-path.js";
1
2
  import { FnHelper } from "../types.js";
2
3
  import { InvalidQueryError } from "@directus/errors";
3
- import { toPath } from "lodash-es";
4
4
 
5
5
  //#region src/database/helpers/fn/dialects/postgres.ts
6
6
  const parseLocaltime = (columnType) => {
@@ -49,34 +49,12 @@ var FnHelperPostgres = class extends FnHelper {
49
49
  const collectionName = options?.originalCollectionName || table;
50
50
  const fieldSchema = this.schema.collections?.[collectionName]?.fields?.[column];
51
51
  if (!fieldSchema || fieldSchema.type !== "json" || !options?.jsonPath) throw new InvalidQueryError({ reason: `${collectionName}.${column} is not a JSON field` });
52
- const { template, bindings } = buildPostgresJsonPath(options.jsonPath);
52
+ const { template, bindings } = buildPostgresJsonPath(options.jsonPath, { asText: options.jsonReturnType !== void 0 });
53
53
  const cast = fieldSchema.dbType === "jsonb" ? "jsonb" : "json";
54
+ if (options.jsonReturnType === "numeric") return this.knex.raw(`(??::${cast}${template})::numeric`, [table + "." + column, ...bindings]);
54
55
  return this.knex.raw(`??::${cast}${template}`, [table + "." + column, ...bindings]);
55
56
  }
56
57
  };
57
- /**
58
- * Build a parameterized PostgreSQL JSON path using -> operators.
59
- * Returns a template string containing only operators and ? placeholders,
60
- * plus a bindings array with the actual values.
61
- *
62
- * @example ".color" → { template: "->?", bindings: ["color"] }
63
- * @example ".items[0].name" → { template: "->?->?->?", bindings: ["items", 0, "name"] }
64
- */
65
- function buildPostgresJsonPath(path) {
66
- const parts = toPath(path.startsWith(".") ? path.slice(1) : path);
67
- let template = "";
68
- const bindings = [];
69
- for (let i = 0; i < parts.length; i++) {
70
- const num = Number(parts[i]);
71
- template += "->?";
72
- if (!isNaN(num) && num >= 0 && Number.isInteger(num)) bindings.push(num);
73
- else bindings.push(parts[i]);
74
- }
75
- return {
76
- template,
77
- bindings
78
- };
79
- }
80
58
 
81
59
  //#endregion
82
- export { FnHelperPostgres, buildPostgresJsonPath };
60
+ export { FnHelperPostgres };
@@ -0,0 +1,22 @@
1
+ import { toPath } from "lodash-es";
2
+
3
+ //#region src/database/helpers/fn/json/mysql-json-path.ts
4
+ /**
5
+ * Build a MySQL/MariaDB JSON path string using dot notation.
6
+ *
7
+ * @example ".color" → "$.color"
8
+ * @example ".items[0].name" → "$.items[0].name"
9
+ */
10
+ function convertToMySQLPath(path) {
11
+ const parts = toPath(path.startsWith(".") ? path.slice(1) : path);
12
+ let result = "$";
13
+ for (const part of parts) {
14
+ const num = Number(part);
15
+ if (Number.isInteger(num) && num >= 0 && String(num) === part) result += `[${part}]`;
16
+ else result += `.${part}`;
17
+ }
18
+ return result;
19
+ }
20
+
21
+ //#endregion
22
+ export { convertToMySQLPath };
@@ -17,6 +17,18 @@ function calculateJsonPathDepth(path) {
17
17
  return depth;
18
18
  }
19
19
  /**
20
+ * Validates and normalizes a bare JSON path string (e.g. "color" or "settings.theme").
21
+ * Adds a leading dot if absent. Throws on unsupported expressions or depth overflow.
22
+ */
23
+ function parseJsonPath(path) {
24
+ const normalized = path.startsWith("[") ? path : `.${path}`;
25
+ if (/\[\]|\.\.|[*?@$]|\[-/.test(normalized)) throw new InvalidQueryError({ reason: "Invalid JSON path: unsupported path expression" });
26
+ if (!/^[\p{L}\p{N}\p{Extended_Pictographic}\w.[\]]+$/u.test(normalized)) throw new InvalidQueryError({ reason: "Invalid JSON path: unsupported path expression" });
27
+ const depth = calculateJsonPathDepth(normalized);
28
+ if (depth > MAX_JSON_QUERY_DEPTH) throw new InvalidQueryError({ reason: `JSON path depth (${depth}) exceeds allowed maximum of ${MAX_JSON_QUERY_DEPTH}` });
29
+ return normalized;
30
+ }
31
+ /**
20
32
  * Parses a json function selection into its field and path components.
21
33
  * Expects relational prefixes to have already been extracted by parseFilterFunctionPath,
22
34
  * so the field should always be a simple column name.
@@ -33,15 +45,11 @@ function parseJsonFunction(functionString) {
33
45
  const field = content.substring(0, commaIndex).trim();
34
46
  const pathContent = content.substring(commaIndex + 1).trim();
35
47
  if (!pathContent) throw new InvalidQueryError({ reason: "Invalid json() syntax: missing path" });
36
- if (pathContent.includes("[]") || /[*?@$]/.test(pathContent)) throw new InvalidQueryError({ reason: "Invalid json() syntax: unsupported path expression" });
37
- const path = pathContent.startsWith("[") ? pathContent : "." + pathContent;
38
- const depth = calculateJsonPathDepth(path);
39
- if (depth > MAX_JSON_QUERY_DEPTH) throw new InvalidQueryError({ reason: `JSON path depth (${depth}) exceeds allowed maximum of ${MAX_JSON_QUERY_DEPTH}` });
40
48
  return {
41
49
  field,
42
- path
50
+ path: parseJsonPath(pathContent)
43
51
  };
44
52
  }
45
53
 
46
54
  //#endregion
47
- export { calculateJsonPathDepth, parseJsonFunction };
55
+ export { calculateJsonPathDepth, parseJsonFunction, parseJsonPath };
@@ -0,0 +1,54 @@
1
+ import { toPath } from "lodash-es";
2
+
3
+ //#region src/database/helpers/fn/json/postgres-json-path.ts
4
+ /**
5
+ * Build a parameterized PostgreSQL JSON path using -> operators.
6
+ * Returns a template string containing only operators and ? placeholders,
7
+ * plus a bindings array with the actual values.
8
+ *
9
+ * When asText is true, the final step uses ->> to return text instead of json,
10
+ * which is required for WHERE clause comparisons (LIKE, =, etc.) and ORDER BY.
11
+ *
12
+ * @example ".color" → { template: "->?", bindings: ["color"] }
13
+ * @example ".color" (asText) → { template: "->>?", bindings: ["color"] }
14
+ * @example ".items[0].name" → { template: "->?->?->?", bindings: ["items", 0, "name"] }
15
+ * @example ".items[0].name" (asText) → { template: "->?->?->>?", bindings: ["items", 0, "name"] }
16
+ */
17
+ function buildPostgresJsonPath(path, options) {
18
+ const parts = toPath(path.startsWith(".") ? path.slice(1) : path);
19
+ let template = "";
20
+ const bindings = [];
21
+ for (let i = 0; i < parts.length - 1; i++) {
22
+ const part$1 = parts[i];
23
+ if (isArrayIndex(part$1)) template += `->${part$1}`;
24
+ else {
25
+ template += "->?";
26
+ bindings.push(part$1);
27
+ }
28
+ }
29
+ const part = parts[parts.length - 1];
30
+ const operator = options?.asText ? "->>" : "->";
31
+ if (isArrayIndex(part)) template += operator + part;
32
+ else {
33
+ template += operator + "?";
34
+ bindings.push(part);
35
+ }
36
+ return {
37
+ template,
38
+ bindings
39
+ };
40
+ }
41
+ /**
42
+ * Checks if a provided value is a valid positive number for array access
43
+ */
44
+ function isArrayIndex(value) {
45
+ if (typeof value === "number") return value >= 0 && Number.isInteger(value);
46
+ if (typeof value === "string") {
47
+ const num = Number(value);
48
+ return String(num) === value && num >= 0 && Number.isInteger(num);
49
+ }
50
+ return false;
51
+ }
52
+
53
+ //#endregion
54
+ export { buildPostgresJsonPath };
@@ -1,10 +1,10 @@
1
1
  //#region src/database/migrations/20260110A-add-ai-provider-settings.ts
2
2
  const DEFAULT_OPENAI_MODELS = [
3
- "gpt-5-nano",
4
- "gpt-5-mini",
5
- "gpt-5"
3
+ "gpt-5.4-nano",
4
+ "gpt-5.4-mini",
5
+ "gpt-5.4"
6
6
  ];
7
- const DEFAULT_ANTHROPIC_MODELS = ["claude-haiku-4-5", "claude-sonnet-4-5"];
7
+ const DEFAULT_ANTHROPIC_MODELS = ["claude-haiku-4-5", "claude-sonnet-4-6"];
8
8
  const DEFAULT_GOOGLE_MODELS = [
9
9
  "gemini-3-pro-preview",
10
10
  "gemini-3-flash-preview",
@@ -0,0 +1,14 @@
1
+ //#region src/database/migrations/20260217A-null-item-versions.ts
2
+ async function up(knex) {
3
+ await knex.schema.alterTable("directus_versions", (table) => {
4
+ table.string("item").nullable().alter();
5
+ });
6
+ }
7
+ async function down(knex) {
8
+ await knex.schema.alterTable("directus_versions", (table) => {
9
+ table.string("item").notNullable().alter();
10
+ });
11
+ }
12
+
13
+ //#endregion
14
+ export { down, up };
@@ -0,0 +1,18 @@
1
+ //#region src/database/migrations/20260312A-add-ai-translation-settings.ts
2
+ async function up(knex) {
3
+ await knex.schema.alterTable("directus_settings", (table) => {
4
+ table.text("ai_translation_default_model").nullable();
5
+ table.json("ai_translation_glossary").nullable();
6
+ table.text("ai_translation_style_guide").nullable();
7
+ });
8
+ }
9
+ async function down(knex) {
10
+ await knex.schema.alterTable("directus_settings", (table) => {
11
+ table.dropColumn("ai_translation_default_model");
12
+ table.dropColumn("ai_translation_glossary");
13
+ table.dropColumn("ai_translation_style_guide");
14
+ });
15
+ }
16
+
17
+ //#endregion
18
+ export { down, up };
@@ -0,0 +1,22 @@
1
+ //#region src/database/migrations/20260507A-add-licensing.ts
2
+ async function up(knex) {
3
+ await knex.schema.alterTable("directus_settings", (table) => {
4
+ table.string("license_key").nullable().defaultTo(null);
5
+ table.text("license_token").nullable().defaultTo(null);
6
+ });
7
+ await knex.schema.alterTable("directus_collections", (table) => {
8
+ table.string("status").notNullable().defaultTo("active");
9
+ });
10
+ }
11
+ async function down(knex) {
12
+ await knex.schema.alterTable("directus_settings", (table) => {
13
+ table.dropColumn("license_key");
14
+ table.dropColumn("license_token");
15
+ });
16
+ await knex.schema.alterTable("directus_collections", (table) => {
17
+ table.dropColumn("status");
18
+ });
19
+ }
20
+
21
+ //#endregion
22
+ export { down, up };
@@ -0,0 +1,14 @@
1
+ //#region src/database/migrations/20260512A-add-autosave-revision-interval.ts
2
+ async function up(knex) {
3
+ await knex.schema.alterTable("directus_collections", (table) => {
4
+ table.float("autosave_revision_interval").nullable();
5
+ });
6
+ }
7
+ async function down(knex) {
8
+ await knex.schema.alterTable("directus_collections", (table) => {
9
+ table.dropColumn("autosave_revision_interval");
10
+ });
11
+ }
12
+
13
+ //#endregion
14
+ export { down, up };
@@ -0,0 +1,87 @@
1
+ //#region src/database/migrations/20260512B-add-mcp-oauth.ts
2
+ async function up(knex) {
3
+ await knex.schema.alterTable("directus_settings", (table) => {
4
+ table.boolean("mcp_oauth_enabled").defaultTo(false).notNullable();
5
+ table.boolean("mcp_oauth_dcr_enabled").defaultTo(false).notNullable();
6
+ table.boolean("mcp_oauth_cimd_enabled").defaultTo(false).notNullable();
7
+ });
8
+ await knex.schema.createTable("directus_oauth_clients", (table) => {
9
+ table.string("client_id", 255).primary().notNullable();
10
+ table.string("client_name", 200).notNullable();
11
+ table.json("redirect_uris").notNullable();
12
+ table.json("grant_types").notNullable();
13
+ table.string("token_endpoint_auth_method").notNullable().defaultTo("none");
14
+ table.string("client_secret_hash", 64).nullable();
15
+ table.string("registration_type", 10).notNullable().defaultTo("dcr");
16
+ table.text("client_uri").nullable();
17
+ table.text("logo_uri").nullable();
18
+ table.text("tos_uri").nullable();
19
+ table.text("policy_uri").nullable();
20
+ table.timestamp("metadata_fetched_at").nullable();
21
+ table.timestamp("metadata_expires_at").nullable();
22
+ table.string("metadata_etag", 255).nullable();
23
+ table.timestamp("date_created").notNullable().defaultTo(knex.fn.now()).index();
24
+ });
25
+ await knex.schema.createTable("directus_oauth_consents", (table) => {
26
+ table.uuid("id").primary().notNullable();
27
+ table.uuid("user").notNullable().references("id").inTable("directus_users").onDelete("CASCADE");
28
+ table.string("client", 255).notNullable().references("client_id").inTable("directus_oauth_clients").onDelete("CASCADE");
29
+ table.string("redirect_uri", 255).notNullable();
30
+ table.string("scope").nullable();
31
+ table.timestamp("date_created").notNullable();
32
+ table.timestamp("date_updated").notNullable();
33
+ table.unique([
34
+ "user",
35
+ "client",
36
+ "redirect_uri"
37
+ ]);
38
+ table.index("client");
39
+ });
40
+ await knex.schema.createTable("directus_oauth_codes", (table) => {
41
+ table.uuid("id").primary().notNullable();
42
+ table.string("code_hash", 64).notNullable().unique();
43
+ table.string("client", 255).notNullable().references("client_id").inTable("directus_oauth_clients").onDelete("CASCADE");
44
+ table.uuid("user").notNullable().references("id").inTable("directus_users").onDelete("CASCADE");
45
+ table.string("redirect_uri", 255).notNullable();
46
+ table.string("resource").notNullable();
47
+ table.string("code_challenge", 128).notNullable();
48
+ table.string("code_challenge_method", 10).notNullable();
49
+ table.string("scope").nullable();
50
+ table.timestamp("expires_at").notNullable().index();
51
+ table.timestamp("used_at").nullable().index();
52
+ });
53
+ await knex.schema.createTable("directus_oauth_tokens", (table) => {
54
+ table.uuid("id").primary().notNullable();
55
+ table.string("client", 255).notNullable().references("client_id").inTable("directus_oauth_clients").onDelete("CASCADE");
56
+ table.uuid("user").notNullable().references("id").inTable("directus_users").onDelete("CASCADE");
57
+ table.string("session", 64).notNullable().index();
58
+ table.string("previous_session", 64).nullable().index();
59
+ table.string("resource").notNullable();
60
+ table.string("code_hash", 64).notNullable().index();
61
+ table.string("scope").nullable();
62
+ table.timestamp("expires_at").notNullable().index();
63
+ table.timestamp("date_created").notNullable();
64
+ table.unique(["client", "user"]);
65
+ });
66
+ await knex.schema.alterTable("directus_sessions", (table) => {
67
+ table.string("oauth_client", 255).nullable().references("client_id").inTable("directus_oauth_clients").onDelete("CASCADE").index();
68
+ });
69
+ }
70
+ async function down(knex) {
71
+ await knex.schema.alterTable("directus_sessions", (table) => {
72
+ table.dropForeign("oauth_client");
73
+ table.dropColumn("oauth_client");
74
+ });
75
+ await knex.schema.dropTable("directus_oauth_tokens");
76
+ await knex.schema.dropTable("directus_oauth_codes");
77
+ await knex.schema.dropTable("directus_oauth_consents");
78
+ await knex.schema.dropTable("directus_oauth_clients");
79
+ await knex.schema.alterTable("directus_settings", (table) => {
80
+ table.dropColumn("mcp_oauth_enabled");
81
+ table.dropColumn("mcp_oauth_dcr_enabled");
82
+ table.dropColumn("mcp_oauth_cimd_enabled");
83
+ });
84
+ }
85
+
86
+ //#endregion
87
+ export { down, up };