@rebasepro/server 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{admin_block-0Xu0r6eZ.js → admin_block-DxKLmdiv.js} +26 -8
- package/dist/{admin_block-0Xu0r6eZ.js.map → admin_block-DxKLmdiv.js.map} +1 -1
- package/dist/api/ast-schema-editor.d.ts +15 -0
- package/dist/api/rest/api-generator.d.ts +28 -0
- package/dist/{ast-schema-editor-C6mDz0XN.js → ast-schema-editor-CslO8Oje.js} +24 -4
- package/dist/ast-schema-editor-CslO8Oje.js.map +1 -0
- package/dist/auth/jwt.d.ts +67 -0
- package/dist/auth/rls-scope.d.ts +14 -0
- package/dist/{auth-DJsLXsCR.js → auth-gzoRjnuh.js} +24 -12
- package/dist/auth-gzoRjnuh.js.map +1 -0
- package/dist/boot/env.d.ts +8 -2
- package/dist/boot/fetch-bundle.d.ts +19 -0
- package/dist/boot/role.d.ts +2 -0
- package/dist/contract-routes-eLxV0le1.js.map +1 -1
- package/dist/{cron-routes-Bfwni8Zg.js → cron-routes-BamgO0Xr.js} +2 -2
- package/dist/{cron-routes-Bfwni8Zg.js.map → cron-routes-BamgO0Xr.js.map} +1 -1
- package/dist/{cron-store-Bsiw4Q6u.js → cron-store-yXOyIEmn.js} +2 -3
- package/dist/{cron-store-Bsiw4Q6u.js.map → cron-store-yXOyIEmn.js.map} +1 -1
- package/dist/db/ambient-transaction.d.ts +48 -0
- package/dist/ddl-bootstrap-CfNvxMuK.js.map +1 -1
- package/dist/deploy/pod-contract.d.ts +1 -1
- package/dist/errors-DMImyqyR.js.map +1 -1
- package/dist/functions/index.js.map +1 -1
- package/dist/{history-recorder-r5_IzSHK.js → history-recorder-hBGuXcZc.js} +2 -2
- package/dist/{history-recorder-r5_IzSHK.js.map → history-recorder-hBGuXcZc.js.map} +1 -1
- package/dist/{history-store-D4RVK-uZ.js → history-store-C9EtGtoL.js} +5 -6
- package/dist/history-store-C9EtGtoL.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.es.js +2266 -93
- package/dist/index.es.js.map +1 -1
- package/dist/init/callback-client.d.ts +16 -0
- package/dist/init/surfaces.d.ts +37 -7
- package/dist/jobs/types.d.ts +5 -0
- package/dist/{jobs-CW5lm_Ix.js → jobs-DZ8DLLcU.js} +82 -13
- package/dist/jobs-DZ8DLLcU.js.map +1 -0
- package/dist/{jwt-DATvkKB_.js → jwt-C4OW-DNq.js} +136 -4
- package/dist/jwt-C4OW-DNq.js.map +1 -0
- package/dist/logger-DO2PZc4i.js.map +1 -1
- package/dist/mcp/consent-page.d.ts +75 -0
- package/dist/mcp/mcp-routes.d.ts +52 -0
- package/dist/mcp/mcp-tools.d.ts +51 -0
- package/dist/mcp/oauth-metadata.d.ts +123 -0
- package/dist/mcp/oauth-routes.d.ts +52 -0
- package/dist/mcp/oauth-store.d.ts +157 -0
- package/dist/metrics/index.d.ts +16 -1
- package/dist/{openapi-generator-DGyLbISS.js → openapi-generator-CAFKF36E.js} +2 -8
- package/dist/openapi-generator-CAFKF36E.js.map +1 -0
- package/dist/{query-parser-BQiPZrM-.js → query-parser-uqzkZ82i.js} +2 -2
- package/dist/{query-parser-BQiPZrM-.js.map → query-parser-uqzkZ82i.js.map} +1 -1
- package/dist/{schema-editor-routes-C3TLZqAC.js → schema-editor-routes-BKOmdf4M.js} +2 -2
- package/dist/{schema-editor-routes-C3TLZqAC.js.map → schema-editor-routes-BKOmdf4M.js.map} +1 -1
- package/dist/services/webhook-service.d.ts +11 -8
- package/dist/sql-rows-C6GEc2oE.js +45 -0
- package/dist/sql-rows-C6GEc2oE.js.map +1 -0
- package/dist/src-Br6ARbs6.js.map +1 -1
- package/dist/{src-DqZ9YiGA.js → src-DW1cQ_28.js} +261 -17
- package/dist/src-DW1cQ_28.js.map +1 -0
- package/dist/utils/portable-crypto.d.ts +8 -0
- package/package.json +5 -5
- package/dist/ast-schema-editor-C6mDz0XN.js.map +0 -1
- package/dist/auth-DJsLXsCR.js.map +0 -1
- package/dist/history-store-D4RVK-uZ.js.map +0 -1
- package/dist/internal-tables-DYVcFFSv.js +0 -117
- package/dist/internal-tables-DYVcFFSv.js.map +0 -1
- package/dist/jobs-CW5lm_Ix.js.map +0 -1
- package/dist/jwt-DATvkKB_.js.map +0 -1
- package/dist/openapi-generator-DGyLbISS.js.map +0 -1
- package/dist/src-DqZ9YiGA.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query-parser-BQiPZrM-.js","names":[],"sources":["../src/api/rest/soft-delete-params.ts","../src/api/rest/field-access-query.ts","../src/api/rest/query-parser.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","import type { CollectionConfig } from \"@rebasepro/types\";\nimport type { FilterCondition, LogicalCondition } from \"@rebasepro/types\";\nimport { type FieldViewer, 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\";\n\nconst WHERE_LABEL: Record<Where, string> = {\n filter: \"a filter\",\n orderBy: \"`orderBy`\",\n fields: \"`fields`\",\n select: \"`select`\",\n groupBy: \"`groupBy`\"\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 },\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","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 UnknownFilterOperatorError\n} from \"@rebasepro/common\";\nimport { QueryOptions } from \"../types\";\nimport { ApiError } from \"../errors\";\nimport { DELETED_QUERY_PARAM, HARD_DELETE_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 const reservedQueryKeys = [\"limit\", \"offset\", \"page\", \"after\", \"orderBy\", \"include\", \"fields\", \"distinct\", \"searchString\", \"searchExplain\", \"vector_search\", \"vector\", \"vector_distance\", \"vector_threshold\", \"or\", \"and\", \"not\", \"where\", \"select\", \"groupBy\", DELETED_QUERY_PARAM, HARD_DELETE_QUERY_PARAM];\n const filterDict: Record<string, unknown> = {};\n for (const [key, rawValue] of Object.entries(query)) {\n if (reservedQueryKeys.includes(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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"}
|
|
1
|
+
{"version":3,"file":"query-parser-uqzkZ82i.js","names":[],"sources":["../src/api/rest/soft-delete-params.ts","../src/api/rest/field-access-query.ts","../src/api/rest/query-parser.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","import type { CollectionConfig } from \"@rebasepro/types\";\nimport type { FilterCondition, LogicalCondition } from \"@rebasepro/types\";\nimport { type FieldViewer, 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\";\n\nconst WHERE_LABEL: Record<Where, string> = {\n filter: \"a filter\",\n orderBy: \"`orderBy`\",\n fields: \"`fields`\",\n select: \"`select`\",\n groupBy: \"`groupBy`\"\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 },\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","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 UnknownFilterOperatorError\n} from \"@rebasepro/common\";\nimport { QueryOptions } from \"../types\";\nimport { ApiError } from \"../errors\";\nimport { DELETED_QUERY_PARAM, HARD_DELETE_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 const reservedQueryKeys = [\"limit\", \"offset\", \"page\", \"after\", \"orderBy\", \"include\", \"fields\", \"distinct\", \"searchString\", \"searchExplain\", \"vector_search\", \"vector\", \"vector_distance\", \"vector_threshold\", \"or\", \"and\", \"not\", \"where\", \"select\", \"groupBy\", DELETED_QUERY_PARAM, HARD_DELETE_QUERY_PARAM];\n const filterDict: Record<string, unknown> = {};\n for (const [key, rawValue] of Object.entries(query)) {\n if (reservedQueryKeys.includes(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,7 +3,7 @@ import __rebaseProcess from "process";
|
|
|
3
3
|
globalThis.process ??= __rebaseProcess;
|
|
4
4
|
__rebaseCreateRequire(import.meta.url);
|
|
5
5
|
import { n as errorHandler, t as ApiError } from "./errors-DMImyqyR.js";
|
|
6
|
-
import { AstSchemaEditor } from "./ast-schema-editor-
|
|
6
|
+
import { AstSchemaEditor } from "./ast-schema-editor-CslO8Oje.js";
|
|
7
7
|
import { Hono } from "hono";
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
//#region src/api/schema-editor-routes.ts
|
|
@@ -84,4 +84,4 @@ function createSchemaEditorRoutes(collectionsDir) {
|
|
|
84
84
|
//#endregion
|
|
85
85
|
export { createSchemaEditorRoutes };
|
|
86
86
|
|
|
87
|
-
//# sourceMappingURL=schema-editor-routes-
|
|
87
|
+
//# sourceMappingURL=schema-editor-routes-BKOmdf4M.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-editor-routes-
|
|
1
|
+
{"version":3,"file":"schema-editor-routes-BKOmdf4M.js","names":[],"sources":["../src/api/schema-editor-routes.ts"],"sourcesContent":["import { Hono } from \"hono\";\nimport { z } from \"zod\";\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()),\n partial: z.boolean().optional()\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, partial } = await body(c, collectionSaveSchema);\n await refusalsAsBadRequest(() =>\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":";;;;;;;;;;;;;;;;;;;AAgBA,SAAS,qBAAwB,KAAmC;CAChE,OAAO,IAAI,CAAC,CAAC,OAAO,UAAmB;EACnC,IAAI,iBAAiB,UAAU,MAAM;EACrC,MAAM,SAAS,WACX,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GACrD,qBACJ;CACJ,CAAC;AACL;;AAGA,IAAM,qBAAqB,EAAE,OAAO,CAAC,CAAC,IAAI,GAAG,4BAA4B;AACzE,IAAM,oBAAoB,EAAE,OAAO,CAAC,CAAC,IAAI,GAAG,2BAA2B;AAEvE,IAAM,qBAAqB,EAAE,OAAO;CAChC,cAAc;CACd,aAAa;CACb,gBAAgB,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC;AACpD,CAAC;AACD,IAAM,uBAAuB,EAAE,OAAO;CAClC,cAAc;CACd,aAAa;AACjB,CAAC;AACD,IAAM,uBAAuB,EAAE,OAAO;CAClC,cAAc;CACd,gBAAgB,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC;CAChD,SAAS,EAAE,QAAQ,CAAC,CAAC,SAAS;AAClC,CAAC;AACD,IAAM,yBAAyB,EAAE,OAAO,EACpC,cAAc,mBAClB,CAAC;;AAGD,eAAe,KAA0B,GAA8C,QAAgC;CACnH,MAAM,MAAM,MAAM,EAAE,IAAI,KAAK,CAAC,CAAC,YAAY,KAAA,CAAS;CACpD,MAAM,SAAS,OAAO,UAAU,GAAG;CACnC,IAAI,CAAC,OAAO,SACR,MAAM,SAAS,WACX,OAAO,MAAM,OAAO,KAAI,MAAK,GAAG,EAAE,KAAK,KAAK,GAAG,KAAK,OAAO,IAAI,EAAE,SAAS,CAAC,CAAC,KAAK,IAAI,GACrF,eACJ;CAEJ,OAAO,OAAO;AAClB;AAEA,SAAgB,yBAAyB,gBAAuC;CAC5E,MAAM,SAAS,IAAI,KAAc;CACjC,OAAO,QAAQ,YAAY;CAC3B,MAAM,SAAS,IAAI,gBAAgB,cAAc;CAEjD,OAAO,KAAK,kBAAkB,OAAO,MAAM;EACvC,MAAM,EAAE,cAAc,aAAa,mBAAmB,MAAM,KAAK,GAAG,kBAAkB;EACtF,MAAM,2BAA2B,OAAO,aAAa,cAAc,aAAa,cAAc,CAAC;EAC/F,OAAO,EAAE,KAAK,EAAE,SAAS,KAAK,CAAC;CACnC,CAAC;CAED,OAAO,KAAK,oBAAoB,OAAO,MAAM;EACzC,MAAM,EAAE,cAAc,gBAAgB,MAAM,KAAK,GAAG,oBAAoB;EACxE,MAAM,2BAA2B,OAAO,eAAe,cAAc,WAAW,CAAC;EACjF,OAAO,EAAE,KAAK,EAAE,SAAS,KAAK,CAAC;CACnC,CAAC;;;;;;;;CASD,OAAO,KAAK,oBAAoB,OAAO,MAAM;EACzC,MAAM,EAAE,cAAc,gBAAgB,YAAY,MAAM,KAAK,GAAG,oBAAoB;EACpF,MAAM,2BACF,OAAO,eAAe,cAAc,gBAAgB,EAAE,SAAS,YAAY,KAAK,CAAC,CAAC;EACtF,OAAO,EAAE,KAAK,EAAE,SAAS,KAAK,CAAC;CACnC,CAAC;CAED,OAAO,KAAK,sBAAsB,OAAO,MAAM;EAC3C,MAAM,EAAE,iBAAiB,MAAM,KAAK,GAAG,sBAAsB;EAC7D,MAAM,2BAA2B,OAAO,iBAAiB,YAAY,CAAC;EACtE,OAAO,EAAE,KAAK,EAAE,SAAS,KAAK,CAAC;CACnC,CAAC;CAED,OAAO;AACX"}
|
|
@@ -94,17 +94,20 @@ export declare class WebhookDispatcher {
|
|
|
94
94
|
* for a third party. It also means a receiver that is down can no longer
|
|
95
95
|
* fail the customer's write.
|
|
96
96
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* the
|
|
100
|
-
* before
|
|
101
|
-
* and `onDelivery` to record failures.
|
|
97
|
+
* Called inside a write — from a callback — the deliveries are held until
|
|
98
|
+
* that write commits and dropped if it rolls back. They used to go out
|
|
99
|
+
* whatever happened to the row: a receiver could hear about a change a few
|
|
100
|
+
* milliseconds before it committed, or about one that never did.
|
|
102
101
|
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
102
|
+
* The cost, when no `jobQueue` is configured: the queue is in-process and
|
|
103
|
+
* in-memory. A crash or a deploy between the commit and the delivery drops
|
|
104
|
+
* the event. Use {@link flush} on shutdown, and `onDelivery` to record
|
|
105
|
+
* failures. With `jobQueue` set, the delivery becomes a row in
|
|
106
|
+
* `rebase.jobs` and survives the deploy and the pod being rescheduled — the
|
|
107
|
+
* only gap left is a crash between the commit and that insert.
|
|
106
108
|
*/
|
|
107
109
|
enqueueEntityChange(table: string, event: "INSERT" | "UPDATE" | "DELETE", id: string, entity: Record<string, unknown> | null, previousEntity?: Record<string, unknown> | null): void;
|
|
110
|
+
private dispatchDeliveries;
|
|
108
111
|
/**
|
|
109
112
|
* Deliver one queued job, as the worker runs it.
|
|
110
113
|
*
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createRequire as __rebaseCreateRequire } from "module";
|
|
2
|
+
import __rebaseProcess from "process";
|
|
3
|
+
globalThis.process ??= __rebaseProcess;
|
|
4
|
+
__rebaseCreateRequire(import.meta.url);
|
|
5
|
+
//#region ../common/src/util/sql-rows.ts
|
|
6
|
+
/**
|
|
7
|
+
* Read the rows out of whatever a SQL driver actually returned.
|
|
8
|
+
*
|
|
9
|
+
* Two shapes reach this repository's stores through the same call, and which
|
|
10
|
+
* one arrives is a property of the driver rather than of the query:
|
|
11
|
+
* node-postgres hands back a `{ rows }` envelope, and the other paths — a bare
|
|
12
|
+
* `executeSql`, Drizzle's `db.execute()` on some builds — hand back an array.
|
|
13
|
+
* `@rebasepro/server`'s `SqlExec` type declares the array, so the envelope is
|
|
14
|
+
* off-contract every time it turns up, and it still turns up.
|
|
15
|
+
*
|
|
16
|
+
* Reading one shape only is how a store silently sees nothing: the rate limiter
|
|
17
|
+
* that did it counted every caller as being on their first request — a limiter
|
|
18
|
+
* that never limits, with no error anywhere to say so.
|
|
19
|
+
*
|
|
20
|
+
* Written inline it came to `result as unknown as { rows?: T[] } | T[]`, five
|
|
21
|
+
* times across three packages. That is the problem restated as an assertion: a
|
|
22
|
+
* union the caller has to re-test at runtime anyway, with `unknown` in the
|
|
23
|
+
* middle only because neither half overlaps what the signature promised. The
|
|
24
|
+
* `Array.isArray` below is that same test done once, and it *narrows* — so
|
|
25
|
+
* these branches are checked rather than claimed.
|
|
26
|
+
*
|
|
27
|
+
* The element type is the one claim that stays a claim: these are rows from
|
|
28
|
+
* hand-written SQL, and nothing at this layer can check a column list.
|
|
29
|
+
*/
|
|
30
|
+
function sqlRows(result) {
|
|
31
|
+
if (Array.isArray(result)) return result;
|
|
32
|
+
if (result !== null && typeof result === "object") {
|
|
33
|
+
const rows = result.rows;
|
|
34
|
+
if (Array.isArray(rows)) return rows;
|
|
35
|
+
}
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
/** The first row of {@link sqlRows}, or `undefined` when there were none. */
|
|
39
|
+
function firstSqlRow(result) {
|
|
40
|
+
return sqlRows(result)[0];
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { sqlRows as n, firstSqlRow as t };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=sql-rows-C6GEc2oE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql-rows-C6GEc2oE.js","names":[],"sources":["../../common/src/util/sql-rows.ts"],"sourcesContent":["/**\n * Read the rows out of whatever a SQL driver actually returned.\n *\n * Two shapes reach this repository's stores through the same call, and which\n * one arrives is a property of the driver rather than of the query:\n * node-postgres hands back a `{ rows }` envelope, and the other paths — a bare\n * `executeSql`, Drizzle's `db.execute()` on some builds — hand back an array.\n * `@rebasepro/server`'s `SqlExec` type declares the array, so the envelope is\n * off-contract every time it turns up, and it still turns up.\n *\n * Reading one shape only is how a store silently sees nothing: the rate limiter\n * that did it counted every caller as being on their first request — a limiter\n * that never limits, with no error anywhere to say so.\n *\n * Written inline it came to `result as unknown as { rows?: T[] } | T[]`, five\n * times across three packages. That is the problem restated as an assertion: a\n * union the caller has to re-test at runtime anyway, with `unknown` in the\n * middle only because neither half overlaps what the signature promised. The\n * `Array.isArray` below is that same test done once, and it *narrows* — so\n * these branches are checked rather than claimed.\n *\n * The element type is the one claim that stays a claim: these are rows from\n * hand-written SQL, and nothing at this layer can check a column list.\n */\nexport function sqlRows<T = Record<string, unknown>>(result: unknown): T[] {\n if (Array.isArray(result)) return result as T[];\n if (result !== null && typeof result === \"object\") {\n const rows = (result as { rows?: unknown }).rows;\n if (Array.isArray(rows)) return rows as T[];\n }\n return [];\n}\n\n/** The first row of {@link sqlRows}, or `undefined` when there were none. */\nexport function firstSqlRow<T = Record<string, unknown>>(result: unknown): T | undefined {\n return sqlRows<T>(result)[0];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,QAAqC,QAAsB;CACvE,IAAI,MAAM,QAAQ,MAAM,GAAG,OAAO;CAClC,IAAI,WAAW,QAAQ,OAAO,WAAW,UAAU;EAC/C,MAAM,OAAQ,OAA8B;EAC5C,IAAI,MAAM,QAAQ,IAAI,GAAG,OAAO;CACpC;CACA,OAAO,CAAC;AACZ;;AAGA,SAAgB,YAAyC,QAAgC;CACrF,OAAO,QAAW,MAAM,CAAC,CAAC;AAC9B"}
|