@rebasepro/server 0.23.0 → 0.24.1-canary.g56af4a4
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/README.md +1 -1
- package/bin/rebase-server.js +4 -2
- package/dist/GCSStorageController-DivE9bAx.js +402 -0
- package/dist/GCSStorageController-DivE9bAx.js.map +1 -0
- package/dist/S3StorageController-D_vKs4kP.js +480 -0
- package/dist/S3StorageController-D_vKs4kP.js.map +1 -0
- package/dist/api/ast-schema-editor.d.ts +92 -1
- package/dist/api/errors.d.ts +9 -0
- package/dist/api/live-schema-routes.d.ts +38 -8
- package/dist/api/logs-routes.d.ts +39 -1
- package/dist/api/openapi-generator.d.ts +17 -0
- package/dist/api/rest/api-generator.d.ts +97 -11
- package/dist/api/rest/auth-collection-writes.d.ts +28 -21
- package/dist/api/rest/field-access-query.d.ts +28 -8
- package/dist/api/rest/field-ops.d.ts +19 -7
- package/dist/api/rest/query-body.d.ts +10 -0
- package/dist/api/rest/write-validation.d.ts +9 -7
- package/dist/api/schema-editor-routes.d.ts +10 -1
- package/dist/api/sql-export-routes.d.ts +8 -0
- package/dist/api/types.d.ts +28 -1
- package/dist/{ast-schema-editor-Mvr50v_S.js → ast-schema-editor-CSdL0iCk.js} +310 -12
- package/dist/ast-schema-editor-CSdL0iCk.js.map +1 -0
- package/dist/auth/access.d.ts +121 -0
- package/dist/auth/adapter-middleware.d.ts +2 -1
- package/dist/auth/address-ownership.d.ts +16 -1
- package/dist/auth/admin-roles-route.d.ts +4 -2
- package/dist/auth/admin-roles.d.ts +17 -20
- package/dist/auth/admin-user-ops.d.ts +24 -2
- package/dist/auth/admin-users-route.d.ts +2 -0
- package/dist/auth/api-keys/api-key-middleware.d.ts +56 -55
- package/dist/auth/api-keys/api-key-routes.d.ts +41 -11
- package/dist/auth/api-keys/api-key-store.d.ts +31 -8
- package/dist/auth/api-keys/api-key-types.d.ts +14 -16
- package/dist/auth/api-keys/http-operation.d.ts +19 -0
- package/dist/auth/api-keys/index.d.ts +11 -11
- package/dist/auth/api-keys/key-grant.d.ts +41 -0
- package/dist/auth/api-keys/legacy-permissions.d.ts +33 -0
- package/dist/auth/auth-hooks.d.ts +46 -7
- package/dist/auth/builtin-auth-adapter.d.ts +8 -0
- package/dist/auth/cookie-utils.d.ts +7 -0
- package/dist/auth/deliverable-address.d.ts +6 -0
- package/dist/auth/email-change-routes.d.ts +41 -0
- package/dist/auth/expired-token-sweep.d.ts +71 -0
- package/dist/auth/impersonation.d.ts +110 -0
- package/dist/auth/index.d.ts +4 -2
- package/dist/auth/interfaces.d.ts +120 -59
- package/dist/auth/jwt-crypto.d.ts +7 -0
- package/dist/auth/jwt-keys.d.ts +4 -0
- package/dist/auth/jwt.d.ts +54 -3
- package/dist/auth/magic-link-routes.d.ts +2 -6
- package/dist/auth/mfa-routes.d.ts +2 -9
- package/dist/auth/middleware.d.ts +17 -5
- package/dist/auth/otp-routes.d.ts +2 -6
- package/dist/auth/passwordless-signup.d.ts +27 -0
- package/dist/auth/platform-token.d.ts +122 -0
- package/dist/auth/rate-limiter.d.ts +54 -1
- package/dist/auth/registration-policy.d.ts +17 -0
- package/dist/auth/routes.d.ts +45 -0
- package/dist/auth/scope-routes.d.ts +22 -0
- package/dist/auth/session-routes.d.ts +11 -6
- package/dist/auth/token-revocation.d.ts +50 -1
- package/dist/auth/user-picking.d.ts +35 -0
- package/dist/auth/verify-credential.d.ts +28 -0
- package/dist/{auth-B-GIMpDG.js → auth-OPq_FeMs.js} +3115 -638
- package/dist/auth-OPq_FeMs.js.map +1 -0
- package/dist/auth_adapter-CIItCTc8.js +21 -0
- package/dist/auth_adapter-CIItCTc8.js.map +1 -0
- package/dist/backend-DTAOsLQc.js.map +1 -1
- package/dist/backup/backup-common.d.ts +24 -3
- package/dist/backup/backup-routes.d.ts +46 -5
- package/dist/backup/backup-schedule.d.ts +33 -0
- package/dist/backup/backup-storage.d.ts +16 -0
- package/dist/backup/index.d.ts +5 -1
- package/dist/backup-5d7XhLX3.js +581 -0
- package/dist/backup-5d7XhLX3.js.map +1 -0
- package/dist/boot/bundle.d.ts +19 -0
- package/dist/boot/driver.d.ts +3 -0
- package/dist/boot/env.d.ts +56 -4
- package/dist/boot/http-server.d.ts +22 -0
- package/dist/boot/options.d.ts +16 -1
- package/dist/boot/runtime-cors.d.ts +17 -0
- package/dist/boot/security-headers.d.ts +26 -0
- package/dist/boot/static-routing.d.ts +65 -0
- package/dist/collection_patch-BRu-BvDv.js +472 -0
- package/dist/collection_patch-BRu-BvDv.js.map +1 -0
- package/dist/{contract-routes-CbFjuBwa.js → contract-routes-02lASyJI.js} +5 -8
- package/dist/contract-routes-02lASyJI.js.map +1 -0
- package/dist/cron/cron-routes.d.ts +1 -1
- package/dist/cron/cron-scheduler.d.ts +94 -23
- package/dist/cron/cron-store.d.ts +31 -4
- package/dist/cron/define-cron.d.ts +6 -2
- package/dist/cron/index.d.ts +1 -1
- package/dist/{cron-loader-DfTj2Hbi.js → cron-loader-D7OmleAb.js} +11 -7
- package/dist/cron-loader-D7OmleAb.js.map +1 -0
- package/dist/{cron-routes-eE8nif_b.js → cron-routes-CBfiaqEL.js} +34 -8
- package/dist/cron-routes-CBfiaqEL.js.map +1 -0
- package/dist/{cron-scheduler-B0pLfAix.js → cron-scheduler-C2vbETMo.js} +349 -65
- package/dist/cron-scheduler-C2vbETMo.js.map +1 -0
- package/dist/cron-store-B2Ih0gCN.js +540 -0
- package/dist/cron-store-B2Ih0gCN.js.map +1 -0
- package/dist/{ddl-bootstrap-C6mo0Kmz.js → ddl-bootstrap-BaqMSa4Y.js} +2 -2
- package/dist/{ddl-bootstrap-C6mo0Kmz.js.map → ddl-bootstrap-BaqMSa4Y.js.map} +1 -1
- package/dist/email/index.d.ts +2 -2
- package/dist/email/smtp-email-service.d.ts +23 -0
- package/dist/email/templates.d.ts +22 -0
- package/dist/email/types.d.ts +44 -0
- package/dist/env.d.ts +1 -39
- package/dist/{errors-DWsX4yTd.js → errors-D6_y86c5.js} +102 -8
- package/dist/errors-D6_y86c5.js.map +1 -0
- package/dist/filter-dialect-REl-TjZr.js +1628 -0
- package/dist/filter-dialect-REl-TjZr.js.map +1 -0
- package/dist/{function-loader-xnbDAPfa.js → function-loader-BDKKiBmy.js} +5 -4
- package/dist/function-loader-BDKKiBmy.js.map +1 -0
- package/dist/{function-routes-Chet4-lB.js → function-routes-DWB7BBsi.js} +25 -13
- package/dist/function-routes-DWB7BBsi.js.map +1 -0
- package/dist/functions/context.d.ts +26 -6
- package/dist/functions/define-function.d.ts +7 -4
- package/dist/functions/guards.d.ts +22 -5
- package/dist/functions/index.d.ts +2 -2
- package/dist/functions/index.js +114 -37
- package/dist/functions/index.js.map +1 -1
- package/dist/functions/request-timeout.d.ts +7 -5
- package/dist/history/history-routes.d.ts +9 -0
- package/dist/history/retention-sweep.d.ts +81 -0
- package/dist/{history-recorder-B4MpJfJK.js → history-recorder-DHenr3lm.js} +4 -4
- package/dist/{history-recorder-B4MpJfJK.js.map → history-recorder-DHenr3lm.js.map} +1 -1
- package/dist/{history-store-BhxWOuz9.js → history-store-DPofGVDp.js} +2 -2
- package/dist/{history-store-BhxWOuz9.js.map → history-store-DPofGVDp.js.map} +1 -1
- package/dist/index.d.ts +14 -7
- package/dist/index.es.js +23363 -17552
- package/dist/index.es.js.map +1 -1
- package/dist/init/health.d.ts +17 -2
- package/dist/init/shutdown.d.ts +20 -0
- package/dist/init.d.ts +70 -6
- package/dist/internal-tables-L2t39RdT.js +136 -0
- package/dist/internal-tables-L2t39RdT.js.map +1 -0
- package/dist/jobs/job-queue.d.ts +8 -0
- package/dist/jobs/job-store.d.ts +24 -0
- package/dist/jobs/types.d.ts +25 -4
- package/dist/{jobs-CazMYhyy.js → jobs-BpD4Q0qR.js} +235 -25
- package/dist/jobs-BpD4Q0qR.js.map +1 -0
- package/dist/{jwt-DnQHNFCl.js → jwt-Bqcu7cGt.js} +207 -77
- package/dist/jwt-Bqcu7cGt.js.map +1 -0
- package/dist/{keys-CogCQpxG.js → keys-GAVZqbqx.js} +3 -17
- package/dist/{keys-CogCQpxG.js.map → keys-GAVZqbqx.js.map} +1 -1
- package/dist/load-failure-UvzZgGXT.js +48 -0
- package/dist/load-failure-UvzZgGXT.js.map +1 -0
- package/dist/{logger-DO2PZc4i.js → logger-D-S-hO5e.js} +26 -3
- package/dist/logger-D-S-hO5e.js.map +1 -0
- package/dist/{logs-routes-Bj4TYYUl.js → logs-routes-DAdv37GI.js} +48 -8
- package/dist/logs-routes-DAdv37GI.js.map +1 -0
- package/dist/mcp/consent-page.d.ts +1 -1
- package/dist/mcp/mcp-routes.d.ts +7 -0
- package/dist/mcp/mcp-tools.d.ts +23 -9
- package/dist/mcp/oauth-metadata.d.ts +21 -16
- package/dist/mcp/oauth-routes.d.ts +7 -1
- package/dist/metrics/index.d.ts +18 -0
- package/dist/metrics/pool-wait.d.ts +50 -0
- package/dist/offline-8Ks3yp9n-CadBKm2D.js +3305 -0
- package/dist/offline-8Ks3yp9n-CadBKm2D.js.map +1 -0
- package/dist/offline-store-D9ytNNaa-BCr2vsFB.js +1903 -0
- package/dist/offline-store-D9ytNNaa-BCr2vsFB.js.map +1 -0
- package/dist/{openapi-generator-O_O24MAT.js → openapi-generator-BqoUh4Ij.js} +455 -80
- package/dist/openapi-generator-BqoUh4Ij.js.map +1 -0
- package/dist/policy-PdzX-Opd.js +121 -0
- package/dist/policy-PdzX-Opd.js.map +1 -0
- package/dist/project_manifest-R6y_KHms.js +59 -0
- package/dist/project_manifest-R6y_KHms.js.map +1 -0
- package/dist/{proxy-Czngl3p9.js → proxy-qRlqeUmO.js} +2 -2
- package/dist/{proxy-Czngl3p9.js.map → proxy-qRlqeUmO.js.map} +1 -1
- package/dist/{query-parser-DGRVFNM3.js → query-parser-DK9eMkb9.js} +497 -52
- package/dist/query-parser-DK9eMkb9.js.map +1 -0
- package/dist/{request-timeout-C_4C2BeR.js → request-timeout-Dnl2_OBc.js} +17 -10
- package/dist/request-timeout-Dnl2_OBc.js.map +1 -0
- package/dist/resumable-upload-threshold-CKv-Ry0l.js +24 -0
- package/dist/resumable-upload-threshold-CKv-Ry0l.js.map +1 -0
- package/dist/schema-edit/apply-schema-change.d.ts +73 -3
- package/dist/schema-edit/local-source.d.ts +29 -0
- package/dist/schema-edit/project-root.d.ts +3 -2
- package/dist/schema-edit/remote-source.d.ts +20 -5
- package/dist/schema-edit/source-editor.d.ts +28 -0
- package/dist/schema-editor-routes-BFf3i8jm.js +136 -0
- package/dist/schema-editor-routes-BFf3i8jm.js.map +1 -0
- package/dist/serve-spa.d.ts +58 -0
- package/dist/services/outbound-post.d.ts +50 -0
- package/dist/services/outbound-url-guard.d.ts +34 -5
- package/dist/services/routed-realtime-service.d.ts +16 -0
- package/dist/services/webhook-service.d.ts +66 -14
- package/dist/soft-delete-params-47yBGs3_.js +668 -0
- package/dist/soft-delete-params-47yBGs3_.js.map +1 -0
- package/dist/sql-export-routes-BNNuGGLP.js +240 -0
- package/dist/sql-export-routes-BNNuGGLP.js.map +1 -0
- package/dist/{src-vkcwKXbT.js → src-CatHFUym.js} +439 -20
- package/dist/src-CatHFUym.js.map +1 -0
- package/dist/storage/GCSStorageController.d.ts +48 -2
- package/dist/storage/LocalStorageController.d.ts +69 -2
- package/dist/storage/S3StorageController.d.ts +39 -3
- package/dist/storage/image-transform.d.ts +5 -0
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/maintenance-routes.d.ts +23 -0
- package/dist/storage/maintenance.d.ts +126 -0
- package/dist/storage/property-limits.d.ts +41 -6
- package/dist/storage/rendition-cache.d.ts +9 -11
- package/dist/storage/request-keys.d.ts +15 -0
- package/dist/storage/request-principals.d.ts +30 -0
- package/dist/storage/requested-object.d.ts +74 -0
- package/dist/storage/routes.d.ts +52 -14
- package/dist/storage/source-variables.d.ts +10 -0
- package/dist/storage/streams.d.ts +42 -0
- package/dist/storage/tus-handler.d.ts +76 -7
- package/dist/storage/types.d.ts +101 -7
- package/dist/storage/upload-validation.d.ts +13 -0
- package/dist/storage-resumable-VO-cMpse-C_YX71pO.js +125 -0
- package/dist/storage-resumable-VO-cMpse-C_YX71pO.js.map +1 -0
- package/dist/strings.es-BeZWAodn.js +256 -0
- package/dist/strings.es-BeZWAodn.js.map +1 -0
- package/dist/upload-validation-Dd-fnsbA.js +172 -0
- package/dist/upload-validation-Dd-fnsbA.js.map +1 -0
- package/dist/utils/byte-size.d.ts +10 -0
- package/dist/utils/compression.d.ts +6 -0
- package/dist/utils/load-failure.d.ts +14 -0
- package/dist/utils/logger.d.ts +12 -0
- package/package.json +5 -5
- package/dist/GCSStorageController-CjrA4PMo.js +0 -242
- package/dist/GCSStorageController-CjrA4PMo.js.map +0 -1
- package/dist/S3StorageController-B6pKDNVj.js +0 -264
- package/dist/S3StorageController-B6pKDNVj.js.map +0 -1
- package/dist/admin-roles-vYdp_Pil.js +0 -36
- package/dist/admin-roles-vYdp_Pil.js.map +0 -1
- package/dist/admin_block-DxKLmdiv.js +0 -206
- package/dist/admin_block-DxKLmdiv.js.map +0 -1
- package/dist/ast-schema-editor-Mvr50v_S.js.map +0 -1
- package/dist/auth/api-keys/api-key-permission-guard.d.ts +0 -65
- package/dist/auth-B-GIMpDG.js.map +0 -1
- package/dist/backup-D7YR94N3.js +0 -253
- package/dist/backup-D7YR94N3.js.map +0 -1
- package/dist/contract-routes-CbFjuBwa.js.map +0 -1
- package/dist/cron-loader-DfTj2Hbi.js.map +0 -1
- package/dist/cron-routes-eE8nif_b.js.map +0 -1
- package/dist/cron-scheduler-B0pLfAix.js.map +0 -1
- package/dist/cron-store-TcoGz-xS.js +0 -305
- package/dist/cron-store-TcoGz-xS.js.map +0 -1
- package/dist/dynamic-import-X40pTZUQ.js +0 -23
- package/dist/dynamic-import-X40pTZUQ.js.map +0 -1
- package/dist/errors-DWsX4yTd.js.map +0 -1
- package/dist/function-loader-xnbDAPfa.js.map +0 -1
- package/dist/function-routes-Chet4-lB.js.map +0 -1
- package/dist/jobs-CazMYhyy.js.map +0 -1
- package/dist/jwt-DnQHNFCl.js.map +0 -1
- package/dist/logger-DO2PZc4i.js.map +0 -1
- package/dist/logs-routes-Bj4TYYUl.js.map +0 -1
- package/dist/openapi-generator-O_O24MAT.js.map +0 -1
- package/dist/query-parser-DGRVFNM3.js.map +0 -1
- package/dist/request-timeout-C_4C2BeR.js.map +0 -1
- package/dist/schema-editor-routes-C5-lh_jO.js +0 -87
- package/dist/schema-editor-routes-C5-lh_jO.js.map +0 -1
- package/dist/src-pmvW7BFx.js +0 -6688
- package/dist/src-pmvW7BFx.js.map +0 -1
- package/dist/src-vkcwKXbT.js.map +0 -1
- package/dist/types-BfKcm9do.js +0 -39
- package/dist/types-BfKcm9do.js.map +0 -1
|
@@ -2,58 +2,260 @@ import { createRequire as __rebaseCreateRequire } from "module";
|
|
|
2
2
|
import __rebaseProcess from "process";
|
|
3
3
|
globalThis.process ??= __rebaseProcess;
|
|
4
4
|
__rebaseCreateRequire(import.meta.url);
|
|
5
|
-
import {
|
|
6
|
-
import "./
|
|
7
|
-
import {
|
|
8
|
-
|
|
5
|
+
import { i as ListLimitError, o as resolveClientListLimit } from "./strings.es-BeZWAodn.js";
|
|
6
|
+
import { d as IncludeSpecError, f as deserializeInclude, g as OrderBySpecError, i as deserializeLogicalCondition, m as normalizeInclude, n as UnknownFilterOperatorError, r as deserializeFilter, s as PAGE_WITH_OFFSET_MESSAGE, t as RESERVED_QUERY_KEYS } from "./filter-dialect-REl-TjZr.js";
|
|
7
|
+
import { $ as parseRelationAggregateSort } from "./src-CatHFUym.js";
|
|
8
|
+
import { a as apiHiddenFieldNames, c as fieldKeyForColumn, i as parseWithDeleted, l as findRelation, m as resolveCollectionRelations, s as restrictedFieldNames, t as DELETED_QUERY_PARAM } from "./soft-delete-params-47yBGs3_.js";
|
|
9
|
+
import { t as ApiError } from "./errors-D6_y86c5.js";
|
|
10
|
+
//#region ../common/src/collections/unknown-field.ts
|
|
9
11
|
/**
|
|
10
|
-
* The
|
|
12
|
+
* The fields an unknown-field answer offers as valid, sorted: what a query on
|
|
13
|
+
* this collection can name as a column. A relation contributes its foreign key
|
|
14
|
+
* (`authorId`) when the key is on this table, and nothing otherwise. An
|
|
15
|
+
* `excludeFromApi` field is never offered.
|
|
11
16
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* delete, not to the parser, and a rule spread across the two files that read
|
|
15
|
-
* it is a rule that drifts.
|
|
17
|
+
* Read off the collection rather than the table, so the REST check — which
|
|
18
|
+
* holds no table — lists exactly what the driver lists.
|
|
16
19
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
function queryableFieldNames(collection) {
|
|
21
|
+
const hidden = apiHiddenFieldNames(collection);
|
|
22
|
+
const names = /* @__PURE__ */ new Set();
|
|
23
|
+
let relations;
|
|
24
|
+
for (const [key, property] of Object.entries(collection.properties ?? {})) {
|
|
25
|
+
if (property?.type === "relation") {
|
|
26
|
+
relations ??= resolveCollectionRelations(collection);
|
|
27
|
+
const relation = findRelation(relations, key);
|
|
28
|
+
if (relation?.kind === "belongsTo") names.add(fieldKeyForColumn(collection, relation.localKey));
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
names.add(key);
|
|
32
|
+
}
|
|
33
|
+
return [...names].filter((name) => !hidden.has(name)).sort();
|
|
34
|
+
}
|
|
35
|
+
var validList = (validFields) => validFields.length > 0 ? `. Valid fields: ${validFields.join(", ")}` : "";
|
|
36
|
+
/** A filter key that resolves to nothing on `collectionPath`. */
|
|
37
|
+
function unknownFilterField(field, collectionPath, validFields) {
|
|
38
|
+
return {
|
|
39
|
+
message: `Unknown filter field '${field}' on collection '${collectionPath}'${validList(validFields)}`,
|
|
40
|
+
code: "UNKNOWN_FILTER_FIELD",
|
|
41
|
+
details: {
|
|
42
|
+
field,
|
|
43
|
+
collection: collectionPath,
|
|
44
|
+
...validFields.length > 0 && { validFields }
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** `author.emial` — a relation path whose last segment names nothing on the relation's target. */
|
|
49
|
+
function unknownRelationTargetField(path, target, collectionPath) {
|
|
50
|
+
return {
|
|
51
|
+
message: `Unknown field '${path.fieldKey}' on '${target.slug}', the target of relation '${path.relationKey}' on collection '${collectionPath}'${validList(target.validFields)}`,
|
|
52
|
+
code: "UNKNOWN_FILTER_FIELD",
|
|
53
|
+
details: {
|
|
54
|
+
field: path.field,
|
|
55
|
+
collection: collectionPath,
|
|
56
|
+
relation: path.relationKey,
|
|
57
|
+
targetCollection: target.slug,
|
|
58
|
+
...target.validFields.length > 0 && { validFields: target.validFields }
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** An `orderBy` key that resolves to nothing. `collectionName` is absent when the read carries no collection. */
|
|
63
|
+
function unknownOrderByField(orderBy, collectionName, validFields) {
|
|
64
|
+
return {
|
|
65
|
+
message: `Unknown orderBy field '${orderBy}'${collectionName ? ` on collection '${collectionName}'` : ""}${validList(validFields)}`,
|
|
66
|
+
code: "UNKNOWN_ORDER_BY_FIELD",
|
|
67
|
+
details: {
|
|
68
|
+
field: orderBy,
|
|
69
|
+
...collectionName && { collection: collectionName },
|
|
70
|
+
...validFields.length > 0 && { validFields }
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** A `fields` entry that names nothing. */
|
|
75
|
+
function unknownProjectionField(name, collectionName, validFields) {
|
|
76
|
+
return {
|
|
77
|
+
message: `Unknown field '${name}' in \`fields\` on collection '${collectionName}'. Valid fields: ${validFields.join(", ")}`,
|
|
78
|
+
code: "UNKNOWN_FIELD",
|
|
79
|
+
details: {
|
|
80
|
+
field: name,
|
|
81
|
+
collection: collectionName
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* An `include` naming no relation of the collection. `validRelations` is what
|
|
87
|
+
* the collection declares, never an `excludeFromApi` relation.
|
|
88
|
+
*/
|
|
89
|
+
function unknownIncludeRelation(key, collectionName, validRelations) {
|
|
90
|
+
return {
|
|
91
|
+
message: `Unknown relation '${key}' on collection '${collectionName}'` + (validRelations.length > 0 ? `. Its relations are: ${validRelations.join(", ")}` : ". It declares no relations."),
|
|
92
|
+
code: "UNKNOWN_RELATION",
|
|
93
|
+
details: {
|
|
94
|
+
relation: key,
|
|
95
|
+
collection: collectionName,
|
|
96
|
+
...validRelations.length > 0 && { validRelations }
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** The relations an unknown-relation answer offers, sorted — never an `excludeFromApi` one. */
|
|
101
|
+
function includableRelationNames(collection) {
|
|
102
|
+
const hidden = apiHiddenFieldNames(collection);
|
|
103
|
+
return Object.keys(resolveCollectionRelations(collection)).filter((key) => !hidden.has(key)).sort();
|
|
104
|
+
}
|
|
21
105
|
/**
|
|
22
|
-
*
|
|
106
|
+
* An aggregate's `select` field or `groupBy` key that names no column.
|
|
107
|
+
* `forWhat` is where it was named: `sum()`, `count()`, `groupBy`.
|
|
108
|
+
* `validFields` is {@link queryableFieldNames}, never an `excludeFromApi` field.
|
|
109
|
+
*/
|
|
110
|
+
function unknownAggregateField(field, forWhat, validFields) {
|
|
111
|
+
return {
|
|
112
|
+
message: `Unknown field '${field}' in ${forWhat}${validList(validFields)}`,
|
|
113
|
+
code: "UNKNOWN_AGGREGATE_FIELD",
|
|
114
|
+
details: {
|
|
115
|
+
field,
|
|
116
|
+
...validFields.length > 0 && { validFields }
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The properties a `vector_search` can name, sorted: those declared
|
|
122
|
+
* `{ type: "vector" }`, never an `excludeFromApi` one.
|
|
123
|
+
*/
|
|
124
|
+
function vectorPropertyNames(collection) {
|
|
125
|
+
const hidden = apiHiddenFieldNames(collection);
|
|
126
|
+
return Object.entries(collection.properties ?? {}).filter(([key, property]) => property?.type === "vector" && !hidden.has(key)).map(([key]) => key).sort();
|
|
127
|
+
}
|
|
128
|
+
/** A `vector_search` naming no vector property. `vectorProperties` is {@link vectorPropertyNames}. */
|
|
129
|
+
function unknownVectorProperty(property, vectorProperties) {
|
|
130
|
+
return {
|
|
131
|
+
message: `Unknown vector property "${property}". ` + (vectorProperties.length > 0 ? `This collection's vector properties are: ${vectorProperties.join(", ")}.` : "This collection declares no `vector` property to search."),
|
|
132
|
+
code: "UNKNOWN_VECTOR_PROPERTY",
|
|
133
|
+
details: {
|
|
134
|
+
property,
|
|
135
|
+
...vectorProperties.length > 0 && { vectorProperties }
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region ../common/src/data/cursor.ts
|
|
141
|
+
/** A cursor that cannot be read at all — truncated, re-encoded, or invented. */
|
|
142
|
+
var CursorError = class CursorError extends Error {
|
|
143
|
+
code = "INVALID_CURSOR";
|
|
144
|
+
constructor(detail) {
|
|
145
|
+
super(`Invalid \`after\` cursor: ${detail}. Pass back the \`meta.nextCursor\` from the previous page unchanged — it is opaque and must not be built by hand.`);
|
|
146
|
+
this.name = "CursorError";
|
|
147
|
+
Object.setPrototypeOf(this, CursorError.prototype);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* A cursor that reads fine but describes a different query.
|
|
23
152
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
153
|
+
* Separate from {@link CursorError} because the fix is different: this one is
|
|
154
|
+
* not a corrupt string, it is a correct cursor used against a sort it was not
|
|
155
|
+
* produced under. Seeking anyway would return rows in an order nobody asked
|
|
156
|
+
* for, and — worse — would look like it worked.
|
|
157
|
+
*/
|
|
158
|
+
var CursorMismatchError = class CursorMismatchError extends Error {
|
|
159
|
+
code = "CURSOR_ORDER_MISMATCH";
|
|
160
|
+
constructor(cursorKeys, queryKeys) {
|
|
161
|
+
super(`The \`after\` cursor was produced by a query ordered by ${cursorKeys.map((k) => `"${k}"`).join(", ") || "(nothing)"}, but this query orders by ${queryKeys.map((k) => `"${k}"`).join(", ") || "(nothing)"}. A cursor only continues the listing it came from — keep \`orderBy\` identical across pages, or drop \`after\` to start over.`);
|
|
162
|
+
this.name = "CursorMismatchError";
|
|
163
|
+
Object.setPrototypeOf(this, CursorMismatchError.prototype);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Tag for a value whose JSON round-trip would otherwise lose its type.
|
|
27
168
|
*
|
|
28
|
-
* A
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
169
|
+
* A `timestamp` column comes back from the driver as a `Date`; JSON turns it
|
|
170
|
+
* into a string, and the string would then be compared against the column by
|
|
171
|
+
* whatever cast Postgres chose. Round-tripping it as a `Date` keeps the
|
|
172
|
+
* comparison the one the ORDER BY made.
|
|
32
173
|
*/
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
174
|
+
var DATE_TAG = "$date";
|
|
175
|
+
function decodeValue(value) {
|
|
176
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
177
|
+
const tagged = value[DATE_TAG];
|
|
178
|
+
if (typeof tagged === "string") {
|
|
179
|
+
const date = new Date(tagged);
|
|
180
|
+
return Number.isNaN(date.getTime()) ? tagged : date;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
function fromBase64Url(encoded) {
|
|
186
|
+
const padded = encoded.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - encoded.length % 4) % 4);
|
|
187
|
+
const binary = atob(padded);
|
|
188
|
+
const bytes = new Uint8Array(binary.length);
|
|
189
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
190
|
+
return new TextDecoder().decode(bytes);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Read a cursor produced by {@link encodeCursor}.
|
|
194
|
+
*
|
|
195
|
+
* @throws {CursorError} when the string is not a cursor this codec wrote.
|
|
196
|
+
*/
|
|
197
|
+
function decodeCursor(raw) {
|
|
198
|
+
let parsed;
|
|
199
|
+
try {
|
|
200
|
+
parsed = JSON.parse(fromBase64Url(raw.trim()));
|
|
201
|
+
} catch {
|
|
202
|
+
throw new CursorError("it is not a cursor this API issued");
|
|
203
|
+
}
|
|
204
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new CursorError("it does not decode to a cursor");
|
|
205
|
+
const body = parsed;
|
|
206
|
+
if (!Array.isArray(body.k)) throw new CursorError("it carries no sort keys");
|
|
207
|
+
if (body.i === void 0) throw new CursorError("it carries no row id");
|
|
208
|
+
const orderBy = [];
|
|
209
|
+
for (const entry of body.k) {
|
|
210
|
+
if (!Array.isArray(entry) || typeof entry[0] !== "string") throw new CursorError("one of its sort keys is malformed");
|
|
211
|
+
const direction = entry[1] === "desc" ? "desc" : "asc";
|
|
212
|
+
orderBy.push(entry[2] === "first" || entry[2] === "last" ? [
|
|
213
|
+
entry[0],
|
|
214
|
+
direction,
|
|
215
|
+
entry[2]
|
|
216
|
+
] : [entry[0], direction]);
|
|
217
|
+
}
|
|
218
|
+
const rawValues = body.v && typeof body.v === "object" && !Array.isArray(body.v) ? body.v : {};
|
|
219
|
+
const values = {};
|
|
220
|
+
for (const [field, value] of Object.entries(rawValues)) values[field] = decodeValue(value);
|
|
221
|
+
return {
|
|
222
|
+
orderBy,
|
|
223
|
+
values,
|
|
224
|
+
id: decodeValue(body.i)
|
|
225
|
+
};
|
|
39
226
|
}
|
|
40
227
|
/**
|
|
41
|
-
*
|
|
228
|
+
* The `orderBy` a request should run under, given a cursor and whatever sort
|
|
229
|
+
* the request itself named.
|
|
42
230
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
231
|
+
* A request that names no sort **adopts the cursor's** — that is what makes
|
|
232
|
+
* `find({ after })` work without restating the `orderBy` from the previous
|
|
233
|
+
* call, and it cannot be wrong, since the cursor is the only sort in play.
|
|
234
|
+
* A request that names one must name the *same* one, key for key, direction for
|
|
235
|
+
* direction, nulls for nulls; anything else is {@link CursorMismatchError}.
|
|
46
236
|
*
|
|
47
|
-
*
|
|
48
|
-
* "no" — a typo that silently soft-deletes when the caller asked to purge is a
|
|
49
|
-
* caller who believes the data is gone.
|
|
237
|
+
* @throws {CursorMismatchError}
|
|
50
238
|
*/
|
|
51
|
-
function
|
|
52
|
-
if (
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
239
|
+
function reconcileCursorOrder(cursor, requested) {
|
|
240
|
+
if (!requested || requested.length === 0) return cursor.orderBy;
|
|
241
|
+
const spell = (keys) => keys.map(([field, direction, nulls]) => `${field}:${direction}${nulls ? `:${nulls}` : ""}`);
|
|
242
|
+
const cursorKeys = spell(cursor.orderBy);
|
|
243
|
+
const queryKeys = spell(requested);
|
|
244
|
+
if (cursorKeys.length !== queryKeys.length || cursorKeys.some((key, i) => key !== queryKeys[i])) throw new CursorMismatchError(cursorKeys, queryKeys);
|
|
245
|
+
return requested;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* The `startAfter` shape the driver contract takes, built from a cursor.
|
|
249
|
+
*
|
|
250
|
+
* The driver has always accepted `{ id, values }`; this is the one place that
|
|
251
|
+
* shape is produced, so the REST route and the WebSocket ingress cannot drift
|
|
252
|
+
* into two spellings of the same seek.
|
|
253
|
+
*/
|
|
254
|
+
function cursorToStartAfter(cursor) {
|
|
255
|
+
return {
|
|
256
|
+
id: cursor.id,
|
|
257
|
+
values: cursor.values
|
|
258
|
+
};
|
|
57
259
|
}
|
|
58
260
|
//#endregion
|
|
59
261
|
//#region src/api/rest/field-access-query.ts
|
|
@@ -66,13 +268,22 @@ function parseHardDelete(raw) {
|
|
|
66
268
|
* `?orderBy=salary` returns them in order of it. A column no response can carry
|
|
67
269
|
* has to be a column no query can interrogate, or the read rule is decoration.
|
|
68
270
|
*
|
|
69
|
-
* The refusal names
|
|
70
|
-
* published OpenAPI lists every property of every collection,
|
|
71
|
-
* a given caller cannot read, because the document is one
|
|
72
|
-
* off the app rather than off the authenticated data
|
|
73
|
-
* here would protect nothing and would answer a
|
|
74
|
-
* "unknown field", sending them to look for a
|
|
75
|
-
* there.
|
|
271
|
+
* The refusal names a field restricted by role. That is deliberate and it is
|
|
272
|
+
* not a leak: the published OpenAPI lists every property of every collection,
|
|
273
|
+
* including the ones a given caller cannot read, because the document is one
|
|
274
|
+
* document and is served off the app rather than off the authenticated data
|
|
275
|
+
* router. Hiding the name here would protect nothing and would answer a
|
|
276
|
+
* caller's genuine typo with "unknown field", sending them to look for a
|
|
277
|
+
* spelling mistake that is not there. Those field *names* are public; their
|
|
278
|
+
* *values* are not.
|
|
279
|
+
*
|
|
280
|
+
* An `excludeFromApi` field is the exception, because its name is *not*
|
|
281
|
+
* public: OpenAPI, `list_collections` and the generated SDK all leave it out.
|
|
282
|
+
* "`email` is not readable on `authors`" would confirm the one thing those hide,
|
|
283
|
+
* so a filter, sort, projection, include, aggregate or vector search naming one
|
|
284
|
+
* is answered as one naming a field that does not exist — built by the functions the driver answers a
|
|
285
|
+
* typo with (`@rebasepro/common`'s `unknown-field`), so the two answers are the
|
|
286
|
+
* same bytes. See {@link assertNoHiddenNames}.
|
|
76
287
|
*
|
|
77
288
|
* @module
|
|
78
289
|
*/
|
|
@@ -136,7 +347,8 @@ function assertReadableFields(names, collection, viewer, where) {
|
|
|
136
347
|
if (names.length === 0) return;
|
|
137
348
|
const { refused } = restrictedFieldNames(collection, viewer, "read");
|
|
138
349
|
if (refused.size === 0) return;
|
|
139
|
-
const
|
|
350
|
+
const hidden = apiHiddenFieldNames(collection);
|
|
351
|
+
const named = [...new Set(names.filter((n) => n !== void 0 && refused.has(n) && !hidden.has(n)))];
|
|
140
352
|
if (named.length === 0) return;
|
|
141
353
|
throw ApiError.badRequest(`${named.map((f) => `'${f}'`).join(", ")} ${named.length > 1 ? "are" : "is"} not readable on '${collection.slug}' with your roles, so ${named.length > 1 ? "they" : "it"} cannot be used in ${WHERE_LABEL[where]}.`, "FIELD_NOT_READABLE", {
|
|
142
354
|
collection: collection.slug,
|
|
@@ -156,15 +368,240 @@ function assertReadableFields(names, collection, viewer, where) {
|
|
|
156
368
|
* parsed and then dropped by the list route.
|
|
157
369
|
*/
|
|
158
370
|
function assertQueryFieldsReadable(options, collection, viewer) {
|
|
371
|
+
const filtered = assertRestrictedFieldsUnused(options, collection, viewer);
|
|
372
|
+
const include = options.include !== void 0 ? normalizeInclude(options.include) : void 0;
|
|
373
|
+
assertNoHiddenNames({
|
|
374
|
+
...options,
|
|
375
|
+
filtered,
|
|
376
|
+
include: include?.tree
|
|
377
|
+
}, collection);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* The aggregate route's `select` and `groupBy`, which it parses itself: an
|
|
381
|
+
* `excludeFromApi` name there is answered exactly as the driver answers a
|
|
382
|
+
* column that does not exist. Walked in the driver's order — each aggregate's
|
|
383
|
+
* field, then each group key — and the first hidden *or* unknown name gets the
|
|
384
|
+
* answer, so a typo before a hidden name is still the one named.
|
|
385
|
+
*/
|
|
386
|
+
function assertNoHiddenAggregateNames(aggregates, groupBy, collection) {
|
|
387
|
+
const hidden = apiHiddenFieldNames(collection);
|
|
388
|
+
if (hidden.size === 0) return;
|
|
389
|
+
const asked = [...aggregates.flatMap(({ fn, field }) => field === void 0 ? [] : [{
|
|
390
|
+
name: field,
|
|
391
|
+
forWhat: `${fn}()`
|
|
392
|
+
}]), ...groupBy.map((name) => ({
|
|
393
|
+
name,
|
|
394
|
+
forWhat: "groupBy"
|
|
395
|
+
}))];
|
|
396
|
+
if (!asked.some(({ name }) => hidden.has(name))) return;
|
|
397
|
+
const valid = queryableFieldNames(collection);
|
|
398
|
+
const known = (name) => !hidden.has(name) && (valid.includes(name) || name in (collection.properties ?? {}));
|
|
399
|
+
for (const { name, forWhat } of asked) {
|
|
400
|
+
if (known(name)) continue;
|
|
401
|
+
const answer = unknownAggregateField(name, forWhat, valid);
|
|
402
|
+
throw ApiError.badRequest(answer.message, answer.code, answer.details);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Refuse every field restricted by role that the read names, at every level of
|
|
407
|
+
* its include tree. Returns the filter's column names, for the hidden pass.
|
|
408
|
+
*/
|
|
409
|
+
function assertRestrictedFieldsUnused(options, collection, viewer) {
|
|
159
410
|
const filtered = [];
|
|
160
411
|
if (options.where) filtered.push(...Object.keys(options.where));
|
|
161
412
|
logicalColumns(options.logical, filtered);
|
|
162
|
-
assertReadableFields(filtered, collection, viewer, "filter");
|
|
413
|
+
assertReadableFields(filtered.map(filterHead), collection, viewer, "filter");
|
|
414
|
+
assertRelationPathsReadable(filtered, collection, viewer);
|
|
163
415
|
assertReadableFields((options.orderBy ?? []).map((entry) => orderByColumn(entry.field)), collection, viewer, "orderBy");
|
|
416
|
+
assertAggregateSortsReadable(options.orderBy ?? [], collection, viewer);
|
|
164
417
|
assertReadableFields(options.fields ?? [], collection, viewer, "fields");
|
|
165
418
|
assertReadableFields(options.vectorSearch ? [options.vectorSearch.property] : [], collection, viewer, "vector_search");
|
|
166
419
|
const include = options.include !== void 0 ? normalizeInclude(options.include) : void 0;
|
|
167
420
|
if (include) assertIncludeFieldsReadable(include.tree, collection, viewer);
|
|
421
|
+
return filtered;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* The field a filter key reads on *this* collection: the column before a JSON
|
|
425
|
+
* path's first arrow (`metadata->theme` reads `metadata`), the relation before
|
|
426
|
+
* a relation path's dot (`author.email` reads `author`), the key itself
|
|
427
|
+
* otherwise. A key that is a property outright wins, as the driver resolves it.
|
|
428
|
+
*/
|
|
429
|
+
function filterHead(name) {
|
|
430
|
+
const arrow = name.indexOf("->");
|
|
431
|
+
if (arrow > 0) return name.slice(0, arrow);
|
|
432
|
+
const dot = name.indexOf(".");
|
|
433
|
+
if (dot > 0) return name.slice(0, dot);
|
|
434
|
+
return name;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* `orderBy=max(staff.salary)` ranks the parents by the target's `salary` — the
|
|
438
|
+
* same reading `orderBy=salary` on `staff` is — so the aggregated field is
|
|
439
|
+
* judged by the target's rule. Whether the relation itself may be read is the
|
|
440
|
+
* driver's check (`assertRelationReadable`), which runs on every door.
|
|
441
|
+
*/
|
|
442
|
+
function assertAggregateSortsReadable(orderBy, collection, viewer) {
|
|
443
|
+
let relations;
|
|
444
|
+
for (const { field } of orderBy) {
|
|
445
|
+
const spec = parseRelationAggregateSort(field);
|
|
446
|
+
if (!spec?.field) continue;
|
|
447
|
+
relations ??= resolveCollectionRelations(collection);
|
|
448
|
+
const relation = findRelation(relations, spec.relation);
|
|
449
|
+
if (!relation) continue;
|
|
450
|
+
assertReadableFields([spec.field], relation.target(), viewer, "orderBy");
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* `?author.salary=gt.100000` reads the target's `salary`, a row at a time, as
|
|
455
|
+
* `?salary=gt.100000` on `authors` would — so it is judged by the target's
|
|
456
|
+
* rule. Only a path whose first segment is a relation here; anything else is
|
|
457
|
+
* the driver's to refuse.
|
|
458
|
+
*/
|
|
459
|
+
function assertRelationPathsReadable(filtered, collection, viewer) {
|
|
460
|
+
let relations;
|
|
461
|
+
for (const name of filtered) {
|
|
462
|
+
const dot = name.indexOf(".");
|
|
463
|
+
if (dot <= 0 || name.indexOf("->") >= 0) continue;
|
|
464
|
+
relations ??= resolveCollectionRelations(collection);
|
|
465
|
+
const relation = findRelation(relations, name.slice(0, dot));
|
|
466
|
+
if (!relation) continue;
|
|
467
|
+
assertReadableFields([name.slice(dot + 1)], relation.target(), viewer, "filter");
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Answer a read naming an `excludeFromApi` field — or relation — exactly as the
|
|
472
|
+
* driver answers a read naming a field that does not exist.
|
|
473
|
+
*
|
|
474
|
+
* The names are walked in the order the driver resolves them — the `fields`
|
|
475
|
+
* projection, then the filter, then the sort, then each include, parent before
|
|
476
|
+
* child — and the first that is hidden *or* unknown gets the answer. Answering
|
|
477
|
+
* only the hidden one would still tell the two apart: with `?aaa=x&email=x` the
|
|
478
|
+
* driver names `aaa` when `email` does not exist, so this has to as well.
|
|
479
|
+
*
|
|
480
|
+
* A request naming nothing hidden is left alone entirely, so the driver stays
|
|
481
|
+
* the only one answering ordinary typos.
|
|
482
|
+
*/
|
|
483
|
+
function assertNoHiddenNames(options, collection) {
|
|
484
|
+
const hidden = apiHiddenFieldNames(collection);
|
|
485
|
+
if (hidden.size > 0) {
|
|
486
|
+
const shapes = [
|
|
487
|
+
...(options.fields ?? []).map((name) => ({
|
|
488
|
+
kind: "fields",
|
|
489
|
+
name
|
|
490
|
+
})),
|
|
491
|
+
...options.filtered.map((name) => ({
|
|
492
|
+
kind: "filter",
|
|
493
|
+
name
|
|
494
|
+
})),
|
|
495
|
+
...(options.orderBy ?? []).flatMap((entry) => {
|
|
496
|
+
const name = orderByColumn(entry.field);
|
|
497
|
+
return name === void 0 ? [] : [{
|
|
498
|
+
kind: "orderBy",
|
|
499
|
+
name
|
|
500
|
+
}];
|
|
501
|
+
})
|
|
502
|
+
].map(({ kind, name }) => ({
|
|
503
|
+
kind,
|
|
504
|
+
name,
|
|
505
|
+
shape: nameShape(name, collection, hidden)
|
|
506
|
+
}));
|
|
507
|
+
const vectorHidden = options.vectorSearch !== void 0 && hidden.has(options.vectorSearch.property);
|
|
508
|
+
if (vectorHidden || shapes.some((entry) => entry.shape.hidden)) for (const { kind, name, shape } of shapes) {
|
|
509
|
+
if (shape.known && !shape.hidden) continue;
|
|
510
|
+
const answer = unknownAnswer(kind, name, shape, collection);
|
|
511
|
+
throw ApiError.badRequest(answer.message, answer.code, answer.details);
|
|
512
|
+
}
|
|
513
|
+
if (vectorHidden && options.vectorSearch) {
|
|
514
|
+
const answer = unknownVectorProperty(options.vectorSearch.property, vectorPropertyNames(collection));
|
|
515
|
+
throw ApiError.badRequest(answer.message, answer.code, answer.details);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
const relations = resolveCollectionRelations(collection);
|
|
519
|
+
for (const [key, node] of Object.entries(options.include ?? {})) {
|
|
520
|
+
if (hidden.has(key)) {
|
|
521
|
+
const answer = unknownIncludeRelation(key, collection.slug ?? collection.name, includableRelationNames(collection));
|
|
522
|
+
throw ApiError.badRequest(answer.message, answer.code, answer.details);
|
|
523
|
+
}
|
|
524
|
+
const relation = relations[key];
|
|
525
|
+
if (!relation) continue;
|
|
526
|
+
const filtered = [];
|
|
527
|
+
if (node.where) filtered.push(...Object.keys(node.where));
|
|
528
|
+
logicalColumns(node.logical, filtered);
|
|
529
|
+
assertNoHiddenNames({
|
|
530
|
+
filtered,
|
|
531
|
+
orderBy: node.orderBy?.map(([field]) => ({ field })),
|
|
532
|
+
fields: node.fields,
|
|
533
|
+
include: node.children
|
|
534
|
+
}, relation.target());
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
function nameShape(name, collection, hidden) {
|
|
538
|
+
if (hidden.has(name)) return {
|
|
539
|
+
hidden: true,
|
|
540
|
+
known: false
|
|
541
|
+
};
|
|
542
|
+
const columns = new Set(queryableFieldNames(collection));
|
|
543
|
+
const relations = resolveCollectionRelations(collection);
|
|
544
|
+
if (columns.has(name) || name in (collection.properties ?? {})) return {
|
|
545
|
+
hidden: false,
|
|
546
|
+
known: true
|
|
547
|
+
};
|
|
548
|
+
const arrow = name.indexOf("->");
|
|
549
|
+
if (arrow > 0) {
|
|
550
|
+
const head = name.slice(0, arrow);
|
|
551
|
+
return {
|
|
552
|
+
hidden: hidden.has(head),
|
|
553
|
+
known: columns.has(head)
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
const dot = name.indexOf(".");
|
|
557
|
+
if (dot > 0) {
|
|
558
|
+
const relationKey = name.slice(0, dot);
|
|
559
|
+
const fieldKey = name.slice(dot + 1);
|
|
560
|
+
if (hidden.has(relationKey)) return {
|
|
561
|
+
hidden: true,
|
|
562
|
+
known: false
|
|
563
|
+
};
|
|
564
|
+
const relation = findRelation(relations, relationKey);
|
|
565
|
+
if (!relation) return {
|
|
566
|
+
hidden: false,
|
|
567
|
+
known: false
|
|
568
|
+
};
|
|
569
|
+
const target = relation.target();
|
|
570
|
+
const targetHidden = apiHiddenFieldNames(target);
|
|
571
|
+
const path = {
|
|
572
|
+
relationKey,
|
|
573
|
+
fieldKey,
|
|
574
|
+
target
|
|
575
|
+
};
|
|
576
|
+
if (targetHidden.has(fieldKey)) return {
|
|
577
|
+
hidden: true,
|
|
578
|
+
known: false,
|
|
579
|
+
path
|
|
580
|
+
};
|
|
581
|
+
return {
|
|
582
|
+
hidden: false,
|
|
583
|
+
known: queryableFieldNames(target).includes(fieldKey) || fieldKey in (target.properties ?? {}),
|
|
584
|
+
path
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
return {
|
|
588
|
+
hidden: false,
|
|
589
|
+
known: false
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
function unknownAnswer(kind, name, shape, collection) {
|
|
593
|
+
const slug = collection.slug ?? collection.name;
|
|
594
|
+
if (kind === "fields") return unknownProjectionField(name, slug, queryableFieldNames(collection));
|
|
595
|
+
if (kind === "orderBy") return unknownOrderByField(name, slug, queryableFieldNames(collection));
|
|
596
|
+
if (shape.path) return unknownRelationTargetField({
|
|
597
|
+
field: name,
|
|
598
|
+
relationKey: shape.path.relationKey,
|
|
599
|
+
fieldKey: shape.path.fieldKey
|
|
600
|
+
}, {
|
|
601
|
+
slug: shape.path.target.slug,
|
|
602
|
+
validFields: queryableFieldNames(shape.path.target)
|
|
603
|
+
}, slug);
|
|
604
|
+
return unknownFilterField(name, slug, queryableFieldNames(collection));
|
|
168
605
|
}
|
|
169
606
|
/**
|
|
170
607
|
* Every relation an `include` loads, judged like the read it is part of.
|
|
@@ -186,7 +623,7 @@ function assertIncludeFieldsReadable(tree, collection, viewer) {
|
|
|
186
623
|
const relation = relations[key];
|
|
187
624
|
if (!relation) continue;
|
|
188
625
|
const target = relation.target();
|
|
189
|
-
|
|
626
|
+
assertRestrictedFieldsUnused({
|
|
190
627
|
where: node.where,
|
|
191
628
|
logical: node.logical,
|
|
192
629
|
orderBy: node.orderBy?.map(([field]) => ({ field })),
|
|
@@ -263,6 +700,7 @@ function parseLogicalGroup(type, raw) {
|
|
|
263
700
|
try {
|
|
264
701
|
parsed = deserializeLogicalCondition(`${type}(${inner})`);
|
|
265
702
|
} catch (e) {
|
|
703
|
+
if (e instanceof UnknownFilterOperatorError) throw invalidParam(e.message, e.code, e.details);
|
|
266
704
|
throw invalidParam(`Invalid \`${type}\` parameter: ${e instanceof Error ? e.message : String(e)}`, "INVALID_LOGICAL_GROUP");
|
|
267
705
|
}
|
|
268
706
|
return "type" in parsed ? parsed : void 0;
|
|
@@ -514,6 +952,7 @@ function parseQueryOptions(query, limits = {}, access) {
|
|
|
514
952
|
const offsetVal = getLastValue(query.offset);
|
|
515
953
|
if (offsetVal) options.offset = parseWindowParam(offsetVal, "offset", 0, "INVALID_OFFSET");
|
|
516
954
|
const pageVal = getLastValue(query.page);
|
|
955
|
+
if (pageVal && offsetVal) throw invalidParam(PAGE_WITH_OFFSET_MESSAGE, "PAGE_WITH_OFFSET");
|
|
517
956
|
if (pageVal) {
|
|
518
957
|
const page = parseWindowParam(pageVal, "page", 1, "INVALID_PAGE");
|
|
519
958
|
const limit = resolveListLimitParam(rawLimit, {
|
|
@@ -566,6 +1005,12 @@ function parseQueryOptions(query, limits = {}, access) {
|
|
|
566
1005
|
if (text !== "true" && text !== "1" && text !== "false" && text !== "0") throw invalidParam(`Invalid \`distinct\` parameter: expected \`true\` or \`false\`, got ${JSON.stringify(String(distinctVal))}.`, "INVALID_DISTINCT");
|
|
567
1006
|
options.distinct = text === "true" || text === "1";
|
|
568
1007
|
}
|
|
1008
|
+
const countVal = getLastValue(query.count);
|
|
1009
|
+
if (countVal !== void 0 && countVal !== null && String(countVal) !== "") {
|
|
1010
|
+
const text = String(countVal).trim().toLowerCase();
|
|
1011
|
+
if (text !== "exact" && text !== "none") throw invalidParam(`Invalid \`count\` parameter: expected \`exact\` or \`none\`, got ${JSON.stringify(String(countVal))}.`, "INVALID_COUNT");
|
|
1012
|
+
options.count = text;
|
|
1013
|
+
}
|
|
569
1014
|
const afterVal = getLastValue(query.after);
|
|
570
1015
|
if (afterVal !== void 0 && afterVal !== null && String(afterVal).trim() !== "") {
|
|
571
1016
|
let cursor;
|
|
@@ -628,6 +1073,6 @@ function parseQueryOptions(query, limits = {}, access) {
|
|
|
628
1073
|
return options;
|
|
629
1074
|
}
|
|
630
1075
|
//#endregion
|
|
631
|
-
export { resolveListLimitParam as a,
|
|
1076
|
+
export { resolveListLimitParam as a, assertReadableFields as c, decodeCursor as d, reconcileCursorOrder as f, parseQueryOptions as i, requestViewer as l, parseAggregateSelect as n, assertNoHiddenAggregateNames as o, parseGroupBy as r, assertQueryFieldsReadable as s, orderByEntriesToTuples as t, cursorToStartAfter as u };
|
|
632
1077
|
|
|
633
|
-
//# sourceMappingURL=query-parser-
|
|
1078
|
+
//# sourceMappingURL=query-parser-DK9eMkb9.js.map
|