@rebasepro/server 0.23.0 → 0.24.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/README.md +1 -1
- package/bin/rebase-server.js +4 -2
- package/dist/{GCSStorageController-CjrA4PMo.js → GCSStorageController-BSiP1c-f.js} +22 -8
- package/dist/GCSStorageController-BSiP1c-f.js.map +1 -0
- package/dist/{S3StorageController-B6pKDNVj.js → S3StorageController-CAwFRgjV.js} +19 -7
- package/dist/S3StorageController-CAwFRgjV.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 +44 -10
- package/dist/api/rest/write-validation.d.ts +2 -2
- package/dist/api/types.d.ts +17 -1
- package/dist/{ast-schema-editor-Mvr50v_S.js → ast-schema-editor-CWqS_sLJ.js} +309 -11
- package/dist/ast-schema-editor-CWqS_sLJ.js.map +1 -0
- package/dist/auth/access.d.ts +105 -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-users-route.d.ts +1 -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 +67 -0
- package/dist/auth/impersonation.d.ts +110 -0
- package/dist/auth/index.d.ts +4 -2
- package/dist/auth/interfaces.d.ts +110 -59
- package/dist/auth/jwt.d.ts +49 -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 +41 -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/verify-credential.d.ts +28 -0
- package/dist/{auth-B-GIMpDG.js → auth-DMLngxn_.js} +2159 -569
- package/dist/auth-DMLngxn_.js.map +1 -0
- package/dist/backend-DTAOsLQc.js.map +1 -1
- package/dist/backup/backup-common.d.ts +10 -0
- package/dist/backup/backup-routes.d.ts +24 -4
- package/dist/backup/backup-schedule.d.ts +33 -0
- package/dist/backup/backup-storage.d.ts +14 -0
- package/dist/backup/index.d.ts +2 -0
- package/dist/backup-CN0s50D2.js +444 -0
- package/dist/backup-CN0s50D2.js.map +1 -0
- package/dist/boot/bundle.d.ts +19 -0
- package/dist/boot/env.d.ts +49 -4
- package/dist/boot/security-headers.d.ts +26 -0
- package/dist/boot/static-routing.d.ts +56 -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-fz8i4pxs.js} +17 -4
- package/dist/contract-routes-fz8i4pxs.js.map +1 -0
- package/dist/cron/cron-scheduler.d.ts +25 -20
- package/dist/cron/cron-store.d.ts +6 -2
- package/dist/{cron-loader-DfTj2Hbi.js → cron-loader-CwaANlOG.js} +4 -4
- package/dist/cron-loader-CwaANlOG.js.map +1 -0
- package/dist/{cron-routes-eE8nif_b.js → cron-routes-Bc-SB0Se.js} +10 -7
- package/dist/cron-routes-Bc-SB0Se.js.map +1 -0
- package/dist/{cron-scheduler-B0pLfAix.js → cron-scheduler-CYQgco86.js} +52 -34
- package/dist/cron-scheduler-CYQgco86.js.map +1 -0
- package/dist/{cron-store-TcoGz-xS.js → cron-store-D2Q9-Aco.js} +10 -15
- package/dist/cron-store-D2Q9-Aco.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/templates.d.ts +22 -0
- package/dist/email/types.d.ts +26 -0
- package/dist/env.d.ts +1 -2
- package/dist/{errors-DWsX4yTd.js → errors-D6_y86c5.js} +102 -8
- package/dist/errors-D6_y86c5.js.map +1 -0
- package/dist/{function-loader-xnbDAPfa.js → function-loader-D7o5Epjj.js} +2 -2
- package/dist/{function-loader-xnbDAPfa.js.map → function-loader-D7o5Epjj.js.map} +1 -1
- package/dist/{function-routes-Chet4-lB.js → function-routes-CaNG4waN.js} +24 -12
- package/dist/function-routes-CaNG4waN.js.map +1 -0
- package/dist/functions/context.d.ts +17 -6
- package/dist/functions/guards.d.ts +22 -5
- package/dist/functions/index.d.ts +2 -2
- package/dist/functions/index.js +90 -36
- package/dist/functions/index.js.map +1 -1
- package/dist/{history-recorder-B4MpJfJK.js → history-recorder-Nr8zLvoU.js} +4 -4
- package/dist/{history-recorder-B4MpJfJK.js.map → history-recorder-Nr8zLvoU.js.map} +1 -1
- package/dist/{history-store-BhxWOuz9.js → history-store-rcAm_xFR.js} +2 -2
- package/dist/{history-store-BhxWOuz9.js.map → history-store-rcAm_xFR.js.map} +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.es.js +3084 -753
- package/dist/index.es.js.map +1 -1
- package/dist/init/health.d.ts +17 -2
- package/dist/init/shutdown.d.ts +10 -0
- package/dist/init.d.ts +54 -0
- package/dist/{jobs-CazMYhyy.js → jobs-DqYNfquG.js} +5 -5
- package/dist/{jobs-CazMYhyy.js.map → jobs-DqYNfquG.js.map} +1 -1
- package/dist/{jwt-DnQHNFCl.js → jwt-R6bSPMjk.js} +39 -15
- package/dist/{jwt-DnQHNFCl.js.map → jwt-R6bSPMjk.js.map} +1 -1
- package/dist/{keys-CogCQpxG.js → keys-GAVZqbqx.js} +3 -17
- package/dist/{keys-CogCQpxG.js.map → keys-GAVZqbqx.js.map} +1 -1
- 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 +15 -9
- package/dist/mcp/oauth-metadata.d.ts +21 -16
- package/dist/mcp/oauth-routes.d.ts +7 -1
- package/dist/{openapi-generator-O_O24MAT.js → openapi-generator-DAq_XVDu.js} +104 -13
- package/dist/openapi-generator-DAq_XVDu.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-BgiKJKvc.js} +6 -56
- package/dist/query-parser-BgiKJKvc.js.map +1 -0
- package/dist/{request-timeout-C_4C2BeR.js → request-timeout-DgH7j8qO.js} +3 -3
- package/dist/{request-timeout-C_4C2BeR.js.map → request-timeout-DgH7j8qO.js.map} +1 -1
- package/dist/schema-edit/apply-schema-change.d.ts +63 -3
- package/dist/schema-edit/project-root.d.ts +3 -2
- package/dist/schema-edit/remote-source.d.ts +9 -4
- package/dist/{schema-editor-routes-C5-lh_jO.js → schema-editor-routes-oIyuWl3L.js} +12 -7
- package/dist/schema-editor-routes-oIyuWl3L.js.map +1 -0
- package/dist/serve-spa.d.ts +58 -0
- package/dist/services/routed-realtime-service.d.ts +11 -0
- package/dist/soft-delete-params-BWPilMPF.js +59 -0
- package/dist/soft-delete-params-BWPilMPF.js.map +1 -0
- package/dist/{src-vkcwKXbT.js → src-CatHFUym.js} +439 -20
- package/dist/src-CatHFUym.js.map +1 -0
- package/dist/{src-pmvW7BFx.js → src-I3aG1PcY.js} +252 -70
- package/dist/src-I3aG1PcY.js.map +1 -0
- package/dist/storage/GCSStorageController.d.ts +2 -0
- package/dist/storage/LocalStorageController.d.ts +2 -0
- package/dist/storage/S3StorageController.d.ts +2 -0
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/property-limits.d.ts +41 -6
- package/dist/storage/request-keys.d.ts +15 -0
- package/dist/storage/requested-object.d.ts +74 -0
- package/dist/storage/routes.d.ts +36 -18
- package/dist/storage/tus-handler.d.ts +30 -5
- package/dist/storage/types.d.ts +19 -0
- package/dist/types-BfKcm9do.js.map +1 -1
- package/dist/utils/logger.d.ts +12 -0
- package/package.json +5 -5
- package/dist/GCSStorageController-CjrA4PMo.js.map +0 -1
- 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.map +0 -1
- package/dist/errors-DWsX4yTd.js.map +0 -1
- package/dist/function-routes-Chet4-lB.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/schema-editor-routes-C5-lh_jO.js.map +0 -1
- package/dist/src-pmvW7BFx.js.map +0 -1
- package/dist/src-vkcwKXbT.js.map +0 -1
|
@@ -2,60 +2,10 @@ 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 "./src-
|
|
7
|
-
import { t as ApiError } from "./errors-
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* The two query parameters soft delete adds to the REST surface.
|
|
11
|
-
*
|
|
12
|
-
* Kept in their own module so the call sites in `query-parser.ts` and the
|
|
13
|
-
* delete routes are a single line each: the parsing rules belong to soft
|
|
14
|
-
* delete, not to the parser, and a rule spread across the two files that read
|
|
15
|
-
* it is a rule that drifts.
|
|
16
|
-
*/
|
|
17
|
-
/** `?deleted=` — what to do about rows a soft delete has stamped. */
|
|
18
|
-
var DELETED_QUERY_PARAM = "deleted";
|
|
19
|
-
/** `?hard=` — ask for a real `DELETE` on a soft-delete collection. */
|
|
20
|
-
var HARD_DELETE_QUERY_PARAM = "hard";
|
|
21
|
-
/**
|
|
22
|
-
* `?deleted=include|only` → the driver's `withDeleted`.
|
|
23
|
-
*
|
|
24
|
-
* Spelled `deleted` on the wire and `withDeleted` in the driver, deliberately:
|
|
25
|
-
* the URL reads as a question about the rows (`?deleted=only` — "only the
|
|
26
|
-
* deleted ones"), and the driver option reads as an instruction about the query.
|
|
27
|
-
*
|
|
28
|
-
* A value neither word is a 400 rather than a silent fallback to the default.
|
|
29
|
-
* `?deleted=true` quietly hiding every deleted row is the worst of both: it
|
|
30
|
-
* looks like it worked and answers the opposite question. Absent is the
|
|
31
|
-
* default, which is "hide them".
|
|
32
|
-
*/
|
|
33
|
-
function parseWithDeleted(raw) {
|
|
34
|
-
if (raw === void 0 || raw === null || raw === "") return void 0;
|
|
35
|
-
const value = String(raw).trim().toLowerCase();
|
|
36
|
-
if (value === "include") return true;
|
|
37
|
-
if (value === "only") return "only";
|
|
38
|
-
throw ApiError.badRequest(`Invalid \`?${DELETED_QUERY_PARAM}=${String(raw)}\`. It takes 'include' (live rows and deleted ones) or 'only' (deleted rows alone). Omit it to see only the live rows.`, "INVALID_DELETED_PARAM");
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* `?hard=true` → a real `DELETE` on a collection that soft-deletes.
|
|
42
|
-
*
|
|
43
|
-
* Needs no permission beyond the delete it replaces: it is the same verb, and a
|
|
44
|
-
* second access-control surface for one operation is a second thing to get
|
|
45
|
-
* wrong. What it changes is whether the row can be restored.
|
|
46
|
-
*
|
|
47
|
-
* Only the exact words `true` and `1` mean yes. Anything else is a 400, not a
|
|
48
|
-
* "no" — a typo that silently soft-deletes when the caller asked to purge is a
|
|
49
|
-
* caller who believes the data is gone.
|
|
50
|
-
*/
|
|
51
|
-
function parseHardDelete(raw) {
|
|
52
|
-
if (raw === void 0 || raw === null || raw === "") return false;
|
|
53
|
-
const value = String(raw).trim().toLowerCase();
|
|
54
|
-
if (value === "true" || value === "1") return true;
|
|
55
|
-
if (value === "false" || value === "0") return false;
|
|
56
|
-
throw ApiError.badRequest(`Invalid \`?${HARD_DELETE_QUERY_PARAM}=${String(raw)}\`. It takes 'true' or 'false'.`, "INVALID_HARD_PARAM");
|
|
57
|
-
}
|
|
58
|
-
//#endregion
|
|
5
|
+
import { C as decodeCursor, E as restrictedFieldNames, W as resolveCollectionRelations, _ as OrderBySpecError, a as deserializeFilter, b as CursorError, et as ListLimitError, f as IncludeSpecError, h as normalizeInclude, i as UnknownFilterOperatorError, nt as resolveClientListLimit, o as deserializeLogicalCondition, p as deserializeInclude, r as RESERVED_QUERY_KEYS, w as reconcileCursorOrder, x as CursorMismatchError } from "./src-I3aG1PcY.js";
|
|
6
|
+
import "./src-CatHFUym.js";
|
|
7
|
+
import { t as ApiError } from "./errors-D6_y86c5.js";
|
|
8
|
+
import { i as parseWithDeleted, t as DELETED_QUERY_PARAM } from "./soft-delete-params-BWPilMPF.js";
|
|
59
9
|
//#region src/api/rest/field-access-query.ts
|
|
60
10
|
/**
|
|
61
11
|
* A read may not name a field the caller cannot read.
|
|
@@ -628,6 +578,6 @@ function parseQueryOptions(query, limits = {}, access) {
|
|
|
628
578
|
return options;
|
|
629
579
|
}
|
|
630
580
|
//#endregion
|
|
631
|
-
export { resolveListLimitParam as a, requestViewer as c, parseQueryOptions as i,
|
|
581
|
+
export { resolveListLimitParam as a, requestViewer as c, parseQueryOptions as i, parseAggregateSelect as n, assertQueryFieldsReadable as o, parseGroupBy as r, assertReadableFields as s, orderByEntriesToTuples as t };
|
|
632
582
|
|
|
633
|
-
//# sourceMappingURL=query-parser-
|
|
583
|
+
//# sourceMappingURL=query-parser-BgiKJKvc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query-parser-BgiKJKvc.js","names":[],"sources":["../src/api/rest/field-access-query.ts","../src/api/rest/query-parser.ts"],"sourcesContent":["import type { CollectionConfig, IncludeSpec } from \"@rebasepro/types\";\nimport type { FilterCondition, LogicalCondition } from \"@rebasepro/types\";\nimport { type FieldViewer, type IncludeNode, normalizeInclude, resolveCollectionRelations, restrictedFieldNames } from \"@rebasepro/common\";\nimport { ApiError } from \"../errors\";\n\n/**\n * A read may not name a field the caller cannot read.\n *\n * The strip in the row pipeline is what keeps the *value* off the wire. This is\n * the other half, and without it the value is still readable one bit at a time:\n * `?salary=gt.100000` returns the rows whose withheld salary is above 100k, and\n * `?orderBy=salary` returns them in order of it. A column no response can carry\n * has to be a column no query can interrogate, or the read rule is decoration.\n *\n * The refusal names the field. That is deliberate and it is not a leak: the\n * published OpenAPI lists every property of every collection, including the ones\n * a given caller cannot read, because the document is one document and is served\n * off the app rather than off the authenticated data router. Hiding the name\n * here would protect nothing and would answer a caller's genuine typo with\n * \"unknown field\", sending them to look for a spelling mistake that is not\n * there. Field *names* are public; field *values* are not.\n *\n * @module\n */\n\n/**\n * The roles behind a request, as a viewer a field rule can judge.\n *\n * Never `undefined`, and that is the point. `undefined` means the trusted server\n * plane in {@link FieldViewer}, which satisfies every non-empty role list — so\n * returning it for a request that merely has no `user` on the context would\n * hand an unauthenticated caller every field in the database. The auth\n * middleware scopes such a request's driver as `roles: [\"anon\"]` but sets no\n * `user`, so the fallback here has to be the same list the driver was scoped\n * with, not nothing.\n *\n * @param c anything carrying the Hono context's `get` — the batch route passes a\n * shim rather than the context itself, exactly as the API-key\n * permission check does.\n */\nexport function requestViewer(c: { get: (key: never) => unknown }): FieldViewer {\n const user = c.get(\"user\" as never) as { roles?: readonly string[] } | undefined;\n return { roles: user?.roles ?? ANON_ROLES };\n}\n\n/** What the auth middleware scopes an unauthenticated request's driver with. */\nconst ANON_ROLES: readonly string[] = Object.freeze([\"anon\"]);\n\n/** Which query parameter a refused field arrived in, for the message. */\ntype Where = \"filter\" | \"orderBy\" | \"fields\" | \"select\" | \"groupBy\" | \"vector_search\";\n\nconst WHERE_LABEL: Record<Where, string> = {\n filter: \"a filter\",\n orderBy: \"`orderBy`\",\n fields: \"`fields`\",\n select: \"`select`\",\n groupBy: \"`groupBy`\",\n vector_search: \"`vector_search`\"\n};\n\n/** Every column a logical group compares, however deeply nested. */\nfunction logicalColumns(logical: LogicalCondition | undefined, into: string[]): void {\n if (!logical?.conditions) return;\n for (const condition of logical.conditions) {\n if (\"conditions\" in condition) logicalColumns(condition as LogicalCondition, into);\n else if ((condition as FilterCondition).column) into.push((condition as FilterCondition).column);\n }\n}\n\n/**\n * The bare column an `orderBy` key names, or `undefined` for one that is not a\n * column at all.\n *\n * A sort key may be a relation aggregate (`comments.count()`) or one of the\n * computed keys a search adds (`_score`, `_distance`). Neither is a property of\n * this collection, so neither is a field this rule has anything to say about;\n * the field it *would* have named is checked by the same walk one level down\n * when the driver resolves the relation.\n */\nfunction orderByColumn(field: string): string | undefined {\n if (field.startsWith(\"_\")) return undefined;\n if (field.includes(\"(\") || field.includes(\".\")) return undefined;\n return field;\n}\n\n/**\n * Refuse the request when any of `names` is a field this caller cannot read.\n *\n * Exported so the aggregate route — whose `select` and `groupBy` are parsed\n * outside `parseQueryOptions` — applies the identical rule. `count(*)` over a\n * withheld column is the same disclosure as reading it, one predicate at a time.\n */\nexport function assertReadableFields(\n names: readonly (string | undefined)[],\n collection: CollectionConfig,\n viewer: FieldViewer | undefined,\n where: Where\n): void {\n if (names.length === 0) return;\n const { refused } = restrictedFieldNames(collection, viewer, \"read\");\n if (refused.size === 0) return;\n\n const named = [...new Set(names.filter((n): n is string => n !== undefined && refused.has(n)))];\n if (named.length === 0) return;\n\n throw ApiError.badRequest(\n `${named.map(f => `'${f}'`).join(\", \")} ${named.length > 1 ? \"are\" : \"is\"} not readable ` +\n `on '${collection.slug}' with your roles, so ${named.length > 1 ? \"they\" : \"it\"} cannot be used in ` +\n `${WHERE_LABEL[where]}.`,\n \"FIELD_NOT_READABLE\",\n {\n collection: collection.slug,\n fields: named,\n violations: named.map(field => ({\n field,\n code: \"access\",\n message: `'${field}' is not readable with your roles.`\n }))\n }\n );\n}\n\n/**\n * The whole of a parsed read request, checked in one pass.\n *\n * One call rather than five, because five call sites is five chances to add a\n * sixth query parameter and forget it — which is exactly how `?or=` came to be\n * parsed and then dropped by the list route.\n */\nexport function assertQueryFieldsReadable(\n options: {\n where?: Record<string, unknown>;\n logical?: LogicalCondition;\n orderBy?: { field: string }[];\n fields?: string[];\n include?: IncludeSpec;\n vectorSearch?: { property: string };\n },\n collection: CollectionConfig,\n viewer: FieldViewer | undefined\n): void {\n const filtered: string[] = [];\n if (options.where) filtered.push(...Object.keys(options.where));\n logicalColumns(options.logical, filtered);\n assertReadableFields(filtered, collection, viewer, \"filter\");\n\n assertReadableFields(\n (options.orderBy ?? []).map(entry => orderByColumn(entry.field)),\n collection, viewer, \"orderBy\"\n );\n\n assertReadableFields(options.fields ?? [], collection, viewer, \"fields\");\n\n // A similarity search reads the vector it ranks by: `_distance` from a\n // point the caller chose is a measurement of it, enough of them locate it,\n // and `vector_threshold` alone answers \"is it within r of here\".\n assertReadableFields(\n options.vectorSearch ? [options.vectorSearch.property] : [],\n collection, viewer, \"vector_search\"\n );\n\n const include = options.include !== undefined ? normalizeInclude(options.include) : undefined;\n if (include) assertIncludeFieldsReadable(include.tree, collection, viewer);\n}\n\n/**\n * Every relation an `include` loads, judged like the read it is part of.\n *\n * An include's own `where`, `logical`, `orderBy` and `fields` read the columns\n * of the relation's *target*: `?include={\"staff\":{\"where\":{\"salary\":[\">\",100000]}}}`\n * answers which departments employ someone paid over 100k, and `orderBy` with a\n * `limit` of one names who — while `?salary=gt.100000` on `staff` itself is\n * refused. So each node is checked against its target's rules, level by level.\n *\n * A name that is not a relation of the collection is skipped rather than\n * refused here: the driver refuses it with a 400 naming the relations there\n * are, and loads nothing for it. Looked up by exact key, as the driver does, so\n * both judge the same relation.\n */\nfunction assertIncludeFieldsReadable(\n tree: Record<string, IncludeNode>,\n collection: CollectionConfig,\n viewer: FieldViewer | undefined\n): void {\n const relations = resolveCollectionRelations(collection);\n for (const [key, node] of Object.entries(tree)) {\n const relation = relations[key];\n if (!relation) continue;\n const target = relation.target();\n assertQueryFieldsReadable({\n where: node.where,\n logical: node.logical,\n orderBy: node.orderBy?.map(([field]) => ({ field })),\n fields: node.fields\n }, target, viewer);\n assertIncludeFieldsReadable(node.children, target, viewer);\n }\n}\n","import type { CollectionConfig, FilterValues, ListLimitBounds, LogicalCondition, NullsPlacement, OrderByTuple, VectorSearchParams } from \"@rebasepro/types\";\nimport { toCanonicalOp, resolveClientListLimit, ListLimitError, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT } from \"@rebasepro/types\";\nimport type { DecodedCursor } from \"@rebasepro/common\";\nimport {\n CursorError,\n CursorMismatchError,\n type FieldViewer,\n IncludeSpecError,\n OrderBySpecError,\n decodeCursor,\n deserializeFilter,\n deserializeInclude,\n deserializeLogicalCondition,\n normalizeInclude,\n reconcileCursorOrder,\n RESERVED_QUERY_KEYS,\n UnknownFilterOperatorError\n} from \"@rebasepro/common\";\nimport { QueryOptions } from \"../types\";\nimport { ApiError } from \"../errors\";\nimport { DELETED_QUERY_PARAM, parseWithDeleted } from \"./soft-delete-params\";\nimport { assertQueryFieldsReadable } from \"./field-access-query\";\n\nexport const mapOperator = (op: string) => toCanonicalOp(op) ?? null;\n\n/**\n * A malformed query parameter, refused with a 400.\n *\n * Every rejection in this file is one of these, and every one of them is\n * `expected` — the flag `errorHandler` reads to log a routine outcome at debug\n * instead of warn. A client that mistypes an operator, a sort direction or a\n * limit is not an incident: nothing on the server is wrong, the request never\n * reached the database, and the caller has already been told what to fix in the\n * response body. Left at warn, a single frontend holding a stale field name\n * writes a `⚠️` line per request forever, and the warn level stops meaning\n * anything — which is why \"routine 4xx logs at WARN\" is a standing finding\n * against this API.\n *\n * Not a factory on `ApiError`: the class's members are part of the tracked\n * runtime surface (`api-surface/server.api.txt`), and this needs no addition to\n * it. `ApiError.unauthenticated` is the same idea one status code up.\n */\nfunction invalidParam(message: string, code: string, details?: unknown): ApiError {\n return new ApiError(400, code, message, details, true);\n}\n\n/**\n * Decode a filter, turning the shared codec's operator rejection into a 400.\n *\n * `deserializeFilter` lives in `@rebasepro/common`, which cannot throw an\n * `ApiError` — it does not depend on this package, and the browser SDK decodes\n * through the same function and has nothing to render one with. So it throws\n * `UnknownFilterOperatorError`, and the HTTP boundary is where that becomes a\n * status code. Same seam `parseLogicalGroup` uses for the nesting bound.\n *\n * Without this the operator string became a *value*: `?where={\"title\":\n * [\"!!\",\"Hello\"]}` compiled to `title IN ('!!','Hello')` and answered 200 with\n * the row the caller was filtering out, and `{\"id\":[\">>\",0]}` reached Postgres\n * and came back a 500 quoting `invalid input syntax for type integer`. Both are\n * malformed requests and now say so.\n */\nfunction decodeFilter(query: Record<string, unknown>): FilterValues<string> {\n try {\n return deserializeFilter(query);\n } catch (e) {\n if (e instanceof UnknownFilterOperatorError) {\n throw invalidParam(e.message, e.code, e.details);\n }\n throw e;\n }\n}\n\nfunction getLastValue(val: unknown): unknown {\n if (Array.isArray(val)) {\n return val[val.length - 1];\n }\n return val;\n}\n\n/**\n * Parse an `or(...)` / `and(...)` logical group from its wire form.\n *\n * The wire carries the inner conditions wrapped in parens (e.g.\n * `(status.eq.active,age.gte.18)`); we re-attach the `or`/`and` prefix and\n * delegate to the canonical filter dialect (`@rebasepro/common`). Values are\n * preserved as strings — type coercion is the schema-aware driver's job, so\n * this path stays byte-for-byte consistent with the SDK/admin path (which\n * also parses via the shared dialect).\n */\nfunction parseLogicalGroup(type: \"or\" | \"and\" | \"not\", raw: unknown): LogicalCondition | undefined {\n let inner = String(raw).trim();\n if (inner.startsWith(\"(\") && inner.endsWith(\")\")) {\n inner = inner.slice(1, -1);\n }\n inner = inner.trim();\n if (!inner) return undefined;\n let parsed;\n try {\n parsed = deserializeLogicalCondition(`${type}(${inner})`);\n } catch (e) {\n // The parser refuses a nesting depth no real filter reaches. That is a\n // request problem, and without this it surfaced as a 500 — the\n // unbounded version reached `RangeError: Maximum call stack size\n // exceeded`, which tells the caller nothing about their filter.\n throw invalidParam(\n `Invalid \\`${type}\\` parameter: ${e instanceof Error ? e.message : String(e)}`,\n \"INVALID_LOGICAL_GROUP\"\n );\n }\n return \"type\" in parsed ? parsed : undefined;\n}\n\n/**\n * Parse the `?where=` JSON filter object.\n *\n * This is the dialect the OpenAPI document publishes on every\n * `GET /api/data/{slug}` — `{\"status\":[\"==\",\"active\"]}`: field → canonical\n * `[WhereFilterOp, value]` tuple. It is normalized through the same\n * `deserializeFilter` as the `?field=op.value` params below, so a value that\n * arrives as a PostgREST dot-string (`{\"status\":\"eq.active\"}`) or as a bare\n * scalar (`{\"status\":\"active\"}`) compiles to the same condition. Unlike the\n * querystring dialect, JSON carries types — a number stays a number.\n *\n * A malformed value is a 400 rather than a silent drop: dropping the filter\n * would run the read unfiltered and return everything RLS happens to allow.\n */\nfunction parseWhereParam(raw: unknown): FilterValues<string> | undefined {\n const str = String(raw).trim();\n if (!str) return undefined;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(str);\n } catch {\n throw invalidParam(\n \"Invalid `where` parameter: expected a JSON object, e.g. {\\\"status\\\":[\\\"==\\\",\\\"active\\\"]}\",\n \"INVALID_WHERE\"\n );\n }\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n throw invalidParam(\n \"Invalid `where` parameter: expected a JSON object mapping fields to conditions, \"\n + \"e.g. {\\\"status\\\":[\\\"==\\\",\\\"active\\\"]}\",\n \"INVALID_WHERE\"\n );\n }\n\n const filter = decodeFilter(parsed as Record<string, unknown>);\n return Object.keys(filter).length > 0 ? filter : undefined;\n}\n\ntype OrderByEntry = { field: string; direction: \"asc\" | \"desc\"; nulls?: NullsPlacement };\n\n/**\n * The parsed entries as the driver contract spells them: `[field, direction]`\n * tuples in order of significance.\n *\n * The REST layer used to hand the driver `orderBy[0].field` and drop the rest,\n * so `?orderBy=[{\"field\":\"roles\"},{\"field\":\"created_at\",\"direction\":\"desc\"}]`\n * — a shape this parser has always accepted and validated in full — sorted by\n * `roles` alone and returned the ties in whatever order Postgres pleased.\n */\nexport function orderByEntriesToTuples(entries?: OrderByEntry[]): OrderByTuple[] | undefined {\n if (!entries || entries.length === 0) return undefined;\n return entries.map(({ field, direction, nulls }) => (nulls\n ? [field, direction, nulls]\n : [field, direction]) as OrderByTuple);\n}\n\nfunction invalidOrderBy(detail: string): never {\n throw invalidParam(\n `Invalid \\`orderBy\\` parameter: ${detail}. Expected \\`field\\`, \\`field:desc\\`, `\n + \"`field:desc:last`, or a JSON array like \"\n + \"[{\\\"field\\\":\\\"created_at\\\",\\\"direction\\\":\\\"desc\\\",\\\"nulls\\\":\\\"last\\\"}]\",\n \"INVALID_ORDER_BY\"\n );\n}\n\n/**\n * The `nulls` slot: `first`/`last`, or a refusal naming the entry.\n *\n * Refused rather than defaulted, for the reason every other parameter here is:\n * a sort quietly ordered by a convention the caller did not ask for reads as\n * though it obeyed them. See {@link NullsPlacement} for what the default is\n * when the slot is simply absent.\n */\nfunction toNulls(raw: unknown, context: string): NullsPlacement | undefined {\n if (raw === undefined || raw === null || raw === \"\") return undefined;\n if (raw !== \"first\" && raw !== \"last\") {\n invalidOrderBy(`${context} has nulls '${String(raw)}'`);\n }\n return raw;\n}\n\n/** The aggregate functions `?select=` accepts. */\nconst AGGREGATE_FUNCTIONS = new Set([\"count\", \"sum\", \"avg\", \"min\", \"max\"]);\n\nexport interface ParsedAggregate {\n fn: \"count\" | \"sum\" | \"avg\" | \"min\" | \"max\";\n /** Absent only for `count()`, which counts rows rather than values. */\n field?: string;\n /** The key this appears under in the response. */\n alias: string;\n}\n\n/**\n * Parse `?select=count(),sum(total),avg(total)`.\n *\n * The spelling is SQL's, because whoever writes it is thinking in SQL and\n * because any other spelling has to be learned first. `count()` with no field\n * counts rows; every other function names a column.\n *\n * Aliases are derived rather than accepted: `sum(total)` returns as\n * `sum_total`, `count()` as `count`. Letting a caller choose would mean\n * checking their alias is not also a `groupBy` field — a rule nobody would\n * guess, and a silently overwritten value if it went unchecked.\n */\nexport function parseAggregateSelect(raw: unknown): ParsedAggregate[] | undefined {\n const value = getLastValue(raw);\n if (!value) return undefined;\n\n const entries = String(value).split(\",\").map(s => s.trim()).filter(Boolean);\n if (entries.length === 0) return undefined;\n\n return entries.map((entry) => {\n const match = /^([a-z]+)\\(\\s*([A-Za-z0-9_]*)\\s*\\)$/i.exec(entry);\n if (!match) {\n throw invalidParam(\n `Invalid \\`select\\` entry \"${entry}\". Expected \\`fn(field)\\`, e.g. \\`sum(total)\\` or \\`count()\\`.`,\n \"INVALID_AGGREGATE_SELECT\"\n );\n }\n\n const fn = match[1].toLowerCase();\n const field = match[2] || undefined;\n\n if (!AGGREGATE_FUNCTIONS.has(fn)) {\n throw invalidParam(\n `Unknown aggregate function \"${fn}\". Expected: ${[...AGGREGATE_FUNCTIONS].join(\", \")}.`,\n \"INVALID_AGGREGATE_FUNCTION\"\n );\n }\n if (fn !== \"count\" && !field) {\n // `sum()` has no sensible reading, and guessing one would be\n // inventing a column on the caller's behalf.\n throw invalidParam(\n `\\`${fn}()\\` needs a field, e.g. \\`${fn}(total)\\`. Only \\`count()\\` may be empty.`,\n \"INVALID_AGGREGATE_SELECT\"\n );\n }\n\n return {\n fn: fn as ParsedAggregate[\"fn\"],\n field,\n alias: field ? `${fn}_${field}` : fn\n };\n });\n}\n\n/** Parse `?groupBy=status,country`. */\nexport function parseGroupBy(raw: unknown): string[] | undefined {\n const value = getLastValue(raw);\n if (!value) return undefined;\n const fields = String(value).split(\",\").map(s => s.trim()).filter(Boolean);\n return fields.length > 0 ? fields : undefined;\n}\n\n/** `asc`/`desc`, in any case. Anything else is a request to sort in a way that does not exist. */\nfunction toDirection(raw: unknown, context: string): \"asc\" | \"desc\" {\n if (raw === undefined || raw === null) return \"asc\";\n if (typeof raw !== \"string\") invalidOrderBy(`${context} has a non-string \\`direction\\``);\n const lowered = raw.toLowerCase();\n if (lowered !== \"asc\" && lowered !== \"desc\") {\n invalidOrderBy(`${context} has direction '${raw}'`);\n }\n return lowered;\n}\n\n/** One entry: the canonical `{field, direction}`, or the `field:direction` shorthand as a string. */\nfunction toOrderByEntry(raw: unknown, index: number): OrderByEntry {\n const context = `entry ${index}`;\n if (typeof raw === \"string\") {\n // Split here rather than through `deserializeOrderBy`, which is the\n // *client* end of the codec and normalises anything that is not\n // literally \"desc\" to \"asc\". Routed through it, `?orderBy=x:DESC`\n // reached `toDirection` already collapsed to \"asc\" and answered 200\n // with the rows in the opposite order — a newest-first list showing\n // the oldest rows — and `x:sideways` did the same. The direction token\n // has to arrive here raw for `toDirection` to have anything to refuse.\n const idx = raw.indexOf(\":\");\n const field = (idx === -1 ? raw : raw.slice(0, idx)).trim();\n if (!field) invalidOrderBy(`${context} is an empty field name`);\n if (idx === -1) return { field, direction: \"asc\" };\n // `field:direction:nulls`. The third segment is optional, so every\n // `field:desc` written before it existed parses exactly as it did.\n const rest = raw.slice(idx + 1);\n const nullsIdx = rest.indexOf(\":\");\n const direction = toDirection(nullsIdx === -1 ? rest : rest.slice(0, nullsIdx), context);\n const nulls = nullsIdx === -1 ? undefined : toNulls(rest.slice(nullsIdx + 1), context);\n return nulls ? { field, direction, nulls } : { field, direction };\n }\n if (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) {\n invalidOrderBy(`${context} is not a field name or a {field, direction} object`);\n }\n const entry = raw as Record<string, unknown>;\n if (typeof entry.field !== \"string\" || entry.field.trim() === \"\") {\n invalidOrderBy(`${context} has no \\`field\\``);\n }\n const nulls = toNulls(entry.nulls, context);\n const direction = toDirection(entry.direction, context);\n return nulls ? { field: entry.field, direction, nulls } : { field: entry.field, direction };\n}\n\n/**\n * Parse the `orderBy` query parameter.\n *\n * The field *name* has been validated against the schema for a while — an\n * `?orderBy=titel` is a 400 rather than 200 with unsorted rows, on the grounds\n * that silently dropping the sort leaves the caller believing in an order that\n * is not there. The parameter's *shape* was never checked the same way, and it\n * failed in exactly the same silent manner one layer earlier: whatever\n * `JSON.parse` returned was assigned to an option declared as an array of\n * `{field, direction}`, and the REST layer reads only `orderBy[0].field`. So\n * `?orderBy={\"field\":\"name\"}` — an object rather than an array, and the most\n * natural thing for a client to try — read `undefined`, dropped the ORDER BY,\n * and answered 200. So did a number, a boolean, `null`, and `[\"name\"]`.\n *\n * This refuses those, the way `parseWhereParam` above already refuses a\n * malformed filter and for the same reason. What it keeps working is every\n * shape that worked before: the `field` and `field:desc` shorthands, and the\n * canonical JSON array.\n */\nfunction parseOrderByParam(raw: unknown): OrderByEntry[] | undefined {\n if (Array.isArray(raw)) {\n // A repeated query parameter arrives pre-split; treat it as the list.\n return raw.length === 0 ? undefined : raw.map(toOrderByEntry);\n }\n\n const str = String(raw).trim();\n if (!str) return undefined;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(str);\n } catch {\n // Not JSON at all, so it is the `field:direction` shorthand.\n return [toOrderByEntry(str, 0)];\n }\n\n // `JSON.parse` succeeding says nothing about the shape being usable.\n if (Array.isArray(parsed)) {\n if (parsed.length === 0) return undefined;\n return parsed.map(toOrderByEntry);\n }\n if (typeof parsed === \"string\") return [toOrderByEntry(parsed, 0)];\n if (typeof parsed === \"object\" && parsed !== null) {\n // A bare `{field, direction}` is a near miss rather than nonsense, but\n // accepting it would leave two spellings of one parameter. Name it.\n invalidOrderBy(\"a single object was given where a JSON array was expected\");\n }\n invalidOrderBy(`${typeof parsed} is not a field name or a list of them`);\n}\n\n// Re-exported for callers/tests that reference the REST list bounds. The\n// numbers and the rule live in `@rebasepro/types` so the REST parser and the\n// WebSocket ingress enforce ONE shared guarantee. See `resolveClientListLimit`.\nexport { DEFAULT_LIST_LIMIT, DEFAULT_VECTOR_LIST_LIMIT, MAX_LIST_LIMIT } from \"@rebasepro/types\";\n\n/**\n * Overridable list-pagination bounds for {@link parseQueryOptions}. Without\n * these, `GET /<collection>` with no `?limit` would buffer the ENTIRE table\n * into a JS array + JSON response (a trivial OOM/DoS), and `?limit=100000000`\n * would be honoured verbatim.\n */\nexport interface ListLimitOptions {\n /**\n * Page size used when the client sends no `?limit`. Applied to plain and\n * text-search reads — a vector search falls back to its own default (10).\n */\n defaultLimit?: number;\n /** Largest `?limit` a client may ask for. A larger one is a 400, not a clamp. */\n maxLimit?: number;\n}\n\n/**\n * {@link resolveClientListLimit} for an HTTP route: the same bounds, answered\n * with a 400 rather than a 500.\n *\n * The shared resolver throws a `ListLimitError`, which carries `status` — but\n * the Hono error handler discriminates on `statusCode`, so an unconverted one\n * reaches the client as `INTERNAL_ERROR` with its message stripped, telling the\n * caller nothing about the parameter it got wrong. Every REST list ingress\n * routes its `limit` through here so all of them name the ceiling the same way.\n */\nexport function resolveListLimitParam(\n rawLimit: number | string | null | undefined,\n opts: ListLimitBounds & { vectorSearch?: boolean } = {}\n): number {\n try {\n return resolveClientListLimit(rawLimit, opts);\n } catch (e) {\n if (e instanceof ListLimitError) {\n throw invalidParam(e.message, \"INVALID_LIMIT\");\n }\n throw e;\n }\n}\n\n/**\n * A whole number at or above `minimum`, or a 400 naming the parameter.\n *\n * `parseInt` was the whole of the validation, and it answers `NaN` for\n * `?offset=abc` and a negative for `?offset=-5`. Neither was checked:\n *\n * - `NaN` reached the driver, where `OFFSET NaN` is a 500 about a syntax error\n * in a query the caller never wrote;\n * - `?page=0` computed `offset = -limit`, a negative offset, which Postgres\n * also refuses — and `?page=-3` refused deeper;\n * - `?offset=1.5` truncated silently to `1`, so the caller paged a window they\n * had not asked for.\n *\n * Every one of those is the caller's parameter, so every one is a 400 named\n * after the parameter — the shape `INVALID_LIMIT` already had, and the reason a\n * limit is *rejected* rather than clamped: a window quietly different from the\n * one asked for cannot be told apart from having reached the end.\n *\n * `expected: true` on the error (via {@link invalidParam}): a mistyped query\n * parameter never reached the database and the response body already says what\n * to fix, so it logs at debug rather than putting a warning in production logs\n * on every request from a client holding a stale link.\n */\nfunction parseWindowParam(raw: unknown, name: string, minimum: number, code: string): number {\n const text = String(raw).trim();\n const value = Number(text);\n if (text === \"\" || !Number.isFinite(value) || !Number.isInteger(value) || value < minimum) {\n throw invalidParam(\n `Invalid \\`${name}\\` parameter: expected a whole number ${minimum === 0 ? \"of 0 or more\" : `of ${minimum} or more`}, got ${JSON.stringify(text)}.`,\n code\n );\n }\n return value;\n}\n\n/**\n * Parse query parameters into QueryOptions\n */\nexport function parseQueryOptions(\n query: Record<string, unknown>,\n limits: ListLimitOptions = {},\n /**\n * The collection being read and who is reading it. Optional so the parser\n * stays a pure parser for the callers that have neither (tests, the WS\n * ingress, anything parsing a query it is not about to run); when present,\n * a `where`, `orderBy` or `fields` naming a field the caller cannot read is\n * a 400 rather than a query the driver would happily answer. See\n * {@link assertQueryFieldsReadable}.\n */\n access?: { collection: CollectionConfig; viewer?: FieldViewer }\n): QueryOptions {\n const options: QueryOptions = {};\n const rawLimit = getLastValue(query.limit) as number | string | null | undefined;\n\n // `?deleted=include|only` — soft delete. See `soft-delete-params.ts`.\n const withDeleted = parseWithDeleted(getLastValue(query[DELETED_QUERY_PARAM]));\n if (withDeleted !== undefined) options.withDeleted = withDeleted;\n\n const offsetVal = getLastValue(query.offset);\n if (offsetVal) options.offset = parseWindowParam(offsetVal, \"offset\", 0, \"INVALID_OFFSET\");\n\n const pageVal = getLastValue(query.page);\n if (pageVal) {\n const page = parseWindowParam(pageVal, \"page\", 1, \"INVALID_PAGE\");\n // Page stride uses the same bounded page size the read will use, so\n // pages neither overlap nor gap. (Vector search never paginates by\n // page, so the plain/text default is correct here.)\n const limit = resolveListLimitParam(rawLimit, {\n defaultLimit: limits.defaultLimit,\n maxLimit: limits.maxLimit\n });\n options.offset = (page - 1) * limit;\n }\n\n // ── Logical conditions (or / and / not) ────────────────────────────\n //\n // `?not=(status.eq.draft,views.gte.10)` negates the **conjunction** of its\n // conditions: `not(a)` is `NOT a`, `not(a,b)` is `NOT (a AND b)`. The rule\n // lives on `LogicalCondition` and is applied identically by this parser,\n // the shared wire codec and every driver compiler — one negation, one\n // meaning. A group nests, so `?not=(or(a,b))` is the De Morgan case.\n //\n // Three parameters and one slot, so exactly one applies. `or` wins over\n // `and`, and both over `not` — the precedence `or`/`and` already had, with\n // the third added at the end rather than in the middle, where it would have\n // silently changed which of two existing parameters was honoured.\n const orVal = getLastValue(query.or);\n const andVal = getLastValue(query.and);\n const notVal = getLastValue(query.not);\n if (orVal) {\n const logical = parseLogicalGroup(\"or\", orVal);\n if (logical) options.logical = logical;\n } else if (andVal) {\n const logical = parseLogicalGroup(\"and\", andVal);\n if (logical) options.logical = logical;\n } else if (notVal) {\n const logical = parseLogicalGroup(\"not\", notVal);\n if (logical) options.logical = logical;\n }\n\n // ── PostgREST-style field filters: ?field=op.value ─────────────────\n // Delegate to the canonical filter dialect (the single source of truth\n // for the wire grammar: operator codes, list/escape handling, implicit\n // eq). Values stay strings; the schema-aware driver coerces them to\n // column types. This keeps the REST path byte-for-byte consistent with\n // the SDK/admin path, which parses through the same `deserializeFilter`.\n //\n // `where` is reserved: it is the JSON filter dialect (see\n // `parseWhereParam`), not a column named \"where\". Leaving it out of this\n // list made the documented `?where={...}` compile as a filter on a\n // nonexistent field — which used to be dropped, widening the read to the\n // whole table, and is now a 400 `UNKNOWN_FILTER_FIELD`.\n //\n // `select` and `groupBy` are reserved for the same reason: on\n // `/aggregate` they are the request, and left out of this list\n // `?select=sum(total)` compiles into the filter as a comparison on a\n // column named \"select\" — a 400 on the one endpoint that requires it.\n // `not`, `after` and `distinct` join the list for the reason the comment\n // above gives: a reserved key left out of it compiles as a filter on a\n // column of that name, which is a 400 `UNKNOWN_FILTER_FIELD` on the one\n // request that needs the parameter. So do `?deleted=` and `?hard=`, which\n // ask about the soft-delete stamp rather than name a column.\n //\n // The list is shared with the SDK, which sends a filter on a column with\n // one of these names inside `?where=` rather than as its own parameter.\n const filterDict: Record<string, unknown> = {};\n for (const [key, rawValue] of Object.entries(query)) {\n if (RESERVED_QUERY_KEYS.has(key)) continue;\n filterDict[key] = rawValue;\n }\n // Both dialects may be sent together; an explicit `?field=op.value` wins\n // over the same field inside `where`, being the more specific request.\n const whereVal = getLastValue(query.where);\n const where = {\n ...(whereVal !== undefined && whereVal !== null ? parseWhereParam(whereVal) : undefined),\n ...decodeFilter(filterDict)\n };\n if (Object.keys(where).length > 0) {\n options.where = where;\n }\n\n // Sorting\n const orderByVal = getLastValue(query.orderBy);\n if (orderByVal) {\n options.orderBy = parseOrderByParam(orderByVal);\n }\n\n // ── Relation includes ──────────────────────────────────────────────\n //\n // Two spellings on one parameter, told apart by a leading `{`:\n //\n // ?include=author,comments.author — names and dotted paths\n // ?include={\"comments\":{\"limit\":5,\"include\":{\"author\":true}}}\n //\n // The flat form is what a human types and what every existing client\n // sends; the JSON form exists because the flat one has nowhere to put a\n // per-relation `limit`/`where`/`orderBy`/`fields`, and inventing a\n // punctuation for those (`comments(limit:5)`) would be a third grammar to\n // learn beside the two this API already has. Both compile to the same\n // request — `deserializeInclude` in `@rebasepro/common` is the codec, and\n // the SDK serialises through its inverse.\n const includeVal = getLastValue(query.include);\n if (includeVal !== undefined && includeVal !== null) {\n try {\n const include = deserializeInclude(String(includeVal));\n // Normalized for its *checks* — the depth bound and the shape of a\n // per-relation options object — and then discarded: what travels on\n // is the caller's own spelling, which the driver normalizes again\n // (idempotently) when it reads it. Validating here is what makes a\n // malformed include a 400 at the boundary rather than an\n // `IncludeSpecError` escaping from the driver as a 500, which is\n // what `?include=a.b.c.d` used to answer.\n normalizeInclude(include);\n options.include = include;\n } catch (e) {\n if (e instanceof IncludeSpecError) throw invalidParam(e.message, e.code);\n if (e instanceof OrderBySpecError) {\n throw invalidParam(`Invalid \\`include\\`: ${e.message}`, \"INVALID_INCLUDE\");\n }\n throw e;\n }\n }\n\n // Field selection. A projection at the driver, not a trim of the response:\n // the columns named here are the columns read.\n const fieldsVal = getLastValue(query.fields);\n if (fieldsVal) {\n const fieldsStr = String(fieldsVal).trim();\n options.fields = fieldsStr.split(\",\").map(s => s.trim()).filter(Boolean);\n }\n\n // `?distinct=true` — `SELECT DISTINCT` over the projection. Only `true`\n // and `1` mean yes; anything else is refused rather than read as \"no\",\n // because a `?distinct=1&` typo'd into `?distinct=ture` would otherwise\n // return duplicate rows while looking exactly like it had worked.\n const distinctVal = getLastValue(query.distinct);\n if (distinctVal !== undefined && distinctVal !== null && String(distinctVal) !== \"\") {\n const text = String(distinctVal).trim().toLowerCase();\n if (text !== \"true\" && text !== \"1\" && text !== \"false\" && text !== \"0\") {\n throw invalidParam(\n `Invalid \\`distinct\\` parameter: expected \\`true\\` or \\`false\\`, got ${JSON.stringify(String(distinctVal))}.`,\n \"INVALID_DISTINCT\"\n );\n }\n options.distinct = text === \"true\" || text === \"1\";\n }\n\n // ── Keyset cursor ──────────────────────────────────────────────────\n //\n // `?after=<meta.nextCursor>`. Decoded here so a malformed cursor is one\n // 400 in one place, and so the sort a cursor implies is settled before any\n // route reads `orderBy`: a request that names no sort adopts the cursor's,\n // and one that names a different sort is refused rather than seeked in an\n // order nobody asked for.\n const afterVal = getLastValue(query.after);\n if (afterVal !== undefined && afterVal !== null && String(afterVal).trim() !== \"\") {\n let cursor: DecodedCursor;\n try {\n cursor = decodeCursor(String(afterVal));\n } catch (e) {\n if (e instanceof CursorError) throw invalidParam(e.message, e.code);\n throw e;\n }\n try {\n const reconciled = reconcileCursorOrder(cursor, orderByEntriesToTuples(options.orderBy));\n options.orderBy = reconciled.map(([field, direction, nulls]) =>\n (nulls ? { field, direction, nulls } : { field, direction }));\n } catch (e) {\n if (e instanceof CursorMismatchError) throw invalidParam(e.message, e.code);\n throw e;\n }\n options.cursor = cursor;\n // A cursor and an offset describe the same window two incompatible\n // ways, and honouring both would start the page `offset` rows past\n // where the cursor pointed — a gap the caller cannot see.\n if (options.offset !== undefined) {\n throw invalidParam(\n \"`after` and `offset`/`page` cannot be combined: a cursor already says where the page \"\n + \"starts, and an offset on top of it skips rows. Use one or the other.\",\n \"CURSOR_WITH_OFFSET\"\n );\n }\n }\n\n // ── Vector similarity search ───────────────────────────────────────\n // Every rejection here is a malformed *request*, so it must carry a 400.\n // A bare `Error` reaches the handler with no `statusCode` and no known\n // `code`, which makes it a 500 — logged with a full stack as an incident,\n // and answered with \"An unexpected error occurred\", because the handler\n // only forwards a message to the client below 500. The caller was told\n // nothing about what it got wrong.\n const vectorSearchVal = getLastValue(query.vector_search);\n const vectorVal = getLastValue(query.vector);\n if (vectorSearchVal && vectorVal) {\n const vectorStr = String(vectorVal);\n let decoded: unknown;\n try {\n decoded = JSON.parse(vectorStr);\n } catch {\n decoded = undefined;\n }\n // Validated outside the `try` on purpose: inside it, the thrown\n // ApiError would be caught by its own `catch` and re-thrown as\n // something else.\n if (!Array.isArray(decoded) || !decoded.every(v => typeof v === \"number\")) {\n throw invalidParam(\n \"Invalid `vector` format. Expected a JSON array of numbers, e.g. [0.1,0.2,0.3]\",\n \"INVALID_VECTOR\"\n );\n }\n const queryVector = decoded as number[];\n\n const distanceParamVal = getLastValue(query.vector_distance);\n const distanceParam = distanceParamVal ? String(distanceParamVal) : \"cosine\";\n if (distanceParam !== \"cosine\" && distanceParam !== \"l2\" && distanceParam !== \"inner_product\") {\n throw invalidParam(\n `Invalid \\`vector_distance\\`: ${distanceParam}. Expected: cosine, l2, or inner_product`,\n \"INVALID_VECTOR_DISTANCE\"\n );\n }\n\n const vectorSearch: VectorSearchParams = {\n property: String(vectorSearchVal),\n vector: queryVector,\n distance: distanceParam\n };\n\n const thresholdVal = getLastValue(query.vector_threshold);\n if (thresholdVal) {\n const threshold = parseFloat(String(thresholdVal));\n if (isNaN(threshold)) {\n throw invalidParam(\n \"Invalid `vector_threshold`. Expected a number.\",\n \"INVALID_VECTOR_THRESHOLD\"\n );\n }\n vectorSearch.threshold = threshold;\n }\n\n options.vectorSearch = vectorSearch;\n }\n\n // Resolve the limit LAST — once we know whether this is a vector search —\n // so a client-supplied limit above the ceiling is refused with a 400 and an\n // absent one falls back to the correct mode default (plain/text =\n // defaultLimit, vector = 10). Without this a bare `GET /<collection>` would\n // return the whole table. Shared with the WebSocket ingress via\n // `resolveClientListLimit`.\n options.limit = resolveListLimitParam(rawLimit, {\n vectorSearch: !!options.vectorSearch,\n defaultLimit: limits.defaultLimit,\n maxLimit: limits.maxLimit\n });\n\n // Every field the request named, against what this caller may read. Last,\n // so a malformed parameter is still answered as malformed rather than as a\n // permission problem.\n if (access) assertQueryFieldsReadable(options, access.collection, access.viewer);\n\n return options;\n}\n"],"mappings":"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"}
|
|
@@ -3,8 +3,8 @@ import __rebaseProcess from "process";
|
|
|
3
3
|
globalThis.process ??= __rebaseProcess;
|
|
4
4
|
__rebaseCreateRequire(import.meta.url);
|
|
5
5
|
import { n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
|
|
6
|
-
import { r as logger } from "./logger-
|
|
7
|
-
import { r as errorHandler, t as ApiError } from "./errors-
|
|
6
|
+
import { r as logger } from "./logger-D-S-hO5e.js";
|
|
7
|
+
import { r as errorHandler, t as ApiError } from "./errors-D6_y86c5.js";
|
|
8
8
|
//#region src/functions/request-timeout.ts
|
|
9
9
|
var request_timeout_exports = /* @__PURE__ */ __exportAll({
|
|
10
10
|
DEFAULT_FUNCTIONS_TIMEOUT_MS: () => DEFAULT_FUNCTIONS_TIMEOUT_MS,
|
|
@@ -78,4 +78,4 @@ function createFunctionsRequestTimeout(ms) {
|
|
|
78
78
|
//#endregion
|
|
79
79
|
export { request_timeout_exports as t };
|
|
80
80
|
|
|
81
|
-
//# sourceMappingURL=request-timeout-
|
|
81
|
+
//# sourceMappingURL=request-timeout-DgH7j8qO.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request-timeout-
|
|
1
|
+
{"version":3,"file":"request-timeout-DgH7j8qO.js","names":[],"sources":["../src/functions/request-timeout.ts"],"sourcesContent":["import type { MiddlewareHandler } from \"hono\";\nimport type { HonoEnv } from \"../api/types\";\nimport { logger } from \"../utils/logger\";\nimport { ApiError, errorHandler } from \"../api/errors\";\n\n/** Default ceiling for a custom function request, in milliseconds. */\nexport const DEFAULT_FUNCTIONS_TIMEOUT_MS = 30_000;\n\n/**\n * Resolve the functions request timeout from config, then env, then the default.\n *\n * `0` (or any non-positive number) disables the ceiling — for a deployment\n * whose proxy already imposes one, or a function that legitimately streams for\n * minutes.\n */\nexport function resolveFunctionsTimeoutMs(configured?: number): number {\n if (typeof configured === \"number\" && Number.isFinite(configured)) {\n return Math.max(0, Math.floor(configured));\n }\n // Blank means unset, not zero. `Number(\"\")` and `Number(\" \")` are both 0,\n // and 0 here means \"no ceiling\" — so a compose file with\n // `REBASE_FUNCTIONS_TIMEOUT_MS=${SOMETHING}` and `SOMETHING` undefined, or\n // a `.env` line with the name and no value, silently switched off the one\n // bound on how long code the framework did not write may hold a socket.\n // Declaring a variable without setting it is the ordinary way to write\n // both of those files, and the failure is invisible: nothing logs, and the\n // deployment behaves exactly as it did before the ceiling existed.\n const raw = process.env.REBASE_FUNCTIONS_TIMEOUT_MS?.trim();\n if (raw) {\n const fromEnv = Number(raw);\n if (Number.isFinite(fromEnv) && fromEnv >= 0) {\n return Math.floor(fromEnv);\n }\n }\n return DEFAULT_FUNCTIONS_TIMEOUT_MS;\n}\n\n/**\n * A per-request ceiling for the custom functions router.\n *\n * Custom functions are the one router that runs code the framework did not\n * write, and nothing else in the stack bounds how long that code takes: the\n * Node server is constructed without `requestTimeout`/`headersTimeout`, so a\n * handler awaiting a promise that never settles — a `fetch` to an unreachable\n * third party with no `AbortSignal`, a query on a wedged connection — holds its\n * socket and its request object until the client gives up. On the managed\n * runtime the process is shared between tenants, so that is not only the slow\n * caller's problem.\n *\n * The handler is **not** cancelled — it cannot be, there is no cancellation\n * token to hand user code. What is bounded is the client-visible request: after\n * `ms` the caller gets a 504 and the socket is released, and the handler's\n * eventual result is dropped. It is a ceiling, not a kill switch, and the exact\n * number matters far less than its existence.\n *\n * \"The handler keeps running\" is a **Node** guarantee, not a property of the\n * contract. It follows from the process outliving the request, which is not\n * true on an isolate-based host: there, work still in flight when the response\n * resolves is terminated rather than orphaned. So a handler must not depend on\n * finishing after its 504 — anything that has to complete belongs in\n * `waitUntil()`, which is the one construct both hosts honour. See\n * `./wait-until.ts`.\n *\n * Mounted in front of the auth middleware rather than behind it, so a wedged\n * driver — the failure that also hangs `withAuth()` — is covered too.\n */\nexport function createFunctionsRequestTimeout(ms: number): MiddlewareHandler<HonoEnv> {\n return async (c, next) => {\n if (ms <= 0) return next();\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timedOut = new Promise<\"timeout\">((resolve) => {\n timer = setTimeout(() => resolve(\"timeout\"), ms);\n });\n\n try {\n const outcome = await Promise.race([next().then(() => \"done\" as const), timedOut]);\n if (outcome === \"timeout\") {\n logger.warn(\n `[functions] ${c.req.method} ${c.req.path} exceeded the ${ms}ms request timeout — ` +\n \"answering 504. The handler is still running; it cannot be cancelled from here. \" +\n \"Give outbound calls an AbortSignal, or raise `functionsTimeoutMs` / REBASE_FUNCTIONS_TIMEOUT_MS.\"\n );\n // Through `errorHandler`, not `c.json`: that is what puts the\n // `requestId` in the body and hands the outcome to the request\n // log, so the Studio Logs entry for a timeout carries\n // `errorCode` like every other failure. Hand-built, it was the\n // one 504 nobody could join to a log line.\n return errorHandler(\n new ApiError(504, \"FUNCTION_TIMEOUT\", \"Function timed out\"),\n c\n ) as Response;\n }\n } finally {\n if (timer) clearTimeout(timer);\n }\n return undefined;\n };\n}\n"],"mappings":";;;;;;;;;;;;;;AAMA,IAAa,+BAA+B;;;;;;;;AAS5C,SAAgB,0BAA0B,YAA6B;CACnE,IAAI,OAAO,eAAe,YAAY,OAAO,SAAS,UAAU,GAC5D,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,UAAU,CAAC;CAU7C,MAAM,MAAM,QAAQ,IAAI,6BAA6B,KAAK;CAC1D,IAAI,KAAK;EACL,MAAM,UAAU,OAAO,GAAG;EAC1B,IAAI,OAAO,SAAS,OAAO,KAAK,WAAW,GACvC,OAAO,KAAK,MAAM,OAAO;CAEjC;CACA,OAAO;AACX;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,SAAgB,8BAA8B,IAAwC;CAClF,OAAO,OAAO,GAAG,SAAS;EACtB,IAAI,MAAM,GAAG,OAAO,KAAK;EAEzB,IAAI;EACJ,MAAM,WAAW,IAAI,SAAoB,YAAY;GACjD,QAAQ,iBAAiB,QAAQ,SAAS,GAAG,EAAE;EACnD,CAAC;EAED,IAAI;GAEA,IAAI,MADkB,QAAQ,KAAK,CAAC,KAAK,CAAC,CAAC,WAAW,MAAe,GAAG,QAAQ,CAAC,MACjE,WAAW;IACvB,OAAO,KACH,eAAe,EAAE,IAAI,OAAO,GAAG,EAAE,IAAI,KAAK,gBAAgB,GAAG,uMAGjE;IAMA,OAAO,aACH,IAAI,SAAS,KAAK,oBAAoB,oBAAoB,GAC1D,CACJ;GACJ;EACJ,UAAU;GACN,IAAI,OAAO,aAAa,KAAK;EACjC;CAEJ;AACJ"}
|
|
@@ -54,13 +54,58 @@ export interface SchemaEditRepository {
|
|
|
54
54
|
* first. A missing file is not an error: a new collection has no source yet,
|
|
55
55
|
* and the editor creates one.
|
|
56
56
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
57
|
+
* Also what {@link restore} is snapshotted from. The local working tree
|
|
58
|
+
* implements it for that reason alone: the editor reads its files from
|
|
59
|
+
* disk directly.
|
|
59
60
|
*/
|
|
60
61
|
readFile?(path: string): Promise<string | undefined>;
|
|
62
|
+
/**
|
|
63
|
+
* Put these files back exactly as they were — `undefined` contents meaning
|
|
64
|
+
* the file did not exist — and unstage them.
|
|
65
|
+
*
|
|
66
|
+
* Called when the commit is refused after the change was written: a
|
|
67
|
+
* pre-commit hook, a missing git identity, a held `index.lock`. Without it
|
|
68
|
+
* the refusal left the source and the generated schema rewritten and
|
|
69
|
+
* staged — the panel reported failure while the source had changed — and
|
|
70
|
+
* every retry met those leftovers as somebody else's work in progress.
|
|
71
|
+
*
|
|
72
|
+
* Optional: a repository that writes nothing locally (the GitHub one
|
|
73
|
+
* stages in memory) has nothing to put back. Needs {@link readFile} to take
|
|
74
|
+
* the snapshot it restores.
|
|
75
|
+
*/
|
|
76
|
+
restore?(files: {
|
|
77
|
+
path: string;
|
|
78
|
+
contents: string | undefined;
|
|
79
|
+
}[]): Promise<void>;
|
|
61
80
|
}
|
|
62
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* The commit was refused after the change had been written, and the tree was
|
|
83
|
+
* put back. Nothing changed: not the source, not the database.
|
|
84
|
+
*/
|
|
85
|
+
export declare class CommitRefusedError extends Error {
|
|
86
|
+
readonly restored: boolean;
|
|
87
|
+
constructor(detail: string, restored: boolean);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Runs the DDL. Separate from the repository so neither knows about the other.
|
|
91
|
+
*
|
|
92
|
+
* Throws {@link StatementFailedError} when it can say how far it got.
|
|
93
|
+
*/
|
|
63
94
|
export type SchemaEditApply = (statements: string[]) => Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Statement `appliedCount + 1` failed, after `appliedCount` statements had run.
|
|
97
|
+
*
|
|
98
|
+
* The statements run one at a time with no transaction around them —
|
|
99
|
+
* `CREATE INDEX CONCURRENTLY` may not run inside one — so the ones before the
|
|
100
|
+
* failure have already changed the database. A receipt that said "the database
|
|
101
|
+
* was not changed" after an `ADD COLUMN` had landed and the `FOREIGN KEY` that
|
|
102
|
+
* followed it had timed out described the state before the change.
|
|
103
|
+
*/
|
|
104
|
+
export declare class StatementFailedError extends Error {
|
|
105
|
+
readonly appliedCount: number;
|
|
106
|
+
readonly statement: string;
|
|
107
|
+
constructor(appliedCount: number, statement: string, cause: unknown);
|
|
108
|
+
}
|
|
64
109
|
export interface SchemaEditInput {
|
|
65
110
|
/**
|
|
66
111
|
* What to write and run, from `admin.planSchemaChange`.
|
|
@@ -100,6 +145,13 @@ export interface SchemaEditInput {
|
|
|
100
145
|
* can derive them (`<collectionsDir>/<id>.ts`) without writing anything.
|
|
101
146
|
*/
|
|
102
147
|
sourcePaths?: string[];
|
|
148
|
+
/**
|
|
149
|
+
* "Edit source only": commit the source and the generated schema for a
|
|
150
|
+
* change the database cannot take, and run nothing. The plan is committed
|
|
151
|
+
* whatever its verdict; the caller has checked that every refused change
|
|
152
|
+
* says what it leaves behind.
|
|
153
|
+
*/
|
|
154
|
+
sourceOnly?: boolean;
|
|
103
155
|
}
|
|
104
156
|
export interface SchemaEditResult {
|
|
105
157
|
committed: {
|
|
@@ -109,8 +161,16 @@ export interface SchemaEditResult {
|
|
|
109
161
|
};
|
|
110
162
|
/** True when the DDL ran. False means committed and pending a boot. */
|
|
111
163
|
applied: boolean;
|
|
164
|
+
/** Committed as "Edit source only": nothing was meant to run. */
|
|
165
|
+
sourceOnly?: boolean;
|
|
112
166
|
/** Why the apply did not run, when it did not. Never a reason to fail. */
|
|
113
167
|
applyError?: string;
|
|
168
|
+
/**
|
|
169
|
+
* How many statements ran before the one that failed, when the applier
|
|
170
|
+
* could say. Equal to `statements.length` when everything ran; `undefined`
|
|
171
|
+
* when the apply failed without saying how far it got.
|
|
172
|
+
*/
|
|
173
|
+
appliedStatements?: number;
|
|
114
174
|
statements: string[];
|
|
115
175
|
classified: ClassifiedSchemaChanges;
|
|
116
176
|
/** What to tell the person who pressed the button. */
|
|
@@ -34,8 +34,9 @@ export declare function commitPathsFor(collectionsDir: string, repositoryRoot: s
|
|
|
34
34
|
* migration is Atlas's format with an integrity file, minted by an external
|
|
35
35
|
* binary against a throwaway database. A server process has neither.
|
|
36
36
|
*
|
|
37
|
-
* What it *does* write is
|
|
38
|
-
* `rebase db generate`
|
|
37
|
+
* What it *does* write is the collection source, which is exactly what
|
|
38
|
+
* `rebase db generate` renders its desired state from — so the migration is
|
|
39
|
+
* one command away.
|
|
39
40
|
* The hazard is nobody saying so. A project that deploys by replaying
|
|
40
41
|
* migrations would build its next environment without this change, having been
|
|
41
42
|
* told the change was applied.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SchemaChangeFile } from "@rebasepro/types";
|
|
2
2
|
import type { SchemaEditRepository } from "./apply-schema-change.js";
|
|
3
|
-
export interface RemoteSourceOptions {
|
|
3
|
+
export interface RemoteSourceOptions<Edit = Record<string, unknown>> {
|
|
4
4
|
repository: SchemaEditRepository;
|
|
5
5
|
/**
|
|
6
6
|
* Where the collection *source* lives in the repository, as a repo-relative
|
|
@@ -11,8 +11,13 @@ export interface RemoteSourceOptions {
|
|
|
11
11
|
* output, which is a different directory holding different files.
|
|
12
12
|
*/
|
|
13
13
|
collectionsPath: string;
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Applies the edit. Injected so this module needs no `ts-morph`.
|
|
16
|
+
*
|
|
17
|
+
* `edit` is whatever the caller's editor takes — a whole collection, or the
|
|
18
|
+
* patch of what changed about an existing one — passed through untouched.
|
|
19
|
+
*/
|
|
20
|
+
edit: (collectionsDir: string, collectionId: string, edit: Edit) => Promise<void>;
|
|
16
21
|
}
|
|
17
22
|
/**
|
|
18
23
|
* Fetch, rewrite, and return the changed file — without leaving anything behind.
|
|
@@ -22,4 +27,4 @@ export interface RemoteSourceOptions {
|
|
|
22
27
|
* accumulates those on a long-running server is a slow leak of exactly the
|
|
23
28
|
* thing worth not leaking.
|
|
24
29
|
*/
|
|
25
|
-
export declare function rewriteRemoteCollection(options: RemoteSourceOptions
|
|
30
|
+
export declare function rewriteRemoteCollection<Edit>(options: RemoteSourceOptions<Edit>, collectionId: string, change: Edit): Promise<SchemaChangeFile[]>;
|
|
@@ -2,8 +2,10 @@ 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 {
|
|
5
|
+
import "./src-CatHFUym.js";
|
|
6
|
+
import { i as isCollectionPatch, n as collectionPatchProblems } from "./collection_patch-BRu-BvDv.js";
|
|
7
|
+
import { r as errorHandler, t as ApiError } from "./errors-D6_y86c5.js";
|
|
8
|
+
import { AstSchemaEditor } from "./ast-schema-editor-CWqS_sLJ.js";
|
|
7
9
|
import { Hono } from "hono";
|
|
8
10
|
import { z } from "zod";
|
|
9
11
|
//#region src/api/schema-editor-routes.ts
|
|
@@ -37,9 +39,11 @@ var propertyDeleteSchema = z.object({
|
|
|
37
39
|
});
|
|
38
40
|
var collectionSaveSchema = z.object({
|
|
39
41
|
collectionId: collectionIdSchema,
|
|
40
|
-
collectionData: z.record(z.string(), z.unknown()),
|
|
42
|
+
collectionData: z.record(z.string(), z.unknown()).optional(),
|
|
43
|
+
/** What changed about an existing collection — see `collection_patch.ts`. */
|
|
44
|
+
patch: z.array(z.unknown()).optional(),
|
|
41
45
|
partial: z.boolean().optional()
|
|
42
|
-
});
|
|
46
|
+
}).refine((save) => save.collectionData === void 0 !== (save.patch === void 0), { message: "send `patch` (what changed) or `collectionData` (a new collection), not both" });
|
|
43
47
|
var collectionDeleteSchema = z.object({ collectionId: collectionIdSchema });
|
|
44
48
|
/** Parse a body against a schema, or 400 naming the field. */
|
|
45
49
|
async function body(c, schema) {
|
|
@@ -70,8 +74,9 @@ function createSchemaEditorRoutes(collectionsDir) {
|
|
|
70
74
|
* so an older panel keeps the behaviour it was written against.
|
|
71
75
|
*/
|
|
72
76
|
router.post("/collection/save", async (c) => {
|
|
73
|
-
const { collectionId, collectionData, partial } = await body(c, collectionSaveSchema);
|
|
74
|
-
|
|
77
|
+
const { collectionId, collectionData, patch, partial } = await body(c, collectionSaveSchema);
|
|
78
|
+
if (patch !== void 0 && !isCollectionPatch(patch)) throw ApiError.badRequest(collectionPatchProblems(patch).join(" "), "INVALID_INPUT");
|
|
79
|
+
await refusalsAsBadRequest(() => patch ? editor.applyPatch(collectionId, patch) : editor.saveCollection(collectionId, collectionData ?? {}, { partial: partial === true }));
|
|
75
80
|
return c.json({ success: true });
|
|
76
81
|
});
|
|
77
82
|
router.post("/collection/delete", async (c) => {
|
|
@@ -84,4 +89,4 @@ function createSchemaEditorRoutes(collectionsDir) {
|
|
|
84
89
|
//#endregion
|
|
85
90
|
export { createSchemaEditorRoutes };
|
|
86
91
|
|
|
87
|
-
//# sourceMappingURL=schema-editor-routes-
|
|
92
|
+
//# sourceMappingURL=schema-editor-routes-oIyuWl3L.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-editor-routes-oIyuWl3L.js","names":[],"sources":["../src/api/schema-editor-routes.ts"],"sourcesContent":["import { Hono } from \"hono\";\nimport { z } from \"zod\";\nimport { collectionPatchProblems, isCollectionPatch } from \"@rebasepro/types\";\nimport { AstSchemaEditor } from \"./ast-schema-editor\";\nimport { ApiError, errorHandler } from \"./errors\";\nimport { HonoEnv } from \"./types\";\n\n/**\n * Rewriting collection source from the admin panel.\n *\n * Every refusal in `AstSchemaEditor` is written for the person who will read it\n * — \"Collection X has no file at …\", \"Relation Y has no target collection. Pick\n * one before saving.\" — and every one of them was a plain `Error`, which\n * `errorHandler` treats as an unexpected failure: 500, and a body that says\n * \"Internal Server Error\". The messages never left the server. They are 400s\n * now, so the panel can show what it was told.\n */\nfunction refusalsAsBadRequest<T>(run: () => Promise<T>): Promise<T> {\n return run().catch((error: unknown) => {\n if (error instanceof ApiError) throw error;\n throw ApiError.badRequest(\n error instanceof Error ? error.message : String(error),\n \"SCHEMA_EDIT_REFUSED\"\n );\n });\n}\n\n/** The identifier half of every payload here, checked once. */\nconst collectionIdSchema = z.string().min(1, \"`collectionId` is required\");\nconst propertyKeySchema = z.string().min(1, \"`propertyKey` is required\");\n\nconst propertySaveSchema = z.object({\n collectionId: collectionIdSchema,\n propertyKey: propertyKeySchema,\n propertyConfig: z.record(z.string(), z.unknown())\n});\nconst propertyDeleteSchema = z.object({\n collectionId: collectionIdSchema,\n propertyKey: propertyKeySchema\n});\nconst collectionSaveSchema = z.object({\n collectionId: collectionIdSchema,\n collectionData: z.record(z.string(), z.unknown()).optional(),\n /** What changed about an existing collection — see `collection_patch.ts`. */\n patch: z.array(z.unknown()).optional(),\n partial: z.boolean().optional()\n}).refine(\n save => (save.collectionData === undefined) !== (save.patch === undefined),\n { message: \"send `patch` (what changed) or `collectionData` (a new collection), not both\" }\n);\nconst collectionDeleteSchema = z.object({\n collectionId: collectionIdSchema\n});\n\n/** Parse a body against a schema, or 400 naming the field. */\nasync function body<S extends z.ZodType>(c: { req: { json: () => Promise<unknown> } }, schema: S): Promise<z.infer<S>> {\n const raw = await c.req.json().catch(() => undefined);\n const parsed = schema.safeParse(raw);\n if (!parsed.success) {\n throw ApiError.badRequest(\n parsed.error.issues.map(i => `${i.path.join(\".\") || \"body\"}: ${i.message}`).join(\"; \"),\n \"INVALID_INPUT\"\n );\n }\n return parsed.data;\n}\n\nexport function createSchemaEditorRoutes(collectionsDir: string): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n router.onError(errorHandler);\n const editor = new AstSchemaEditor(collectionsDir);\n\n router.post(\"/property/save\", async (c) => {\n const { collectionId, propertyKey, propertyConfig } = await body(c, propertySaveSchema);\n await refusalsAsBadRequest(() => editor.saveProperty(collectionId, propertyKey, propertyConfig));\n return c.json({ success: true });\n });\n\n router.post(\"/property/delete\", async (c) => {\n const { collectionId, propertyKey } = await body(c, propertyDeleteSchema);\n await refusalsAsBadRequest(() => editor.deleteProperty(collectionId, propertyKey));\n return c.json({ success: true });\n });\n\n /**\n * `partial: true` means \"this payload is what changed\", not \"this is the\n * collection\". Without it a one-key patch — which is what adding a column\n * posts — is read as a whole-collection save and deletes everything it does\n * not mention, `securityRules` included. Absent, it defaults to a full save,\n * so an older panel keeps the behaviour it was written against.\n */\n router.post(\"/collection/save\", async (c) => {\n const { collectionId, collectionData, patch, partial } = await body(c, collectionSaveSchema);\n if (patch !== undefined && !isCollectionPatch(patch)) {\n throw ApiError.badRequest(collectionPatchProblems(patch).join(\" \"), \"INVALID_INPUT\");\n }\n await refusalsAsBadRequest(() => patch\n ? editor.applyPatch(collectionId, patch)\n : editor.saveCollection(collectionId, collectionData ?? {}, { partial: partial === true }));\n return c.json({ success: true });\n });\n\n router.post(\"/collection/delete\", async (c) => {\n const { collectionId } = await body(c, collectionDeleteSchema);\n await refusalsAsBadRequest(() => editor.deleteCollection(collectionId));\n return c.json({ success: true });\n });\n\n return router;\n}\n"],"mappings":"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"}
|
package/dist/serve-spa.d.ts
CHANGED
|
@@ -53,7 +53,65 @@ export interface ServeSPAConfig {
|
|
|
53
53
|
* generator emitted a real file per route.
|
|
54
54
|
*/
|
|
55
55
|
spa?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Whether this app answers a request under `basePath` at all (default:
|
|
58
|
+
* every one).
|
|
59
|
+
*
|
|
60
|
+
* For apps told apart by hostname as well as by path, which `excludePaths`
|
|
61
|
+
* cannot express: two apps can both be rooted at "/", one on
|
|
62
|
+
* `admin.example.com` and one on every other hostname, and no list of paths
|
|
63
|
+
* says which of them a request belongs to. The caller, which sees every
|
|
64
|
+
* app, decides; each mount asks. A request this returns `false` for passes
|
|
65
|
+
* through everything the mount registers — assets, caching, compression and
|
|
66
|
+
* the fallback alike — as if the mount were not there, because an app that
|
|
67
|
+
* declined the fallback but still served its files would answer the other
|
|
68
|
+
* app's hostname with its own `/assets/*`.
|
|
69
|
+
*/
|
|
70
|
+
owns?: (request: StaticAppRequest) => boolean;
|
|
71
|
+
}
|
|
72
|
+
/** What {@link ServeSPAConfig.owns} is asked about. */
|
|
73
|
+
export interface StaticAppRequest {
|
|
74
|
+
/**
|
|
75
|
+
* The `Host` the request named, exactly as sent: any case, and possibly
|
|
76
|
+
* with a port or a trailing dot. The request URL's host when the header is
|
|
77
|
+
* absent, as it is on a request built in-process.
|
|
78
|
+
*
|
|
79
|
+
* Never `X-Forwarded-Host`. The proxy in front of the runtime passes the
|
|
80
|
+
* original `Host` through, and a forwarded header is one any client can
|
|
81
|
+
* write — reading it would let a request for the site choose the admin.
|
|
82
|
+
*/
|
|
83
|
+
host: string;
|
|
84
|
+
/** The request path, as routed. */
|
|
85
|
+
path: string;
|
|
56
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Is `requestPath` the path `prefix`, or something beneath it?
|
|
89
|
+
*
|
|
90
|
+
* Segment-aware on purpose. A plain `startsWith` reads "/api" as excluding
|
|
91
|
+
* "/apidocs", and "/admin" as excluding "/administrators" — both ordinary
|
|
92
|
+
* client-side routes of the app rooted at "/", both then answered with a 404
|
|
93
|
+
* because the SPA fallback declined them and nothing else claims the path.
|
|
94
|
+
* `apiBasePath` is always in the exclusion list, so this reached single-app
|
|
95
|
+
* setups too, not just the multi-app ones the list was added for.
|
|
96
|
+
*
|
|
97
|
+
* Exported because deciding which app owns a request asks the same question of
|
|
98
|
+
* each app's own path, and two readings of "under" would disagree at exactly
|
|
99
|
+
* the boundary this exists for.
|
|
100
|
+
*/
|
|
101
|
+
export declare function isUnderPath(requestPath: string, prefix: string): boolean;
|
|
102
|
+
/**
|
|
103
|
+
* The paths of the apps nested inside the one at `basePath` — what its SPA
|
|
104
|
+
* fallback must leave to them, as `excludePaths`.
|
|
105
|
+
*
|
|
106
|
+
* Only those beneath it. Every other app's path used to be excluded, ancestors
|
|
107
|
+
* included, and an app at `/admin/beta` beside one at `/admin` then declined
|
|
108
|
+
* every deep link it had: each is under "/admin". The enclosing app declined
|
|
109
|
+
* them too, as it should, and a URL two apps were mounted for answered 404. An
|
|
110
|
+
* ancestor's path can never be right here — anything that reaches this app's
|
|
111
|
+
* fallback is under it — and an unrelated app's path can never match, so the
|
|
112
|
+
* apps beneath are the whole list.
|
|
113
|
+
*/
|
|
114
|
+
export declare function nestedAppPaths(basePath: string, otherPaths: readonly string[]): string[];
|
|
57
115
|
/**
|
|
58
116
|
* Serve a Single Page Application from an Hono app.
|
|
59
117
|
*
|
|
@@ -15,6 +15,17 @@ interface ClientMessage {
|
|
|
15
15
|
export interface WsRealtimeService extends RealtimeProvider {
|
|
16
16
|
addClient(clientId: string, ws: unknown): void;
|
|
17
17
|
handleClientMessage(clientId: string, message: ClientMessage, authContext?: unknown): Promise<void> | void;
|
|
18
|
+
/**
|
|
19
|
+
* Re-judge everything a socket holds open as the identity it signed in
|
|
20
|
+
* as — called on every `AUTHENTICATE`, a token refresh included.
|
|
21
|
+
*
|
|
22
|
+
* Required, because the socket calls it. It was added to the Postgres
|
|
23
|
+
* service and the socket and not here, the bootstrapper cast the composite
|
|
24
|
+
* to the service, and every sign-in on a multi-source project answered
|
|
25
|
+
* INTERNAL_ERROR. `routed-realtime-service-forwarding.test.ts` reads this
|
|
26
|
+
* interface and the socket's calls so it cannot happen twice.
|
|
27
|
+
*/
|
|
28
|
+
rescopeClient(clientId: string, authContext: unknown): Promise<void>;
|
|
18
29
|
/**
|
|
19
30
|
* Whether this provider actually implements channels, presence and
|
|
20
31
|
* broadcast. Declared by the provider rather than inferred, because the
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createRequire as __rebaseCreateRequire } from "module";
|
|
2
|
+
import __rebaseProcess from "process";
|
|
3
|
+
globalThis.process ??= __rebaseProcess;
|
|
4
|
+
__rebaseCreateRequire(import.meta.url);
|
|
5
|
+
import { t as ApiError } from "./errors-D6_y86c5.js";
|
|
6
|
+
//#region src/api/rest/soft-delete-params.ts
|
|
7
|
+
/**
|
|
8
|
+
* The two query parameters soft delete adds to the REST surface.
|
|
9
|
+
*
|
|
10
|
+
* Kept in their own module so the call sites in `query-parser.ts` and the
|
|
11
|
+
* delete routes are a single line each: the parsing rules belong to soft
|
|
12
|
+
* delete, not to the parser, and a rule spread across the two files that read
|
|
13
|
+
* it is a rule that drifts.
|
|
14
|
+
*/
|
|
15
|
+
/** `?deleted=` — what to do about rows a soft delete has stamped. */
|
|
16
|
+
var DELETED_QUERY_PARAM = "deleted";
|
|
17
|
+
/** `?hard=` — ask for a real `DELETE` on a soft-delete collection. */
|
|
18
|
+
var HARD_DELETE_QUERY_PARAM = "hard";
|
|
19
|
+
/**
|
|
20
|
+
* `?deleted=include|only` → the driver's `withDeleted`.
|
|
21
|
+
*
|
|
22
|
+
* Spelled `deleted` on the wire and `withDeleted` in the driver, deliberately:
|
|
23
|
+
* the URL reads as a question about the rows (`?deleted=only` — "only the
|
|
24
|
+
* deleted ones"), and the driver option reads as an instruction about the query.
|
|
25
|
+
*
|
|
26
|
+
* A value neither word is a 400 rather than a silent fallback to the default.
|
|
27
|
+
* `?deleted=true` quietly hiding every deleted row is the worst of both: it
|
|
28
|
+
* looks like it worked and answers the opposite question. Absent is the
|
|
29
|
+
* default, which is "hide them".
|
|
30
|
+
*/
|
|
31
|
+
function parseWithDeleted(raw) {
|
|
32
|
+
if (raw === void 0 || raw === null || raw === "") return void 0;
|
|
33
|
+
const value = String(raw).trim().toLowerCase();
|
|
34
|
+
if (value === "include") return true;
|
|
35
|
+
if (value === "only") return "only";
|
|
36
|
+
throw ApiError.badRequest(`Invalid \`?${DELETED_QUERY_PARAM}=${String(raw)}\`. It takes 'include' (live rows and deleted ones) or 'only' (deleted rows alone). Omit it to see only the live rows.`, "INVALID_DELETED_PARAM");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* `?hard=true` → a real `DELETE` on a collection that soft-deletes.
|
|
40
|
+
*
|
|
41
|
+
* Needs no permission beyond the delete it replaces: it is the same verb, and a
|
|
42
|
+
* second access-control surface for one operation is a second thing to get
|
|
43
|
+
* wrong. What it changes is whether the row can be restored.
|
|
44
|
+
*
|
|
45
|
+
* Only the exact words `true` and `1` mean yes. Anything else is a 400, not a
|
|
46
|
+
* "no" — a typo that silently soft-deletes when the caller asked to purge is a
|
|
47
|
+
* caller who believes the data is gone.
|
|
48
|
+
*/
|
|
49
|
+
function parseHardDelete(raw) {
|
|
50
|
+
if (raw === void 0 || raw === null || raw === "") return false;
|
|
51
|
+
const value = String(raw).trim().toLowerCase();
|
|
52
|
+
if (value === "true" || value === "1") return true;
|
|
53
|
+
if (value === "false" || value === "0") return false;
|
|
54
|
+
throw ApiError.badRequest(`Invalid \`?${HARD_DELETE_QUERY_PARAM}=${String(raw)}\`. It takes 'true' or 'false'.`, "INVALID_HARD_PARAM");
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
export { parseWithDeleted as i, HARD_DELETE_QUERY_PARAM as n, parseHardDelete as r, DELETED_QUERY_PARAM as t };
|
|
58
|
+
|
|
59
|
+
//# sourceMappingURL=soft-delete-params-BWPilMPF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"soft-delete-params-BWPilMPF.js","names":[],"sources":["../src/api/rest/soft-delete-params.ts"],"sourcesContent":["import { ApiError } from \"../errors\";\n\n/**\n * The two query parameters soft delete adds to the REST surface.\n *\n * Kept in their own module so the call sites in `query-parser.ts` and the\n * delete routes are a single line each: the parsing rules belong to soft\n * delete, not to the parser, and a rule spread across the two files that read\n * it is a rule that drifts.\n */\n\n/** `?deleted=` — what to do about rows a soft delete has stamped. */\nexport const DELETED_QUERY_PARAM = \"deleted\";\n/** `?hard=` — ask for a real `DELETE` on a soft-delete collection. */\nexport const HARD_DELETE_QUERY_PARAM = \"hard\";\n\n/**\n * `?deleted=include|only` → the driver's `withDeleted`.\n *\n * Spelled `deleted` on the wire and `withDeleted` in the driver, deliberately:\n * the URL reads as a question about the rows (`?deleted=only` — \"only the\n * deleted ones\"), and the driver option reads as an instruction about the query.\n *\n * A value neither word is a 400 rather than a silent fallback to the default.\n * `?deleted=true` quietly hiding every deleted row is the worst of both: it\n * looks like it worked and answers the opposite question. Absent is the\n * default, which is \"hide them\".\n */\nexport function parseWithDeleted(raw: unknown): boolean | \"only\" | undefined {\n if (raw === undefined || raw === null || raw === \"\") return undefined;\n const value = String(raw).trim().toLowerCase();\n if (value === \"include\") return true;\n if (value === \"only\") return \"only\";\n throw ApiError.badRequest(\n `Invalid \\`?${DELETED_QUERY_PARAM}=${String(raw)}\\`. It takes 'include' (live rows and deleted ones) ` +\n \"or 'only' (deleted rows alone). Omit it to see only the live rows.\",\n \"INVALID_DELETED_PARAM\"\n );\n}\n\n/**\n * `?hard=true` → a real `DELETE` on a collection that soft-deletes.\n *\n * Needs no permission beyond the delete it replaces: it is the same verb, and a\n * second access-control surface for one operation is a second thing to get\n * wrong. What it changes is whether the row can be restored.\n *\n * Only the exact words `true` and `1` mean yes. Anything else is a 400, not a\n * \"no\" — a typo that silently soft-deletes when the caller asked to purge is a\n * caller who believes the data is gone.\n */\nexport function parseHardDelete(raw: unknown): boolean {\n if (raw === undefined || raw === null || raw === \"\") return false;\n const value = String(raw).trim().toLowerCase();\n if (value === \"true\" || value === \"1\") return true;\n if (value === \"false\" || value === \"0\") return false;\n throw ApiError.badRequest(\n `Invalid \\`?${HARD_DELETE_QUERY_PARAM}=${String(raw)}\\`. It takes 'true' or 'false'.`,\n \"INVALID_HARD_PARAM\"\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;AAYA,IAAa,sBAAsB;;AAEnC,IAAa,0BAA0B;;;;;;;;;;;;;AAcvC,SAAgB,iBAAiB,KAA4C;CACzE,IAAI,QAAQ,KAAA,KAAa,QAAQ,QAAQ,QAAQ,IAAI,OAAO,KAAA;CAC5D,MAAM,QAAQ,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,YAAY;CAC7C,IAAI,UAAU,WAAW,OAAO;CAChC,IAAI,UAAU,QAAQ,OAAO;CAC7B,MAAM,SAAS,WACX,cAAc,oBAAoB,GAAG,OAAO,GAAG,EAAE,yHAEjD,uBACJ;AACJ;;;;;;;;;;;;AAaA,SAAgB,gBAAgB,KAAuB;CACnD,IAAI,QAAQ,KAAA,KAAa,QAAQ,QAAQ,QAAQ,IAAI,OAAO;CAC5D,MAAM,QAAQ,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,YAAY;CAC7C,IAAI,UAAU,UAAU,UAAU,KAAK,OAAO;CAC9C,IAAI,UAAU,WAAW,UAAU,KAAK,OAAO;CAC/C,MAAM,SAAS,WACX,cAAc,wBAAwB,GAAG,OAAO,GAAG,EAAE,kCACrD,oBACJ;AACJ"}
|