@rebasepro/server 0.17.2-canary.g439a156 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/{ast-schema-editor-BLADP9O2.js → ast-schema-editor-CvkYOvri.js} +2 -2
  2. package/dist/{ast-schema-editor-BLADP9O2.js.map → ast-schema-editor-CvkYOvri.js.map} +1 -1
  3. package/dist/{auth-Nwxjng9S.js → auth-DkbzMZUS.js} +6 -6
  4. package/dist/{auth-Nwxjng9S.js.map → auth-DkbzMZUS.js.map} +1 -1
  5. package/dist/{contract-routes-D8TvxPLo.js → contract-routes-CHPVKTZW.js} +2 -2
  6. package/dist/{contract-routes-D8TvxPLo.js.map → contract-routes-CHPVKTZW.js.map} +1 -1
  7. package/dist/{cron-routes-BsukhMGm.js → cron-routes-DSC-kZXI.js} +2 -2
  8. package/dist/{cron-routes-BsukhMGm.js.map → cron-routes-DSC-kZXI.js.map} +1 -1
  9. package/dist/{cron-store-dof07MjG.js → cron-store-KeN8rN63.js} +3 -3
  10. package/dist/{cron-store-dof07MjG.js.map → cron-store-KeN8rN63.js.map} +1 -1
  11. package/dist/{history-recorder-BMSFyySP.js → history-recorder-B2pL3X8b.js} +2 -2
  12. package/dist/{history-recorder-BMSFyySP.js.map → history-recorder-B2pL3X8b.js.map} +1 -1
  13. package/dist/{history-store-Dtsip8nH.js → history-store-BUJ-37Sm.js} +2 -2
  14. package/dist/{history-store-Dtsip8nH.js.map → history-store-BUJ-37Sm.js.map} +1 -1
  15. package/dist/index.es.js +23 -21
  16. package/dist/index.es.js.map +1 -1
  17. package/dist/{jobs-BKG0mDlS.js → jobs-XXXcZrmj.js} +3 -3
  18. package/dist/{jobs-BKG0mDlS.js.map → jobs-XXXcZrmj.js.map} +1 -1
  19. package/dist/{jwt-5VN4C6ln.js → jwt-DNeOn39r.js} +2 -2
  20. package/dist/{jwt-5VN4C6ln.js.map → jwt-DNeOn39r.js.map} +1 -1
  21. package/dist/{openapi-generator-FFGT0H6O.js → openapi-generator-CpZp2Fai.js} +3 -3
  22. package/dist/{openapi-generator-FFGT0H6O.js.map → openapi-generator-CpZp2Fai.js.map} +1 -1
  23. package/dist/{query-parser--nstjRCh.js → query-parser-DqXNaBIH.js} +3 -3
  24. package/dist/{query-parser--nstjRCh.js.map → query-parser-DqXNaBIH.js.map} +1 -1
  25. package/dist/{schema-editor-routes-BWCIqZoT.js → schema-editor-routes-DNV_nBhW.js} +2 -2
  26. package/dist/{schema-editor-routes-BWCIqZoT.js.map → schema-editor-routes-DNV_nBhW.js.map} +1 -1
  27. package/dist/{src-B-CmIFMr.js → src-C9gKj23J.js} +2 -2
  28. package/dist/{src-B-CmIFMr.js.map → src-C9gKj23J.js.map} +1 -1
  29. package/dist/{src-Cdsw7DqV.js → src-CcOl05Oq.js} +6 -2
  30. package/dist/{src-Cdsw7DqV.js.map → src-CcOl05Oq.js.map} +1 -1
  31. package/package.json +5 -5
@@ -1 +1 @@
1
- {"version":3,"file":"query-parser--nstjRCh.js","names":[],"sources":["../src/api/rest/query-parser.ts"],"sourcesContent":["import type { FilterValues, ListLimitBounds, LogicalCondition, OrderByTuple, VectorSearchParams } from \"@rebasepro/types\";\nimport { toCanonicalOp, resolveClientListLimit, ListLimitError, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT } from \"@rebasepro/types\";\nimport { deserializeFilter, deserializeLogicalCondition, UnknownFilterOperatorError } from \"@rebasepro/common\";\nimport { QueryOptions } from \"../types\";\nimport { ApiError } from \"../errors\";\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\", 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\" };\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 }) => [field, direction] as OrderByTuple);\n}\n\nfunction invalidOrderBy(detail: string): never {\n throw invalidParam(\n `Invalid \\`orderBy\\` parameter: ${detail}. Expected \\`field\\`, \\`field:desc\\`, `\n + \"or a JSON array like [{\\\"field\\\":\\\"created_at\\\",\\\"direction\\\":\\\"desc\\\"}]\",\n \"INVALID_ORDER_BY\"\n );\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 return { field, direction: idx === -1 ? \"asc\" : toDirection(raw.slice(idx + 1), context) };\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 return { field: entry.field, direction: toDirection(entry.direction, context) };\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 * Parse query parameters into QueryOptions\n */\nexport function parseQueryOptions(\n query: Record<string, unknown>,\n limits: ListLimitOptions = {}\n): QueryOptions {\n const options: QueryOptions = {};\n const rawLimit = getLastValue(query.limit) as number | string | null | undefined;\n\n const offsetVal = getLastValue(query.offset);\n if (offsetVal) options.offset = parseInt(String(offsetVal));\n\n const pageVal = getLastValue(query.page);\n if (pageVal) {\n const page = parseInt(String(pageVal));\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) ──────────────────────────────────\n const orVal = getLastValue(query.or);\n const andVal = getLastValue(query.and);\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 }\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 const reservedQueryKeys = [\"limit\", \"offset\", \"page\", \"orderBy\", \"include\", \"fields\", \"searchString\", \"searchExplain\", \"vector_search\", \"vector\", \"vector_distance\", \"vector_threshold\", \"or\", \"and\", \"where\", \"select\", \"groupBy\"];\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 const includeVal = getLastValue(query.include);\n if (includeVal) {\n const includeStr = String(includeVal).trim();\n if (includeStr === \"*\") {\n options.include = [\"*\"];\n } else {\n options.include = includeStr.split(\",\").map(s => s.trim()).filter(Boolean);\n }\n }\n\n // Field selection\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 // ── 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 return options;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"query-parser-DqXNaBIH.js","names":[],"sources":["../src/api/rest/query-parser.ts"],"sourcesContent":["import type { FilterValues, ListLimitBounds, LogicalCondition, OrderByTuple, VectorSearchParams } from \"@rebasepro/types\";\nimport { toCanonicalOp, resolveClientListLimit, ListLimitError, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT } from \"@rebasepro/types\";\nimport { deserializeFilter, deserializeLogicalCondition, UnknownFilterOperatorError } from \"@rebasepro/common\";\nimport { QueryOptions } from \"../types\";\nimport { ApiError } from \"../errors\";\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\", 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\" };\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 }) => [field, direction] as OrderByTuple);\n}\n\nfunction invalidOrderBy(detail: string): never {\n throw invalidParam(\n `Invalid \\`orderBy\\` parameter: ${detail}. Expected \\`field\\`, \\`field:desc\\`, `\n + \"or a JSON array like [{\\\"field\\\":\\\"created_at\\\",\\\"direction\\\":\\\"desc\\\"}]\",\n \"INVALID_ORDER_BY\"\n );\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 return { field, direction: idx === -1 ? \"asc\" : toDirection(raw.slice(idx + 1), context) };\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 return { field: entry.field, direction: toDirection(entry.direction, context) };\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 * Parse query parameters into QueryOptions\n */\nexport function parseQueryOptions(\n query: Record<string, unknown>,\n limits: ListLimitOptions = {}\n): QueryOptions {\n const options: QueryOptions = {};\n const rawLimit = getLastValue(query.limit) as number | string | null | undefined;\n\n const offsetVal = getLastValue(query.offset);\n if (offsetVal) options.offset = parseInt(String(offsetVal));\n\n const pageVal = getLastValue(query.page);\n if (pageVal) {\n const page = parseInt(String(pageVal));\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) ──────────────────────────────────\n const orVal = getLastValue(query.or);\n const andVal = getLastValue(query.and);\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 }\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 const reservedQueryKeys = [\"limit\", \"offset\", \"page\", \"orderBy\", \"include\", \"fields\", \"searchString\", \"searchExplain\", \"vector_search\", \"vector\", \"vector_distance\", \"vector_threshold\", \"or\", \"and\", \"where\", \"select\", \"groupBy\"];\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 const includeVal = getLastValue(query.include);\n if (includeVal) {\n const includeStr = String(includeVal).trim();\n if (includeStr === \"*\") {\n options.include = [\"*\"];\n } else {\n options.include = includeStr.split(\",\").map(s => s.trim()).filter(Boolean);\n }\n }\n\n // Field selection\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 // ── 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 return options;\n}\n"],"mappings":"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"}
@@ -4,7 +4,7 @@ globalThis.process ??= __rebaseProcess;
4
4
  __rebaseCreateRequire(import.meta.url);
5
5
  import { n as errorHandler, t as ApiError } from "./errors-DBwpj9N8.js";
6
6
  import { a as record, i as object, o as string, r as boolean, s as unknown } from "./schemas-DBxgjM9A.js";
7
- import { AstSchemaEditor } from "./ast-schema-editor-BLADP9O2.js";
7
+ import { AstSchemaEditor } from "./ast-schema-editor-CvkYOvri.js";
8
8
  import { Hono } from "hono";
9
9
  //#region src/api/schema-editor-routes.ts
10
10
  /**
@@ -84,4 +84,4 @@ function createSchemaEditorRoutes(collectionsDir) {
84
84
  //#endregion
85
85
  export { createSchemaEditorRoutes };
86
86
 
87
- //# sourceMappingURL=schema-editor-routes-BWCIqZoT.js.map
87
+ //# sourceMappingURL=schema-editor-routes-DNV_nBhW.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema-editor-routes-BWCIqZoT.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,OAAS,CAAC,CAAC,IAAI,GAAG,4BAA4B;AACzE,IAAM,oBAAoB,OAAS,CAAC,CAAC,IAAI,GAAG,2BAA2B;AAEvE,IAAM,qBAAqB,OAAS;CAChC,cAAc;CACd,aAAa;CACb,gBAAgB,OAAS,OAAS,GAAG,QAAU,CAAC;AACpD,CAAC;AACD,IAAM,uBAAuB,OAAS;CAClC,cAAc;CACd,aAAa;AACjB,CAAC;AACD,IAAM,uBAAuB,OAAS;CAClC,cAAc;CACd,gBAAgB,OAAS,OAAS,GAAG,QAAU,CAAC;CAChD,SAAS,QAAU,CAAC,CAAC,SAAS;AAClC,CAAC;AACD,IAAM,yBAAyB,OAAS,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"}
1
+ {"version":3,"file":"schema-editor-routes-DNV_nBhW.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,OAAS,CAAC,CAAC,IAAI,GAAG,4BAA4B;AACzE,IAAM,oBAAoB,OAAS,CAAC,CAAC,IAAI,GAAG,2BAA2B;AAEvE,IAAM,qBAAqB,OAAS;CAChC,cAAc;CACd,aAAa;CACb,gBAAgB,OAAS,OAAS,GAAG,QAAU,CAAC;AACpD,CAAC;AACD,IAAM,uBAAuB,OAAS;CAClC,cAAc;CACd,aAAa;AACjB,CAAC;AACD,IAAM,uBAAuB,OAAS;CAClC,cAAc;CACd,gBAAgB,OAAS,OAAS,GAAG,QAAU,CAAC;CAChD,SAAS,QAAU,CAAC,CAAC,SAAS;AAClC,CAAC;AACD,IAAM,yBAAyB,OAAS,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"}
@@ -3,7 +3,7 @@ import __rebaseProcess from "process";
3
3
  globalThis.process ??= __rebaseProcess;
4
4
  __rebaseCreateRequire(import.meta.url);
5
5
  import { r as __require, t as __commonJSMin } from "./rolldown-runtime-dW7B1o5h.js";
6
- import { _ as REST_TO_CANONICAL, b as toCanonicalOp, d as isRelationalCollectionConfig, g as NULL_OPS, h as CANONICAL_TO_REST, l as getDeclaredSubcollections, m as ALL_WHERE_FILTER_OPS, p as getDataSourceCapabilities, u as isPostgresCollectionConfig, y as sortKeyToString } from "./src-Cdsw7DqV.js";
6
+ import { _ as REST_TO_CANONICAL, b as toCanonicalOp, d as isRelationalCollectionConfig, g as NULL_OPS, h as CANONICAL_TO_REST, l as getDeclaredSubcollections, m as ALL_WHERE_FILTER_OPS, p as getDataSourceCapabilities, u as isPostgresCollectionConfig, y as sortKeyToString } from "./src-CcOl05Oq.js";
7
7
  //#region ../types/src/errors.ts
8
8
  /**
9
9
  * The single error type thrown across the entire Rebase client surface —
@@ -4476,4 +4476,4 @@ function buildRoutedRebaseData({ defaultData, sources, resolveKey }) {
4476
4476
  //#endregion
4477
4477
  export { GeoPoint as A, ListLimitError as C, isAnonymousUid as D, ANONYMOUS_USER_ID as E, RebaseApiError as M, RebaseClientError as N, EntityReference as O, toSnakeCase as S, resolveClientListLimit as T, fieldKeyForColumn as _, deserializeLogicalCondition as a, buildCompositeId as b, collectAllPages as c, normalizeOrderBy as d, serializeOrderBy as f, getEffectiveSecurityRules as g, resolveDataSource as h, deserializeFilter as i, Vector as j, EntityRelation as k, paginateFind as l, createDataSourceRegistry as m, buildSdkData as n, serializeFilter as o, CollectionRegistry as p, UnknownFilterOperatorError as r, serializeLogicalCondition as s, buildRoutedRebaseData as t, resolveFindWindow as u, findRelation as v, MAX_LIST_LIMIT as w, resolvePrimaryKeys as x, resolveCollectionRelations as y };
4478
4478
 
4479
- //# sourceMappingURL=src-B-CmIFMr.js.map
4479
+ //# sourceMappingURL=src-C9gKj23J.js.map