@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.
- package/dist/ai/chat/models/chat-request.js +48 -48
- package/dist/ai/chat/models/object-request.js +6 -6
- package/dist/ai/chat/models/providers.js +14 -14
- package/dist/ai/chat/utils/parse-json-schema-7.js +22 -22
- package/dist/ai/mcp/server.js +44 -6
- package/dist/ai/mcp/utils.js +31 -0
- package/dist/ai/tools/assets/index.js +3 -3
- package/dist/ai/tools/collections/index.js +18 -18
- package/dist/ai/tools/fields/index.js +18 -18
- package/dist/ai/tools/files/index.js +18 -18
- package/dist/ai/tools/flows/index.js +16 -16
- package/dist/ai/tools/folders/index.js +18 -18
- package/dist/ai/tools/items/index.js +17 -17
- package/dist/ai/tools/operations/index.js +16 -16
- package/dist/ai/tools/relations/index.js +22 -22
- package/dist/ai/tools/schema/index.js +3 -3
- package/dist/ai/tools/schema.js +159 -159
- package/dist/ai/tools/system/index.js +3 -3
- package/dist/ai/tools/trigger-flow/index.js +3 -3
- package/dist/app.js +33 -9
- package/dist/auth/drivers/ldap.js +3 -1
- package/dist/auth/drivers/local.js +2 -0
- package/dist/auth/drivers/oauth2.js +3 -1
- package/dist/auth/drivers/openid.js +3 -1
- package/dist/auth/drivers/saml.js +2 -0
- package/dist/auth/utils/check-local-disabled.js +16 -0
- package/dist/auth/utils/check-sso-enabled.js +14 -0
- package/dist/auth.js +8 -5
- package/dist/cli/commands/bootstrap/index.js +3 -0
- package/dist/cli/commands/cache/clear.js +6 -1
- package/dist/cli/commands/roles/create.js +4 -1
- package/dist/cli/commands/users/create.js +3 -0
- package/dist/constants.js +8 -1
- package/dist/controllers/access.js +1 -1
- package/dist/controllers/activity.js +2 -1
- package/dist/controllers/assets.js +45 -1
- package/dist/controllers/auth.js +13 -5
- package/dist/controllers/collections.js +1 -1
- package/dist/controllers/comments.js +1 -1
- package/dist/controllers/dashboards.js +1 -1
- package/dist/controllers/fields.js +1 -1
- package/dist/controllers/files.js +3 -1
- package/dist/controllers/flows.js +6 -5
- package/dist/controllers/folders.js +1 -1
- package/dist/controllers/graphql.js +2 -0
- package/dist/controllers/items.js +3 -1
- package/dist/controllers/license.js +119 -0
- package/dist/controllers/mcp/index.js +38 -0
- package/dist/controllers/mcp/oauth-clients.js +68 -0
- package/dist/controllers/mcp/oauth-consent-page.js +316 -0
- package/dist/controllers/mcp/oauth.js +381 -0
- package/dist/controllers/mcp/templates/oauth-consent.liquid +62 -0
- package/dist/controllers/mcp/templates/oauth-error.liquid +28 -0
- package/dist/controllers/notifications.js +1 -1
- package/dist/controllers/operations.js +1 -1
- package/dist/controllers/panels.js +1 -1
- package/dist/controllers/permissions.js +1 -1
- package/dist/controllers/policies.js +1 -1
- package/dist/controllers/presets.js +1 -1
- package/dist/controllers/revisions.js +3 -2
- package/dist/controllers/roles.js +1 -1
- package/dist/controllers/schema.js +2 -2
- package/dist/controllers/server.js +38 -9
- package/dist/controllers/shares.js +1 -1
- package/dist/controllers/translations.js +1 -1
- package/dist/controllers/users.js +1 -1
- package/dist/controllers/utils.js +2 -2
- package/dist/controllers/versions.js +12 -5
- package/dist/database/get-ast-from-query/lib/convert-wildcards.js +10 -1
- package/dist/database/get-ast-from-query/lib/parse-fields.js +2 -1
- package/dist/database/helpers/fn/dialects/mysql.js +7 -12
- package/dist/database/helpers/fn/dialects/oracle.js +3 -4
- package/dist/database/helpers/fn/dialects/postgres.js +4 -26
- package/dist/database/helpers/fn/json/mysql-json-path.js +22 -0
- package/dist/database/helpers/fn/json/parse-function.js +14 -6
- package/dist/database/helpers/fn/json/postgres-json-path.js +54 -0
- package/dist/database/migrations/20260110A-add-ai-provider-settings.js +4 -4
- package/dist/database/migrations/20260217A-null-item-versions.js +14 -0
- package/dist/database/migrations/20260312A-add-ai-translation-settings.js +18 -0
- package/dist/database/migrations/20260507A-add-licensing.js +22 -0
- package/dist/database/migrations/20260512A-add-autosave-revision-interval.js +14 -0
- package/dist/database/migrations/20260512B-add-mcp-oauth.js +87 -0
- package/dist/database/run-ast/lib/apply-query/filter/operator.js +116 -33
- package/dist/database/run-ast/lib/apply-query/index.js +4 -1
- package/dist/database/run-ast/lib/apply-query/sort.js +17 -7
- package/dist/database/run-ast/lib/get-db-query.js +21 -9
- package/dist/database/run-ast/lib/parse-current-level.js +2 -1
- package/dist/database/run-ast/run-ast.js +2 -1
- package/dist/database/run-ast/utils/get-column.js +2 -1
- package/dist/extensions/lib/installation/manager.js +3 -3
- package/dist/extensions/lib/sandbox/register/operation.js +1 -1
- package/dist/extensions/lib/sync/sync.js +2 -2
- package/dist/extensions/manager.js +5 -5
- package/dist/flows.js +12 -10
- package/dist/license/entitlements/lib/collections.js +37 -0
- package/dist/license/entitlements/lib/custom-llms-enabled.js +18 -0
- package/dist/license/entitlements/lib/custom-permission-rules-enabled.js +41 -0
- package/dist/license/entitlements/lib/flows.js +29 -0
- package/dist/license/entitlements/lib/seats.js +103 -0
- package/dist/license/entitlements/lib/sso-enabled.js +45 -0
- package/dist/license/entitlements/manager.js +256 -0
- package/dist/license/index.js +4 -0
- package/dist/license/manager.js +505 -0
- package/dist/license/utils/compute-license-status.js +27 -0
- package/dist/license/utils/get-core-grace-expires-at.js +38 -0
- package/dist/license/utils/get-license-key.js +23 -0
- package/dist/license/utils/get-license-token.js +23 -0
- package/dist/license/utils/handle-license-error.js +41 -0
- package/dist/license/utils/is-in-core-grace-period.js +11 -0
- package/dist/license/utils/is-sso-bypass-allowed.js +21 -0
- package/dist/license/utils/use-rpc.js +33 -0
- package/dist/middleware/cache.js +4 -1
- package/dist/middleware/error-handler.js +11 -0
- package/dist/middleware/extract-token.js +11 -2
- package/dist/middleware/is-admin.js +16 -0
- package/dist/middleware/is-locked.js +16 -0
- package/dist/middleware/mcp-oauth-guard.js +23 -0
- package/dist/middleware/request-counter.js +5 -2
- package/dist/packages/types/dist/index.js +117 -122
- package/dist/permissions/modules/process-ast/utils/extract-paths-from-query.js +10 -1
- package/dist/permissions/utils/get-unaliased-field-key.js +2 -1
- package/dist/request/is-denied-ip.js +2 -0
- package/dist/schedules/license.js +31 -0
- package/dist/schedules/oauth-cleanup.js +26 -0
- package/dist/schedules/retention.js +1 -1
- package/dist/schedules/telemetry.js +4 -1
- package/dist/schedules/tus.js +1 -1
- package/dist/schedules/utils/duration-to-cron.js +36 -0
- package/dist/services/activity.js +15 -0
- package/dist/services/authentication.js +12 -5
- package/dist/services/collections.js +41 -10
- package/dist/services/fields.js +6 -6
- package/dist/services/flows.js +12 -0
- package/dist/services/graphql/resolvers/system-admin.js +2 -2
- package/dist/services/graphql/resolvers/system-global.js +1 -1
- package/dist/services/graphql/resolvers/system.js +34 -18
- package/dist/services/graphql/schema/get-types.js +23 -2
- package/dist/services/graphql/schema/parse-query.js +8 -0
- package/dist/services/graphql/schema/read.js +12 -0
- package/dist/services/graphql/types/json-filter.js +30 -0
- package/dist/services/index.js +6 -6
- package/dist/services/items.js +32 -14
- package/dist/services/mcp-oauth/cimd.js +307 -0
- package/dist/services/mcp-oauth/index.js +1185 -0
- package/dist/services/mcp-oauth/types/error.js +22 -0
- package/dist/services/mcp-oauth/utils/cimd-egress.js +182 -0
- package/dist/services/mcp-oauth/utils/domain.js +21 -0
- package/dist/services/mcp-oauth/utils/loopback.js +11 -0
- package/dist/services/mcp-oauth/utils/redirect.js +84 -0
- package/dist/services/mcp-oauth/utils/registration-debug.js +131 -0
- package/dist/services/payload.js +2 -1
- package/dist/services/permissions.js +31 -9
- package/dist/services/revisions.js +15 -0
- package/dist/services/schema.js +2 -2
- package/dist/services/server.js +21 -4
- package/dist/services/settings.js +37 -3
- package/dist/services/users.js +13 -6
- package/dist/services/utils.js +6 -1
- package/dist/services/versions.js +138 -69
- package/dist/utils/calculate-field-depth.js +1 -0
- package/dist/utils/deep-freeze.js +24 -0
- package/dist/utils/extract-function-name.js +13 -0
- package/dist/utils/generate-translations.js +5 -5
- package/dist/utils/get-accountability-for-token.js +13 -1
- package/dist/utils/get-cache-key.js +1 -1
- package/dist/utils/get-history-filter-query.js +22 -0
- package/dist/utils/get-schema.js +2 -2
- package/dist/utils/get-service.js +3 -3
- package/dist/utils/is-admin.js +9 -0
- package/dist/utils/parse-oauth-scope.js +12 -0
- package/dist/utils/sanitize-query.js +1 -1
- package/dist/utils/split-field-path.js +29 -0
- package/dist/utils/transaction.js +2 -2
- package/dist/utils/translations-validation.js +2 -2
- package/dist/utils/validate-diff.js +7 -3
- package/dist/utils/validate-query.js +35 -4
- package/dist/utils/validate-user-count-integrity.js +28 -5
- package/dist/utils/verify-session-jwt.js +5 -2
- package/dist/utils/versioning/handle-version.js +130 -48
- package/dist/utils/versioning/remove-circular.js +17 -0
- package/dist/websocket/authenticate.js +2 -1
- package/dist/websocket/collab/collab.js +1 -1
- package/dist/websocket/collab/room.js +1 -1
- package/dist/websocket/controllers/base.js +12 -0
- package/dist/websocket/controllers/graphql.js +1 -1
- package/dist/websocket/handlers/subscribe.js +1 -1
- package/dist/websocket/messages.js +64 -64
- package/dist/websocket/utils/items.js +2 -2
- package/license +90 -80
- package/package.json +33 -33
- package/dist/controllers/mcp.js +0 -31
- package/dist/utils/job-queue.js +0 -24
|
@@ -1,6 +1,6 @@
|
|
|
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 { TranslationsService } from "../services/translations.js";
|
|
6
6
|
import use_collection_default from "../middleware/use-collection.js";
|
|
@@ -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 {
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
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"],
|
|
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
|
|
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)
|
|
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
|
|
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(`
|
|
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
|
|
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-
|
|
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 };
|