@rebasepro/server 0.25.0 → 0.26.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/{GCSStorageController-C7ARrDAp.js → GCSStorageController-40FyFeSx.js} +8 -4
- package/dist/GCSStorageController-40FyFeSx.js.map +1 -0
- package/dist/{S3StorageController-qevJ0pVE.js → S3StorageController-Bl5shp94.js} +8 -4
- package/dist/S3StorageController-Bl5shp94.js.map +1 -0
- package/dist/access-BFAUvsSs.js +1897 -0
- package/dist/access-BFAUvsSs.js.map +1 -0
- package/dist/api/branch-routes.d.ts +25 -0
- package/dist/api/closed-database.d.ts +25 -0
- package/dist/api/live-schema-routes.d.ts +10 -0
- package/dist/api/rest/api-generator.d.ts +9 -1
- package/dist/api/rest/idempotency.d.ts +15 -1
- package/dist/api/root-error-handler.d.ts +6 -5
- package/dist/api/types.d.ts +12 -0
- package/dist/{ast-schema-editor-mCeSC03x.js → ast-schema-editor-madTa9Hh.js} +2 -2
- package/dist/{ast-schema-editor-mCeSC03x.js.map → ast-schema-editor-madTa9Hh.js.map} +1 -1
- package/dist/auth/access.d.ts +10 -0
- package/dist/auth/adapter-middleware.d.ts +2 -1
- package/dist/auth/adapter-service-key.d.ts +24 -0
- package/dist/auth/api-keys/api-key-middleware.d.ts +21 -1
- package/dist/auth/api-keys/api-key-store.d.ts +8 -1
- package/dist/auth/api-keys/key-grant.d.ts +18 -1
- package/dist/auth/expired-token-sweep.d.ts +5 -0
- package/dist/auth/impersonation.d.ts +3 -1
- package/dist/auth/jwt.d.ts +8 -2
- package/dist/auth/oauth-signin-policy.d.ts +21 -35
- package/dist/auth/rls-scope.d.ts +17 -7
- package/dist/auth/rls-simulator-routes.d.ts +51 -0
- package/dist/auth/simulation-read-only.d.ts +28 -0
- package/dist/auth/simulation.d.ts +141 -0
- package/dist/auth/sql-rate-limit-store.d.ts +15 -1
- package/dist/{auth-CteGWxsL.js → auth-izI7AEzC.js} +391 -1893
- package/dist/auth-izI7AEzC.js.map +1 -0
- package/dist/auth_adapter-B7RUMk6e.js +89 -0
- package/dist/auth_adapter-B7RUMk6e.js.map +1 -0
- package/dist/backend/boot-announcements.d.ts +13 -0
- package/dist/backend/create-backend.d.ts +10 -0
- package/dist/backend/options.d.ts +9 -4
- package/dist/{backend-DTAOsLQc.js → backend-CaG_Jyso.js} +9 -2
- package/dist/backend-CaG_Jyso.js.map +1 -0
- package/dist/{backup-Dq0lyJ_D.js → backup-BVbHoKo8.js} +8 -8
- package/dist/{backup-Dq0lyJ_D.js.map → backup-BVbHoKo8.js.map} +1 -1
- package/dist/boot/boot.d.ts +2 -15
- package/dist/boot/ddl-bootstrap.d.ts +34 -0
- package/dist/boot/env.d.ts +6 -0
- package/dist/boot/fetch-bundle.d.ts +5 -1
- package/dist/boot/provision-only.d.ts +23 -0
- package/dist/boot/provision.d.ts +53 -2
- package/dist/boot/role.d.ts +2 -0
- package/dist/boot/schema-change-recorder.d.ts +45 -0
- package/dist/boot/security-headers.d.ts +22 -0
- package/dist/branch-routes-B2vvEx53.js +84 -0
- package/dist/branch-routes-B2vvEx53.js.map +1 -0
- package/dist/{contract-routes-DiFwiLg2.js → contract-routes-CU2A3JnF.js} +3 -3
- package/dist/{contract-routes-DiFwiLg2.js.map → contract-routes-CU2A3JnF.js.map} +1 -1
- package/dist/cron/cron-store.d.ts +8 -1
- package/dist/{cron-loader-NHqRfKF0.js → cron-loader-mMhqbVsl.js} +3 -3
- package/dist/{cron-loader-NHqRfKF0.js.map → cron-loader-mMhqbVsl.js.map} +1 -1
- package/dist/{cron-routes-Dhf82HzT.js → cron-routes-Cs33j_Kr.js} +5 -5
- package/dist/{cron-routes-Dhf82HzT.js.map → cron-routes-Cs33j_Kr.js.map} +1 -1
- package/dist/{cron-scheduler-D1DT0nw5.js → cron-scheduler-Be7CNghL.js} +3 -3
- package/dist/{cron-scheduler-D1DT0nw5.js.map → cron-scheduler-Be7CNghL.js.map} +1 -1
- package/dist/{cron-store-DEpVstUU.js → cron-store-DLn11M8H.js} +11 -10
- package/dist/cron-store-DLn11M8H.js.map +1 -0
- package/dist/{strings.es-BeZWAodn.js → data_driver-D0mg1Y_x.js} +2 -90
- package/dist/data_driver-D0mg1Y_x.js.map +1 -0
- package/dist/{ddl-bootstrap-BNqi2NZe.js → ddl-bootstrap-CjRrn12l.js} +92 -3
- package/dist/ddl-bootstrap-CjRrn12l.js.map +1 -0
- package/dist/deploy/pod-contract.d.ts +1 -1
- package/dist/email/dev-sink.d.ts +6 -0
- package/dist/email/index.d.ts +1 -1
- package/dist/{errors-_ytoGi8A.js → errors-OMWkKSH-.js} +66 -5
- package/dist/errors-OMWkKSH-.js.map +1 -0
- package/dist/{soft-delete-params-BAgpmBvp.js → field-access-DYsvjI_P.js} +27 -57
- package/dist/field-access-DYsvjI_P.js.map +1 -0
- package/dist/{filter-dialect-O1HK6Nvr.js → filter-dialect-TrKFWLqT.js} +3 -3
- package/dist/{filter-dialect-O1HK6Nvr.js.map → filter-dialect-TrKFWLqT.js.map} +1 -1
- package/dist/{function-loader-DbCSoNJj.js → function-loader-B8YzXEsQ.js} +22 -15
- package/dist/function-loader-B8YzXEsQ.js.map +1 -0
- package/dist/{function-routes-DNV4RUF0.js → function-routes-RxrCHNuM.js} +2 -2
- package/dist/{function-routes-DNV4RUF0.js.map → function-routes-RxrCHNuM.js.map} +1 -1
- package/dist/functions/hono-like.d.ts +12 -0
- package/dist/functions/index.js +37 -2
- package/dist/functions/index.js.map +1 -1
- package/dist/functions/request-timeout.d.ts +3 -3
- package/dist/history/history-routes.d.ts +6 -0
- package/dist/history/retention-sweep.d.ts +5 -0
- package/dist/{history-recorder-RZ1Ow4YY.js → history-recorder-Bsau7h4X.js} +7 -7
- package/dist/history-recorder-Bsau7h4X.js.map +1 -0
- package/dist/{history-store-DPofGVDp.js → history-store-BQz5YCkW.js} +26 -2
- package/dist/history-store-BQz5YCkW.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.es.js +3003 -681
- package/dist/index.es.js.map +1 -1
- package/dist/init/process-safety.d.ts +4 -3
- package/dist/init/shutdown.d.ts +8 -0
- package/dist/init.d.ts +6 -1
- package/dist/jobs/job-store.d.ts +8 -1
- package/dist/{jobs-8G-_agld.js → jobs-Co6Ni5HP.js} +7 -7
- package/dist/jobs-Co6Ni5HP.js.map +1 -0
- package/dist/{jwt-YhciR93H.js → jwt-rDS0884_.js} +5 -4
- package/dist/{jwt-YhciR93H.js.map → jwt-rDS0884_.js.map} +1 -1
- package/dist/{keys-COzQ2aE8.js → keys-BXcdRNhJ.js} +2 -2
- package/dist/{keys-COzQ2aE8.js.map → keys-BXcdRNhJ.js.map} +1 -1
- package/dist/{logger-Bt32sHFw.js → logger-CW8pmTS6.js} +35 -3
- package/dist/logger-CW8pmTS6.js.map +1 -0
- package/dist/{logs-routes-CelTiS7C.js → logs-routes-CBt7mMIL.js} +3 -3
- package/dist/{logs-routes-CelTiS7C.js.map → logs-routes-CBt7mMIL.js.map} +1 -1
- package/dist/mcp/consent-page.d.ts +38 -1
- package/dist/mcp/mcp-routes.d.ts +9 -0
- package/dist/mcp/mcp-tools.d.ts +7 -1
- package/dist/mcp/oauth-store.d.ts +12 -5
- package/dist/metrics/history-recorder.d.ts +4 -1
- package/dist/metrics/history-store.d.ts +8 -0
- package/dist/{offline-BrqCRgih-BtDB1ecy.js → offline-AvuEcg7u-DFHL98OT.js} +127 -40
- package/dist/offline-AvuEcg7u-DFHL98OT.js.map +1 -0
- package/dist/{offline-store-BhYXViha-BW5j1We_.js → offline-store-BhYXViha-YTmjBpo2.js} +5 -5
- package/dist/{offline-store-BhYXViha-BW5j1We_.js.map → offline-store-BhYXViha-YTmjBpo2.js.map} +1 -1
- package/dist/{openapi-generator-BamY6-ga.js → openapi-generator-CyxEtYEX.js} +61 -22
- package/dist/openapi-generator-CyxEtYEX.js.map +1 -0
- package/dist/{proxy-BEKbsil6.js → proxy-BYir8tWK.js} +5 -7
- package/dist/proxy-BYir8tWK.js.map +1 -0
- package/dist/{query-parser-CGohjyWI.js → query-parser-CDXWf2hH.js} +7 -6
- package/dist/{query-parser-CGohjyWI.js.map → query-parser-CDXWf2hH.js.map} +1 -1
- package/dist/{request-timeout-CRny5KD0.js → request-timeout-DVjo-c_8.js} +6 -6
- package/dist/request-timeout-DVjo-c_8.js.map +1 -0
- package/dist/rls-simulator-routes-bEY7-5-u.js +190 -0
- package/dist/rls-simulator-routes-bEY7-5-u.js.map +1 -0
- package/dist/schema-edit/deployed-schema-guard.d.ts +47 -0
- package/dist/{schema-editor-routes-CnNGCayU.js → schema-editor-routes-Dc3DU_jm.js} +4 -4
- package/dist/{schema-editor-routes-CnNGCayU.js.map → schema-editor-routes-Dc3DU_jm.js.map} +1 -1
- package/dist/soft-delete-params-BRONCOA2.js +59 -0
- package/dist/soft-delete-params-BRONCOA2.js.map +1 -0
- package/dist/{sql-export-routes-BgTJ7K3S.js → sql-export-routes-QycX8W8S.js} +40 -6
- package/dist/sql-export-routes-QycX8W8S.js.map +1 -0
- package/dist/{src-D5Y3N-S4.js → src-Bh9LRPb9.js} +21 -2
- package/dist/{src-D5Y3N-S4.js.map → src-Bh9LRPb9.js.map} +1 -1
- package/dist/storage/GCSStorageController.d.ts +2 -0
- package/dist/storage/LocalStorageController.d.ts +8 -3
- package/dist/storage/S3StorageController.d.ts +2 -0
- package/dist/storage/maintenance-keys.d.ts +17 -0
- package/dist/storage/maintenance-routes.d.ts +28 -5
- package/dist/storage/maintenance.d.ts +136 -4
- package/dist/storage/orphan-scans.d.ts +120 -0
- package/dist/storage/types.d.ts +11 -0
- package/dist/strings.es-B7QLnHh2.js +96 -0
- package/dist/strings.es-B7QLnHh2.js.map +1 -0
- package/dist/{upload-validation-I9cCUBIW.js → upload-validation-Bgo626uY.js} +2 -2
- package/dist/upload-validation-Bgo626uY.js.map +1 -0
- package/dist/utils/dev-dependency-command.d.ts +15 -0
- package/dist/utils/dev-port.d.ts +8 -0
- package/dist/utils/logger.d.ts +24 -0
- package/package.json +5 -5
- package/dist/GCSStorageController-C7ARrDAp.js.map +0 -1
- package/dist/S3StorageController-qevJ0pVE.js.map +0 -1
- package/dist/auth-CteGWxsL.js.map +0 -1
- package/dist/auth_adapter-CIItCTc8.js +0 -21
- package/dist/auth_adapter-CIItCTc8.js.map +0 -1
- package/dist/backend-DTAOsLQc.js.map +0 -1
- package/dist/cron-store-DEpVstUU.js.map +0 -1
- package/dist/ddl-bootstrap-BNqi2NZe.js.map +0 -1
- package/dist/errors-_ytoGi8A.js.map +0 -1
- package/dist/function-loader-DbCSoNJj.js.map +0 -1
- package/dist/history-recorder-RZ1Ow4YY.js.map +0 -1
- package/dist/history-store-DPofGVDp.js.map +0 -1
- package/dist/jobs-8G-_agld.js.map +0 -1
- package/dist/logger-Bt32sHFw.js.map +0 -1
- package/dist/offline-BrqCRgih-BtDB1ecy.js.map +0 -1
- package/dist/openapi-generator-BamY6-ga.js.map +0 -1
- package/dist/proxy-BEKbsil6.js.map +0 -1
- package/dist/request-timeout-CRny5KD0.js.map +0 -1
- package/dist/soft-delete-params-BAgpmBvp.js.map +0 -1
- package/dist/sql-export-routes-BgTJ7K3S.js.map +0 -1
- package/dist/strings.es-BeZWAodn.js.map +0 -1
- package/dist/upload-validation-I9cCUBIW.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"openapi-generator-BamY6-ga.js","names":[],"sources":["../../types/src/types/relations.ts","../../types/src/types/tenancy.ts","../../common/src/util/tenant.ts","../../common/src/collections/soft-delete.ts","../src/api/openapi-generator.ts"],"sourcesContent":["import type { AnyCollectionConfig } from \"./collections\";\nimport type { Properties } from \"./properties\";\n\n/**\n * @group Models\n */\nexport type OnAction = \"cascade\" | \"restrict\" | \"no action\" | \"set null\" | \"set default\";\n\n/**\n * The key a junction row's own columns are carried under, in both directions.\n *\n * A read that includes a `manyToMany` relation serves each related row with its\n * link's columns nested here — `{ id: 5, name: \"ts\", _pivot: { role: \"owner\" } }`\n * — and a membership write may name the same key on an element to state what\n * the link should hold. One constant because the two have to be the same word:\n * a wire name that differs between the read and the write it round-trips\n * through is a shape no client can echo back.\n *\n * Leading underscore, like `_matches`: it reads as metadata about the row\n * rather than as one of its columns. A payload property may not be named\n * `_pivot` either — `checkJunctionPayload` refuses it — so the key means one\n * thing wherever it appears.\n *\n * @group Models\n */\nexport const JUNCTION_PIVOT_KEY = \"_pivot\";\n\n/**\n * The key a row carries the totals of its counted relations under.\n *\n * An include that asks for a count — `include: { tags: { limit: 4, count: true } }`\n * — serves, beside the (capped) `tags`, how many related rows there are in all:\n * `{ id: 1, tags: [ …4 rows ], _count: { tags: 37 } }`. One object per row,\n * keyed by relation name, so counting a second relation adds a key rather than\n * a second top-level name.\n *\n * Leading underscore, like {@link JUNCTION_PIVOT_KEY} and `_matches`: it\n * describes the read, not the record, and no column or form writes it. A\n * collection with a property of this name cannot ask for a count — the read\n * answers 400 rather than writing over the column.\n *\n * @group Models\n */\nexport const INCLUDE_COUNT_KEY = \"_count\";\n\n/**\n * What kind of link a relation is.\n *\n * The discriminant. Every other field a relation carries belongs to exactly one\n * of these, which is the point: a relation used to be a single open interface\n * where `cardinality`, `direction`, `localKey`, `foreignKeyOnTarget`, `through`\n * and `joinPath` were all optional and any combination typechecked. Which link\n * you meant then had to be *inferred* from which fields you happened to set,\n * and the inference was ~200 lines that guessed, fell back on naming\n * conventions, and swallowed its own failures.\n *\n * Most of that guessing produced bugs rather than convenience. A `many`\n * relation carrying a `localKey` — a combination the old type permitted — made\n * the write path stamp the parent's own foreign key onto the child row. Under\n * these kinds that state cannot be written down.\n *\n * @group Models\n */\nexport type RelationKind = \"belongsTo\" | \"hasOne\" | \"hasMany\" | \"manyToMany\" | \"via\";\n\n/** Fields every relation carries, whatever its kind. @group Models */\nexport interface RelationBase {\n /**\n * The name this link is addressed by: the key in `include`, the tab in the\n * admin panel, and the path segment of a nested URL.\n *\n * Defaults to the declaring property's key, or to the target's slug for an\n * entry in `relations`.\n */\n relationName?: string;\n\n /** The collection on the other end. */\n target: () => AnyCollectionConfig;\n\n /**\n * What the database does to this side's foreign key when the target row's\n * key changes. Emitted as the constraint's `ON UPDATE`.\n *\n * Unset means no clause, which Postgres reads as `NO ACTION`. Set\n * `\"cascade\"` when the target's key is a natural key that can be edited —\n * a slug, a SKU — so the pointers follow it.\n *\n * Only a `belongsTo` puts the key on this table, so this is the only kind\n * where the clause is written here; on the other kinds it describes the\n * constraint the target's own column carries.\n */\n onUpdate?: OnAction;\n /**\n * What the database does to this side's rows when the target row is\n * deleted. Emitted as the constraint's `ON DELETE`.\n *\n * Defaults, when unset, to `\"set null\"` for an optional relation and\n * **`\"restrict\"`** for a required one. `NOT NULL` says a child cannot exist\n * without a parent; it does not say deleting the parent should delete the\n * child. Ask for `\"cascade\"` when that is what you mean — it is the one\n * value that destroys rows you did not name.\n *\n * A `manyToMany` is the exception: its junction rows default to\n * `\"cascade\"`, because the row deleted there is the link and not the target.\n */\n onDelete?: OnAction;\n\n /**\n * Presentation overrides applied when this relation is rendered as a tab.\n *\n * Whether the link is *required* is not here: it is\n * `validation: { required: true }` on the declaring property, the same key\n * every other field uses. A relation carried its own copy until 0.18, and\n * the two disagreed by construction — the DDL generator read the property\n * (so the column was `NOT NULL`) while codegen read the relation (so the\n * generated `Insert` type made it optional), and a `create()` that\n * typechecked failed at the database.\n */\n overrides?: Partial<AnyCollectionConfig>;\n}\n\n/**\n * This collection holds the foreign key. One target row per source row.\n *\n * ```ts\n * author: { kind: \"belongsTo\", target: () => authors, localKey: \"author_id\" }\n * ```\n * @group Models\n */\nexport interface BelongsToRelation extends RelationBase {\n kind: \"belongsTo\";\n /**\n * Column on **this** collection's table holding the target's key.\n * Defaults to `<relationName>_id`.\n */\n localKey?: string;\n /**\n * Whether Rebase creates a btree index on {@link localKey}. Defaults to\n * `true`.\n *\n * Postgres indexes the key a foreign key *points at* but never the column\n * holding it, so without one every read from the other side seq-scans this\n * table: a `hasMany`/`hasOne` include, a nested listing such as\n * `/authors/5/posts`, and the `ON DELETE` check when a target row is\n * deleted. The derived index is skipped on its own when the column already\n * leads one — the primary key, a `validation.unique`, or an entry in\n * `indexes:` — so it never duplicates an index you declared.\n *\n * Set `false` for a table so write-heavy that the index costs more than\n * the lookups it serves, or when you keep an index of your own on the\n * column outside Rebase's `indexes:` block.\n */\n index?: boolean;\n}\n\n/**\n * The target holds the foreign key, and at most one target row points back.\n *\n * ```ts\n * profile: { kind: \"hasOne\", target: () => profiles, foreignKeyOnTarget: \"user_id\" }\n * ```\n * @group Models\n */\nexport interface HasOneRelation extends RelationBase {\n kind: \"hasOne\";\n /**\n * Column on the **target's** table holding this collection's key.\n * Defaults to `<thisCollection>_id`.\n */\n foreignKeyOnTarget?: string;\n /**\n * Column on **this** collection's table whose value `foreignKeyOnTarget`\n * holds. Defaults to this collection's primary key.\n *\n * Set it when the two sides are joined on a natural key rather than on the\n * row id — an external identity id, a SKU, a tenant slug. See\n * {@link HasManyRelation.sourceKey}, which this mirrors.\n */\n sourceKey?: string;\n}\n\n/**\n * The target holds the foreign key, and many target rows point back. The\n * children belong to this parent alone — deleting one deletes a row.\n *\n * ```ts\n * posts: { kind: \"hasMany\", target: () => posts, foreignKeyOnTarget: \"author_id\" }\n * ```\n * @group Models\n */\nexport interface HasManyRelation extends RelationBase {\n kind: \"hasMany\";\n /**\n * Column on the **target's** table holding this collection's key.\n * Defaults to `<thisCollection>_id`.\n */\n foreignKeyOnTarget?: string;\n /**\n * Column on **this** collection's table whose value `foreignKeyOnTarget`\n * holds. Defaults to this collection's primary key.\n *\n * The mirror of `localKey` on {@link BelongsToRelation}: that one names the\n * column this side reads from, this one names the column the other side\n * points at. Without it the pair can only be joined on the row id, which\n * makes a natural-key link — `auth_user_id ↔ auth_user_id`, a SKU, a tenant\n * slug — inexpressible as `hasMany`, and it has to drop to the read-only\n * `via`.\n *\n * The column must be unique: the link addresses one source row per value,\n * and Postgres will not accept a foreign key against a non-unique column.\n *\n * ```ts\n * applications: {\n * kind: \"hasMany\",\n * target: () => talentApplications,\n * sourceKey: \"auth_user_id\",\n * foreignKeyOnTarget: \"auth_user_id\"\n * }\n * ```\n */\n sourceKey?: string;\n}\n\n/**\n * Both sides hold many, through a junction table. The target rows are shared,\n * so this collection owns the *link* and not the row: removing one removes a\n * junction row and leaves the target alone.\n *\n * Declared the same way from either side — there is no owning and inverse\n * version. Swap `sourceColumn` and `targetColumn` to describe the other\n * direction.\n *\n * ```ts\n * tags: { kind: \"manyToMany\", target: () => tags }\n * ```\n * @group Models\n */\nexport interface ManyToManyRelation extends RelationBase {\n kind: \"manyToMany\";\n /**\n * The junction table and its two key columns. Every part defaults: the\n * table to both table names sorted and joined, the columns to\n * `<collection>_id` and `<relationName>_id`.\n */\n through?: {\n table?: string;\n /** Junction column holding **this** collection's key. */\n sourceColumn?: string;\n /** Junction column holding the **target's** key. */\n targetColumn?: string;\n /**\n * Extra columns the junction row carries, declared exactly like a\n * collection's properties.\n *\n * A membership is often not only a membership. \"This user is in that\n * organisation\" is really \"…as an `owner`, since March\"; \"this tag is\n * on that post\" is really \"…in third place\". Until this existed the\n * junction was two key columns and nothing else, so the role and the\n * position had to become a collection of their own — which is a\n * different data model, a different set of policies and a different\n * URL, for what is still one link.\n *\n * The properties are read by the same planner that reads a\n * collection's, so a payload column gets the type, `NOT NULL`,\n * `DEFAULT`, `UNIQUE` and enum type it would get on a table. What it\n * does **not** get is `indexes` (declared per collection, and no\n * collection declares a junction), `search`, `vector`, or anything a\n * relation would put on it — a payload property may not be a\n * `relation`, a `reference` or a `vector`, and config validation\n * refuses one that is.\n *\n * On the wire the values travel under {@link JUNCTION_PIVOT_KEY}: a\n * read serves `{ …target, _pivot: { role } }`, and a membership write\n * accepts `{ id, _pivot: { role } }` beside the bare ids.\n *\n * ```ts\n * members: {\n * kind: \"manyToMany\",\n * target: () => users,\n * through: {\n * table: \"org_members\",\n * properties: {\n * role: { type: \"string\", enum: { owner: \"Owner\", admin: \"Admin\", member: \"Member\" },\n * defaultValue: \"member\", validation: { required: true } },\n * joinedAt: { type: \"date\", autoValue: \"on_create\" }\n * }\n * }\n * }\n * ```\n *\n * Both sides of the same junction may declare it, and both must agree:\n * `resolveJunctionSpecs` refuses two declarations of the same payload\n * key that do not describe the same column, because only one of them\n * could ever be created.\n */\n properties?: Properties;\n };\n}\n\n/**\n * An explicit chain of joins, for links the four shapes above cannot express:\n * multi-hop paths, composite keys, or a join whose condition is not a plain\n * foreign key.\n *\n * Read-only. Rebase will not infer how to write through an arbitrary join\n * chain, and guessing is what this type exists to stop.\n *\n * ```ts\n * permissions: {\n * kind: \"via\",\n * target: () => permissions,\n * cardinality: \"many\",\n * joinPath: [\n * { table: \"user_roles\", on: { from: \"id\", to: \"user_id\" } },\n * { table: \"role_permissions\", on: { from: \"role_id\", to: \"role_id\" } },\n * { table: \"permissions\", on: { from: \"permission_id\", to: \"id\" } }\n * ]\n * }\n * ```\n * @group Models\n */\nexport interface ViaRelation extends RelationBase {\n kind: \"via\";\n /** Whether the chain yields one row or many. Cannot be derived from a join chain. */\n cardinality: \"one\" | \"many\";\n /**\n * The joins, in order, from this collection's table to the target's.\n *\n * Each step names a table and the columns to join it on; the last step's\n * table is the target. Read-only, because Rebase will not work out how to\n * write through an arbitrary chain, and guessing is what this kind exists to\n * stop.\n */\n joinPath: JoinStep[];\n}\n\n/**\n * A link from one collection to another, as authored.\n *\n * A closed union: pick the kind that describes the link and the type offers\n * exactly the fields that kind needs. See {@link ResolvedRelation} for the form\n * the runtime works with, which has every default filled in.\n *\n * @group Models\n */\nexport type Relation =\n | BelongsToRelation\n | HasOneRelation\n | HasManyRelation\n | ManyToManyRelation\n | ViaRelation;\n\n/**\n * A relation with every default filled in — the form the runtime works with.\n *\n * The authored {@link Relation} and this are deliberately different types.\n * They used to be one, which meant no reader could tell which fields had been\n * supplied and which had been guessed, and so every consumer re-derived what it\n * needed with its own chain of `if (through) … else if (localKey) …` fallbacks.\n * Those chains disagreed with each other; that disagreement is what produced\n * silently wrong reads and corrupt writes.\n *\n * Here each variant carries exactly its own fields, all required. A consumer\n * switches on `kind` and gets what it needs without a fallback, and the cases\n * it forgot are a compile error rather than a wrong answer at runtime.\n *\n * @group Models\n */\nexport type ResolvedRelation =\n | ResolvedBelongsTo\n | ResolvedHasOne\n | ResolvedHasMany\n | ResolvedManyToMany\n | ResolvedVia;\n\n/** Fields present on every resolved relation. @group Models */\nexport interface ResolvedRelationBase {\n /** Always set: defaulted during resolution if the author omitted it. */\n relationName: string;\n /**\n * The collection on the other end.\n *\n * Still a thunk — a relation between two collections that import each other\n * has to be — but normalised: resolution unwraps the module namespace the\n * author's `() => import(…)` may hand back, so every consumer gets the\n * config and not a `{ default: … }` wrapper.\n */\n target: () => AnyCollectionConfig;\n /** The target's slug, resolved once so consumers need not call `target()`. */\n targetSlug: string;\n /** As authored — see {@link RelationBase.onUpdate}. Defaults are not filled in. */\n onUpdate?: OnAction;\n /**\n * As authored — see {@link RelationBase.onDelete}. `undefined` here means\n * the author said nothing, and the DDL generator picks the default; it does\n * **not** mean \"no action\".\n */\n onDelete?: OnAction;\n /** Presentation overrides applied when this relation is rendered as a tab. */\n overrides?: Partial<AnyCollectionConfig>;\n /**\n * Whether one row or many come back. Derived from `kind` — kept because it\n * is what most consumers actually branch on, and because `via` is the one\n * kind where it is authored rather than implied.\n */\n cardinality: \"one\" | \"many\";\n /**\n * Whether Rebase knows how to write through this link. False only for\n * {@link ResolvedVia}, whose join chain it will not invent a write for.\n */\n writable: boolean;\n /**\n * Whether the target rows are shared with other parents. True for\n * many-to-many and for multi-hop `via`: what the parent owns is the link,\n * so removing one must not delete the row.\n */\n shared: boolean;\n}\n\n/** @group Models */\nexport interface ResolvedBelongsTo extends ResolvedRelationBase {\n kind: \"belongsTo\";\n cardinality: \"one\";\n writable: true;\n shared: false;\n /** Column on this collection's table. */\n localKey: string;\n /** As authored, with the default filled in — see {@link BelongsToRelation.index}. */\n index: boolean;\n}\n\n/** @group Models */\nexport interface ResolvedHasOne extends ResolvedRelationBase {\n kind: \"hasOne\";\n cardinality: \"one\";\n writable: true;\n shared: false;\n /** Column on the target's table. */\n foreignKeyOnTarget: string;\n /** @see ResolvedHasMany.sourceKey */\n sourceKey?: string;\n}\n\n/** @group Models */\nexport interface ResolvedHasMany extends ResolvedRelationBase {\n kind: \"hasMany\";\n cardinality: \"many\";\n writable: true;\n shared: false;\n /** Column on the target's table. */\n foreignKeyOnTarget: string;\n /**\n * Column on the source's table that `foreignKeyOnTarget` points at, or\n * `undefined` for the source's primary key.\n *\n * The one optional field on a resolved relation, and deliberately so. Every\n * other default is filled in here because it can be: a table name and a\n * column name are derivable from the relation and its two endpoints alone.\n * The primary key is not — this driver resolves it from `isId`, then the\n * Drizzle schema, then a column named `id`, and the middle tier does not\n * exist at resolution time.\n *\n * So `undefined` is a sentinel with exactly one meaning, not a field a\n * consumer is invited to guess at. Read it through `sourceKeyField()`,\n * which is the only place that turns it into a column name.\n */\n sourceKey?: string;\n}\n\n/** @group Models */\nexport interface ResolvedManyToMany extends ResolvedRelationBase {\n kind: \"manyToMany\";\n cardinality: \"many\";\n writable: true;\n shared: true;\n /**\n * The junction table and its two key columns, with every default filled in:\n * the table from both table names sorted and joined, the columns from each\n * endpoint's slug.\n */\n through: {\n table: string;\n sourceColumn: string;\n targetColumn: string;\n /**\n * The payload columns as authored, or `{}` when there are none —\n * never `undefined`, so a consumer reads one shape.\n * See {@link ManyToManyRelation.through}.\n */\n properties: Properties;\n };\n}\n\n/** @group Models */\nexport interface ResolvedVia extends ResolvedRelationBase {\n kind: \"via\";\n writable: false;\n /** The chain as authored — see {@link ViaRelation.joinPath}. Nothing to default. */\n joinPath: JoinStep[];\n}\n\n// ── Narrowing helpers ────────────────────────────────────────────────\n//\n// Consumers that only care about one axis — \"does this list many rows\",\n// \"is there a column on the target\" — should ask that question rather\n// than enumerate kinds, so adding a kind later does not silently skip them.\n\n/** Relations whose target row carries this collection's key. @group Models */\nexport type ResolvedForeignKeyOnTarget = ResolvedHasOne | ResolvedHasMany;\n\n/** @group Models */\nexport function hasForeignKeyOnTarget(relation: ResolvedRelation): relation is ResolvedForeignKeyOnTarget {\n return relation.kind === \"hasOne\" || relation.kind === \"hasMany\";\n}\n\n/** @group Models */\nexport function isManyToMany(relation: ResolvedRelation): relation is ResolvedManyToMany {\n return relation.kind === \"manyToMany\";\n}\n\n/** @group Models */\nexport function isToMany(relation: ResolvedRelation): boolean {\n return relation.cardinality === \"many\";\n}\n\n/**\n * Defines a single, explicit step in a multi-join path.\n *\n * Each step represents one JOIN operation in the sequence. The `from` columns\n * refer to the previous table in the chain (or the source table for the first step),\n * and the `to` columns refer to the current table being joined.\n *\n * @example Single column join:\n * ```typescript\n * {\n * table: \"authors\",\n * on: {\n * from: \"author_id\", // Column from previous table (e.g., posts.author_id)\n * to: \"id\" // Column from current table (authors.id)\n * }\n * }\n * ```\n *\n * @example Multi-column composite key join:\n * ```typescript\n * {\n * table: \"order_items\",\n * on: {\n * from: [\"order_id\", \"store_id\"], // Multiple columns from previous table\n * to: [\"order_id\", \"store_id\"] // Corresponding columns in current table\n * }\n * }\n * ```\n */\nexport interface JoinStep {\n /**\n * The database table name to join TO in this step.\n * This is the table you're joining into, not the table you're joining from.\n *\n * @example \"authors\", \"user_roles\", \"product_categories\"\n */\n table: string;\n\n /**\n * The join condition for this step. Defines how the previous table\n * connects to the current table.\n *\n * - `from`: Column name(s) on the PREVIOUS table in the join chain\n * - `to`: Column name(s) on the CURRENT table (specified in `table`)\n *\n * For the first step, `from` refers to the source collection's table.\n * For subsequent steps, `from` refers to the table from the previous step.\n *\n * Both `from` and `to` support:\n * - Single column: `\"user_id\"`\n * - Multiple columns: `[\"company_id\", \"region_id\"]` for composite keys\n *\n * When using arrays, both `from` and `to` must have the same length,\n * and columns are matched by position (index 0 with index 0, etc.).\n */\n on: {\n from: string | string[];\n to: string | string[];\n };\n}\n","/**\n * First-class multi-tenancy: one declaration, every layer.\n *\n * A tenant-scoped collection was expert work. It took four separate,\n * hand-written pieces that nothing checked against each other — a column, an\n * `existsIn` or raw RLS rule, a value stamped on every insert by a callback,\n * and an index somebody had to remember. Miss the index and the table scans;\n * miss the stamp and the row is invisible the moment it is written; miss the\n * rule and every tenant reads every other tenant's rows, which is the failure\n * nothing surfaces until it is a disclosure.\n *\n * {@link CollectionTenantConfig} is the one place that says \"this collection\n * belongs to a tenant\", and the four pieces are derived from it:\n *\n * - the column is `NOT NULL` and gets a btree index (`planSchema`);\n * - a **restrictive** RLS policy is injected for every operation, so it\n * composes with (rather than replaces) whatever `securityRules` the\n * collection declares — tenancy narrows, it never grants;\n * - the write path stamps the caller's tenant on create, refuses a write that\n * names another tenant, and refuses an update that moves a row between\n * tenants;\n * - the OpenAPI document marks the field so a generated client can see it.\n *\n * @see CollectionTenantConfig\n * @group Models\n */\n\n/**\n * The caller's tenant comes from a claim on their session token.\n *\n * The single-tenant-per-user shape: an identity provider (or Rebase's own\n * custom-claims hook) puts the organization on the token, and every request\n * carries it. Compiles to a comparison against `rebase.jwt() ->> '<claim>'`,\n * which is the same value a hand-written rule would read — so the generated\n * policy and anything an author writes beside it agree by construction.\n *\n * @group Models\n */\nexport interface TenantClaimSource {\n /**\n * The claim's name on the access token, e.g. `\"org_id\"`.\n *\n * Custom claims survive verification and reach RLS as `rebase.jwt()`; the\n * identity claims (`uid`, `roles`, `aal`, `isAnonymous`) are written after\n * them when a token is minted and cannot be shadowed, so naming one of\n * those here is refused rather than quietly reading the identity.\n */\n claim: string;\n}\n\n/**\n * The caller's tenants come from rows of a membership collection.\n *\n * The many-tenants-per-user shape — a `memberships` table with a user column\n * and a tenant column, which is how a person belongs to three organizations at\n * once. Compiles to a correlated `EXISTS` over that table (`policy.existsIn`),\n * so the database answers \"is the caller a member of this row's tenant?\" in the\n * same query rather than in an N+1 of lookups.\n *\n * Nothing is put on the token, so nothing has to be re-minted when somebody\n * joins or leaves a tenant — the next statement already sees the new row.\n *\n * @group Models\n */\nexport interface TenantMembershipSource {\n membership: {\n /** Slug of the collection holding the memberships. */\n collection: string;\n /** The property on it that holds the user id (compared to `rebase.uid()`). */\n userField: string;\n /** The property on it that holds the tenant id. */\n tenantField: string;\n };\n}\n\n/** Where the caller's tenant comes from. @group Models */\nexport type TenantSource = TenantClaimSource | TenantMembershipSource;\n\n/** Narrow a {@link TenantSource} to its claim form. @group Models */\nexport function isTenantClaimSource(source: TenantSource): source is TenantClaimSource {\n return typeof (source as TenantClaimSource).claim === \"string\";\n}\n\n/** Narrow a {@link TenantSource} to its membership form. @group Models */\nexport function isTenantMembershipSource(source: TenantSource): source is TenantMembershipSource {\n return typeof (source as TenantMembershipSource).membership === \"object\"\n && (source as TenantMembershipSource).membership !== null;\n}\n\n/**\n * The roles tenancy does not apply to, when the collection names none.\n *\n * `admin`, mirroring the security baseline every collection already carries\n * (`<table>_default_admin_read` / `_write`): the Studio, `dataAsAdmin` and a\n * support operator all run with it, and a tenancy rule that locked them out\n * would make the admin panel show an empty table on a collection full of rows.\n *\n * @group Models\n */\nexport const DEFAULT_TENANT_BYPASS_ROLES: readonly string[] = [\"admin\"];\n\n/**\n * Declare a collection tenant-scoped.\n *\n * ```ts\n * export const posts = buildCollection({\n * slug: \"posts\",\n * properties: {\n * orgId: { type: \"string\", validation: { required: true } },\n * title: { type: \"string\" }\n * },\n * tenant: { field: \"orgId\", from: { claim: \"org_id\" } }\n * });\n * ```\n *\n * The property has to exist — this says what a column *means*, it does not\n * conjure one into existence, exactly like `softDelete`. A config naming a\n * property the collection does not declare is refused at boot rather than at\n * the first read.\n *\n * ## What it composes with\n *\n * The injected policy is **restrictive**, so it is ANDed with every permissive\n * policy on the table: `securityRules`, `ownerField`, the injected admin\n * baseline. That is the only composition that is safe by construction — a\n * permissive tenancy policy would OR with the author's rules and a single\n * `access: \"public\"` rule would take the whole tenancy boundary off.\n *\n * Postgres-only. RLS is what enforces it, and an engine without row-level\n * security cannot be given this guarantee by an application-layer filter that\n * a raw query goes around.\n *\n * @group Models\n */\nexport interface CollectionTenantConfig<M extends Record<string, unknown> = Record<string, unknown>> {\n /**\n * The property holding the tenant id.\n *\n * A `string` or `number` property, or a `reference` / `belongsTo` relation\n * to the tenants collection — in which case the foreign key the relation\n * already declares is the column, and no second one is created.\n *\n * The column is made `NOT NULL` and indexed: a nullable tenant column is a\n * row that belongs to nobody and is therefore invisible to everybody, and\n * an unindexed one turns every RLS-filtered read into a sequential scan.\n */\n field: Extract<keyof M, string> | string;\n\n /** Where the caller's tenant comes from. */\n from: TenantSource;\n\n /**\n * Roles that see and write across every tenant.\n *\n * Defaults to {@link DEFAULT_TENANT_BYPASS_ROLES}. An empty array means\n * \"nobody bypasses\" — the trusted server context still does, because it is\n * what runs migrations and the auth flows, and a policy that excluded it\n * would break the boot rather than protect a tenant.\n */\n bypassRoles?: readonly string[];\n}\n","/**\n * The one reading of `collection.tenant`.\n *\n * Four things have to agree for a tenant-scoped collection to work — the\n * column, the RLS policy, the value stamped on insert and the index — and\n * before this they were four hand-written declarations that nothing compared.\n * The three that are schema become a {@link SecurityRule} and a column effect\n * derived here and in `planSchema`; the fourth, the write path, is\n * {@link resolveTenantWrite}.\n *\n * Everything in this module is pure. The policy it builds is a `SecurityRule`\n * like any other, which is what makes `db push`, the doctor, boot-ensure, the\n * drift detector and the Studio treat the tenancy policy as what it is —\n * generated, named, and recognisable — rather than as somebody's hand-written\n * SQL that a push should offer to drop.\n */\nimport {\n DEFAULT_TENANT_BYPASS_ROLES,\n isTenantClaimSource,\n policy,\n type CollectionConfig,\n type CollectionTenantConfig,\n type EntityStatus,\n type PolicyExpression,\n type SecurityRule\n} from \"@rebasepro/types\";\nimport { getTableName } from \"./relations\";\n\n/**\n * The collection's tenancy declaration, or nothing.\n *\n * Read through this rather than off the object, so the one shape check —\n * `tenant` is an object carrying a `field` and a `from` — is in one place. A\n * config that is *wrong* is refused by `validateCollectionConfig` with a\n * message; this is only asking whether there is one.\n */\nexport function getTenantConfig(collection: CollectionConfig | undefined): CollectionTenantConfig | undefined {\n const tenant = (collection as { tenant?: unknown } | undefined)?.tenant as CollectionTenantConfig | undefined;\n if (!tenant || typeof tenant !== \"object\") return undefined;\n if (typeof tenant.field !== \"string\" || !tenant.field) return undefined;\n if (!tenant.from || typeof tenant.from !== \"object\") return undefined;\n return tenant;\n}\n\n/** The roles tenancy does not apply to, defaulted. */\nexport function tenantBypassRoles(tenant: CollectionTenantConfig): readonly string[] {\n return tenant.bypassRoles ?? DEFAULT_TENANT_BYPASS_ROLES;\n}\n\n/**\n * The name of the policy a tenant declaration compiles to.\n *\n * Explicit — not a `getPolicyNameHash` of the rule — precisely because the\n * rule's *body* is compiled with more information in some callers than in\n * others (`planSchema` can resolve a relation's target collection and so knows\n * the column's type; the Studio, asking only for names, cannot). A hashed name\n * would then differ between the two, and the same policy would read as drift.\n * A frozen identifier: see `contracts/derived-names.txt`.\n */\nexport function tenantPolicyName(tableName: string): string {\n return `${tableName}_tenant_scope`;\n}\n\n/** The `reason` on the index a tenant column gets. Rendered into `schema.sql`. */\nexport const TENANT_INDEX_REASON = \"tenant scope\";\n\n/**\n * The condition a tenant declaration means, as a policy expression.\n *\n * `serverContext()` first, for the same reason every injected baseline rule\n * carries it: the trusted plane runs migrations, the auth flows and the boot,\n * and a restrictive policy that excluded it would not protect a tenant, it\n * would stop the server from starting.\n *\n * Then the bypass roles, then the tenancy test itself — a claim comparison or a\n * correlated `EXISTS` over the membership table, which are the two ways a\n * deployment answers \"which tenant is this caller in\".\n */\nexport function tenantScopeExpression(tenant: CollectionTenantConfig): PolicyExpression {\n const match: PolicyExpression = isTenantClaimSource(tenant.from)\n ? policy.compare(policy.field(tenant.field), \"eq\", policy.authClaim(tenant.from.claim))\n : policy.existsIn({\n collection: tenant.from.membership.collection,\n where: policy.and(\n policy.compare(\n policy.field(tenant.from.membership.tenantField),\n \"eq\",\n policy.outerField(tenant.field)\n ),\n policy.compare(\n policy.field(tenant.from.membership.userField),\n \"eq\",\n policy.authUid()\n )\n )\n });\n\n const bypass = tenantBypassRoles(tenant);\n return bypass.length > 0\n ? policy.or(policy.serverContext(), policy.rolesOverlap(bypass), match)\n : policy.or(policy.serverContext(), match);\n}\n\n/**\n * The rule a tenant declaration compiles to, or nothing when there is none.\n *\n * **Restrictive**, and that is the whole design. A restrictive policy is ANDed\n * with every other policy on the table, so tenancy narrows what the\n * collection's own `securityRules` allow and can never widen it. A permissive\n * one would OR with them, and a single `access: \"public\"` rule elsewhere in the\n * file would take the entire tenancy boundary off without contradicting\n * anything a reader could see.\n *\n * One rule with `operation: \"all\"` rather than four with `operations: [...]`:\n * `FOR ALL` gives Postgres the USING clause for SELECT/UPDATE/DELETE and the\n * WITH CHECK clause for INSERT/UPDATE, which is exactly the coverage wanted,\n * as one policy with one name instead of four.\n */\nexport function buildTenantSecurityRule(collection: CollectionConfig): SecurityRule | undefined {\n const tenant = getTenantConfig(collection);\n if (!tenant) return undefined;\n const expression = tenantScopeExpression(tenant);\n return {\n name: tenantPolicyName(getTableName(collection)),\n mode: \"restrictive\",\n operation: \"all\",\n condition: expression,\n check: expression\n };\n}\n\n// ── The write path ───────────────────────────────────────────────────────────\n\n/** Why a write was refused by tenancy. */\nexport interface TenantWriteRefusal {\n code: \"TENANT_REQUIRED\" | \"TENANT_MISMATCH\" | \"TENANT_IMMUTABLE\";\n /** The property, for a `violations` entry and for the message. */\n field: string;\n message: string;\n}\n\n/** What {@link resolveTenantWrite} decided. */\nexport type TenantWriteDecision =\n /** The values to write, with the tenant stamped if it was missing. */\n | { values: Record<string, unknown>; refusal?: undefined }\n | { refusal: TenantWriteRefusal; values?: undefined };\n\nexport interface TenantWriteInput {\n tenant: CollectionTenantConfig;\n /** The write's values, after defaults and hooks. */\n values: Record<string, unknown>;\n status: EntityStatus;\n /**\n * Every tenant the caller may write into.\n *\n * One entry for a claim, however many memberships they hold for the\n * membership form, and none for a caller carrying neither.\n */\n callerTenants: readonly unknown[];\n /**\n * Whether `callerTenants` is the whole list.\n *\n * A membership lookup is capped — a caller with more memberships than the\n * cap would otherwise make every write of theirs a large read. When the cap\n * is hit this is `false`, and a value that is not in the list is **let\n * through** rather than refused: the list is no longer evidence of absence,\n * and the policy's `WITH CHECK` is what actually decides. The API check is\n * an earlier, clearer refusal of the same writes, never a second authority.\n */\n callerTenantsComplete?: boolean;\n /**\n * True when tenancy does not apply to this caller — a bypass role, or the\n * trusted server context. The same set the policy lets through, so the API\n * and the database refuse the same writes.\n */\n bypass: boolean;\n /** The row's current values, on an update. */\n previousValues?: Record<string, unknown>;\n /** The collection slug, for the message. */\n slug: string;\n}\n\n/**\n * An id, however it arrived.\n *\n * A tenant field may be a `belongsTo` relation or a `reference`, and those\n * arrive over the wire as `{ id }` envelopes as often as bare ids. Comparing\n * the envelope to a bare id would refuse every correct write with\n * `TENANT_MISMATCH`, which is the most confusing possible failure — the caller\n * sent exactly the tenant they belong to.\n */\nfunction tenantIdOf(value: unknown): unknown {\n if (value === null || value === undefined) return value;\n if (typeof value === \"object\") {\n const id = (value as { id?: unknown }).id;\n return id === undefined ? value : id;\n }\n return value;\n}\n\n/**\n * Compare two tenant ids as the database will.\n *\n * Stringified, because JSON has one number type and Postgres has several: a\n * caller sending `\"42\"` for a `bigint` tenant column is writing the same row as\n * one sending `42`, and Postgres agrees after the cast. Refusing one of them\n * would be an API rule the database does not have.\n */\nfunction sameTenant(a: unknown, b: unknown): boolean {\n if (a === null || a === undefined || b === null || b === undefined) return false;\n return String(tenantIdOf(a)) === String(tenantIdOf(b));\n}\n\n/**\n * Stamp, or refuse, the tenant on a write.\n *\n * Three refusals, and each exists because the alternative lands somewhere\n * worse:\n *\n * - **`TENANT_REQUIRED`** — the caller has no tenant, or belongs to several and\n * named none. Stamping a guess would put the row in the wrong tenant; letting\n * it through would write a NULL into a `NOT NULL` column and surface as a\n * 23502 naming a column the caller never wrote.\n * - **`TENANT_MISMATCH`** — the caller named a tenant that is not theirs. The\n * database refuses this too, through the policy's `WITH CHECK`, but as a\n * 42501 \"new row violates row-level security policy\" with no mention of which\n * field or why. Refused here so the answer names the field.\n * - **`TENANT_IMMUTABLE`** — an update that moves a row to another tenant. RLS\n * would allow it whenever the caller belongs to both, and it is almost never\n * what anybody meant: it takes the row out of one tenant's history and drops\n * it into another's, with no trace on either side. A deliberate move is a\n * `bypassRoles` operation.\n *\n * A bypass caller is exempt from all three: they are trusted across tenants by\n * declaration, and stamping their write would silently confine a support\n * operator's row to whichever tenant they happen to carry.\n */\nexport function resolveTenantWrite(input: TenantWriteInput): TenantWriteDecision {\n const { tenant, values, status, callerTenants, bypass, previousValues, slug } = input;\n const field = tenant.field;\n const complete = input.callerTenantsComplete !== false;\n /** Is `value` one the caller may write? Unknown counts as yes — see `callerTenantsComplete`. */\n const callerHas = (value: unknown): boolean =>\n callerTenants.some(t => sameTenant(t, value)) || !complete;\n\n if (bypass) return { values };\n\n const provided = values[field];\n const creating = status !== \"existing\";\n\n if (!creating) {\n // An update that does not mention the field cannot move the row, and\n // the row's own tenant is already what RLS checked to let the update\n // through. Nothing to do.\n if (provided === undefined) return { values };\n\n const previous = previousValues?.[field];\n if (previous !== undefined && !sameTenant(provided, previous)) {\n return {\n refusal: {\n code: \"TENANT_IMMUTABLE\",\n field,\n message:\n `'${field}' is the tenant '${slug}' rows belong to, and a row cannot change tenant. ` +\n `This update would move it from '${String(tenantIdOf(previous))}' to ` +\n `'${String(tenantIdOf(provided))}'. Create the row in the other tenant and delete ` +\n \"this one, or perform the move with a role listed in `tenant.bypassRoles`.\"\n }\n };\n }\n if (previous === undefined && !callerHas(provided)) {\n return { refusal: mismatch(field, slug, provided, callerTenants) };\n }\n return { values };\n }\n\n if (provided === undefined || provided === null || provided === \"\") {\n if (callerTenants.length === 1) {\n return { values: { ...values, [field]: tenantIdOf(callerTenants[0]) } };\n }\n return {\n refusal: {\n code: \"TENANT_REQUIRED\",\n field,\n message: callerTenants.length === 0\n ? `'${slug}' is scoped to a tenant and this request carries none, so there is nothing ` +\n `to write into '${field}'. ` + sourceHint(tenant)\n : `'${slug}' is scoped to a tenant and this caller belongs to ${callerTenants.length} ` +\n `of them, so '${field}' cannot be inferred. Send it on the write — it must be one ` +\n \"the caller belongs to.\"\n }\n };\n }\n\n if (!callerHas(provided)) {\n return { refusal: mismatch(field, slug, provided, callerTenants) };\n }\n\n return { values };\n}\n\nfunction mismatch(\n field: string,\n slug: string,\n provided: unknown,\n callerTenants: readonly unknown[]\n): TenantWriteRefusal {\n return {\n code: \"TENANT_MISMATCH\",\n field,\n message:\n `'${field}' names tenant '${String(tenantIdOf(provided))}', which this caller does not belong ` +\n `to, so the write to '${slug}' would be refused by the database as well. ` +\n (callerTenants.length === 0\n ? \"This request carries no tenant at all.\"\n : `The caller's ${callerTenants.length === 1 ? \"tenant is\" : \"tenants are\"} ` +\n callerTenants.map(t => `'${String(tenantIdOf(t))}'`).join(\", \") + \".\")\n };\n}\n\n/** Where a caller's tenant was supposed to come from, for the 400. */\nfunction sourceHint(tenant: CollectionTenantConfig): string {\n return isTenantClaimSource(tenant.from)\n ? `The tenant comes from the '${tenant.from.claim}' claim on the caller's token; this one has no ` +\n \"such claim. Sign in, or add the claim in the custom-claims hook.\"\n : `The tenant comes from rows of '${tenant.from.membership.collection}' whose ` +\n `'${tenant.from.membership.userField}' is the caller; this caller has none.`;\n}\n","import type { CollectionConfig } from \"@rebasepro/types\";\n\n/** The field `softDelete: true` means, when the object form names none. */\nexport const DEFAULT_SOFT_DELETE_FIELD = \"deletedAt\";\n\n/**\n * The property a `softDelete` collection records a deletion in — its wire\n * name, what a delete stamps and what a restore (an update to `null`) clears —\n * or `undefined` for a collection without soft delete.\n *\n * One reading of the declaration for every layer that needs it: the driver\n * that stamps and filters on it, and the REST route that has to recognise a\n * restore before it looks the row up.\n */\nexport function softDeleteFieldOf(collection: CollectionConfig | undefined): string | undefined {\n // Declared on the Postgres collection shape only; narrowed, not cast.\n const declared = collection && \"softDelete\" in collection ? collection.softDelete : undefined;\n if (!declared) return undefined;\n return (typeof declared === \"object\" && declared.field) || DEFAULT_SOFT_DELETE_FIELD;\n}\n\n/**\n * Whether an update's values restore a soft-deleted row: they set the\n * soft-delete field back to `null`, the documented restore.\n *\n * A restore is an ordinary update of a row every default read hides, so a door\n * that looks the row up before updating it has to include the trashed rows for\n * this update — and only for this one: any other edit of a trashed row is a\n * 404.\n */\nexport function restoresSoftDeletedRow(\n collection: CollectionConfig | undefined,\n values: Record<string, unknown> | undefined\n): boolean {\n const field = softDeleteFieldOf(collection);\n return field !== undefined && values?.[field] === null;\n}\n","import { CollectionConfig, Property, StringProperty, NumberProperty, ArrayProperty, MapProperty, isToMany, ResolvedRelation, VectorProperty, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT } from \"@rebasepro/types\";\nimport { effectiveAccess, fieldKeyForColumn, findRelation, getTenantConfig, isRelationRequired, resolveCollectionRelations, softDeleteFieldOf } from \"@rebasepro/common\";\nimport { DELETED_QUERY_PARAM, HARD_DELETE_QUERY_PARAM } from \"./rest/soft-delete-params\";\n\n/**\n * OpenAPI 3.0.3 specification generator.\n *\n * Produces a spec that exactly mirrors the REST API consumed by the\n * Rebase SDK client (`@rebasepro/client`).\n *\n * Routes are mounted at `{basePath}/data/{slug}` by `initializeRebaseBackend`.\n */\n\nexport interface OpenApiGeneratorOptions {\n /** Base path for the API (e.g. \"/api\"). Defaults to \"/api\". */\n basePath?: string;\n /** Whether auth is enabled on data routes. Defaults to true. */\n requireAuth?: boolean;\n /**\n * The list-pagination bounds the REST layer applies, so the spec states the\n * ones a request will actually meet.\n *\n * These were hardcoded as `default: 20, maximum: 100` — neither of which\n * the server has ever used. The spec drives the API Explorer and is what a\n * generated client is built from, so an understated ceiling is a request\n * the client refuses to make, and an overstated one is a 400 nobody\n * predicted.\n */\n listLimits?: { defaultLimit?: number; maxLimit?: number };\n}\n\nexport function generateOpenApiSpec(\n collections: CollectionConfig[],\n options: OpenApiGeneratorOptions = {}\n): Record<string, unknown> {\n const basePath = options.basePath ?? \"/api\";\n const requireAuth = options.requireAuth ?? true;\n const defaultLimit = options.listLimits?.defaultLimit ?? DEFAULT_LIST_LIMIT;\n const maxLimit = options.listLimits?.maxLimit ?? MAX_LIST_LIMIT;\n\n /**\n * The query parameters every list endpoint honours.\n *\n * Written once because it was written twice: the root listing named eight\n * and the subcollection listing named four, though both go through the same\n * `parseQueryOptions` and the same fetch. Four capabilities were therefore\n * unreachable from a generated client on nested routes, and `or`/`and` were\n * undocumented on both.\n */\n const listQueryParameters = () => [\n { name: \"limit\", in: \"query\", schema: { type: \"integer\", default: defaultLimit, minimum: 1, maximum: maxLimit },\n description: `Maximum number of records to return. Must be a whole number between 1 and ${maxLimit}; a larger one is rejected with 400 INVALID_LIMIT rather than trimmed, so a short page always means a short collection. Page past the ceiling with \\`offset\\`.` },\n { name: \"offset\", in: \"query\", schema: { type: \"integer\", default: 0 },\n description: \"Number of records to skip\" },\n { name: \"page\", in: \"query\", schema: { type: \"integer\", minimum: 1 },\n description: \"Page number (alternative to offset). Calculates offset as (page-1)*limit. Cannot be combined with `offset` (400 PAGE_WITH_OFFSET).\" },\n {\n name: \"after\",\n in: \"query\",\n schema: { type: \"string\" },\n description:\n \"Keyset cursor: continue after the row the previous page ended on. Pass back \"\n + \"`meta.nextCursor` from that response, unchanged — it is opaque, and encodes both the \"\n + \"sort keys and the last row's values for them. Unlike `offset`, a row inserted or \"\n + \"deleted before the cursor cannot shift the window, so a walk neither repeats nor \"\n + \"skips rows. Cannot be combined with `offset`/`page` (400 CURSOR_WITH_OFFSET), and an \"\n + \"`orderBy` different from the one the cursor was issued under is refused \"\n + \"(400 CURSOR_ORDER_MISMATCH) rather than seeked in an order nobody asked for.\"\n },\n {\n name: \"orderBy\",\n in: \"query\",\n schema: { type: \"string\" },\n description:\n \"Sort field and direction. Accepts `field:asc`, `field:desc`, or `field:desc:last` — \"\n + \"the third segment places NULLs (`first`/`last`), defaulting to Postgres's own \"\n + \"convention (last ascending, first descending). Also accepts a JSON array \"\n + \"`[{\\\"field\\\":\\\"name\\\",\\\"direction\\\":\\\"asc\\\",\\\"nulls\\\":\\\"last\\\"}]` — several entries sort by \"\n + \"each in turn, the second breaking ties on the first.\",\n example: \"created_at:desc:last\"\n },\n {\n name: \"where\",\n in: \"query\",\n schema: { type: \"string\" },\n description: \"JSON object filter, mapping each field to a `[operator, value]` tuple. \"\n + \"Combines with the per-field `?field=op.value` parameters below; on the same field, the per-field parameter wins.\",\n example: \"{\\\"status\\\":[\\\"==\\\",\\\"active\\\"]}\"\n },\n {\n name: \"or\",\n in: \"query\",\n schema: { type: \"string\" },\n description: \"Disjunction of conditions, AND-ed with `where` and `searchString`.\",\n example: \"(status.eq.draft,status.eq.review)\"\n },\n {\n name: \"and\",\n in: \"query\",\n schema: { type: \"string\" },\n description: \"Conjunction of conditions, AND-ed with `where` and `searchString`. Ignored when `or` is also present.\",\n example: \"(views.gte.10,status.eq.draft)\"\n },\n {\n name: \"not\",\n in: \"query\",\n schema: { type: \"string\" },\n description:\n \"Negation, AND-ed with `where` and `searchString`. Negates the **conjunction** of its \"\n + \"conditions: `not(a)` is `NOT a`, `not(a,b)` is `NOT (a AND b)`. Groups nest, so \"\n + \"`not(or(a,b))` is the De Morgan case. Compiles to a real SQL `NOT (...)`, which — \"\n + \"three-valued logic — also excludes rows whose column is NULL. Ignored when `or` or \"\n + \"`and` is also present.\",\n example: \"(status.eq.draft,views.gte.10)\"\n },\n {\n name: \"include\",\n in: \"query\",\n schema: { type: \"string\" },\n description:\n \"Relations to load, in either of two spellings. **Comma-separated names or dotted \"\n + \"paths** — `author,comments.author`, up to 3 hops deep; `*` loads every relation one \"\n + \"hop deep. **JSON**, when a relation needs narrowing — \"\n + \"`{\\\"comments\\\":{\\\"limit\\\":5,\\\"where\\\":{\\\"published\\\":[\\\"==\\\",true]},\"\n + \"\\\"orderBy\\\":\\\"created_at:desc\\\",\\\"fields\\\":\\\"id,body\\\",\\\"include\\\":{\\\"author\\\":true}}}`. \"\n + \"A value starting with `{` is read as the JSON form. A name that is not a relation of \"\n + \"the collection is a 400 UNKNOWN_RELATION, not a silently missing field.\",\n example: \"author,comments.author\"\n },\n {\n name: \"fields\",\n in: \"query\",\n schema: { type: \"string\" },\n description:\n \"Comma-separated columns to return. A projection pushed into the SELECT, so a query \"\n + \"that needs two fields of a wide row reads two columns. The primary key is always \"\n + \"returned (a row that cannot be addressed cannot be updated, deleted, or paged past), \"\n + \"and `excludeFromApi` columns stay hidden whether or not they are named here. An \"\n + \"unknown column is a 400 UNKNOWN_FIELD.\",\n example: \"id,name,created_at\"\n },\n {\n name: \"distinct\",\n in: \"query\",\n schema: { type: \"boolean\" },\n description:\n \"`SELECT DISTINCT` over the returned columns. Only meaningful alongside `fields`: the \"\n + \"primary key is always in the projection, so without narrowing it every row is \"\n + \"already distinct. The response has no `meta.total` — the rows before deduplication \"\n + \"are a different set, so their count would not describe the page — and `meta.hasMore` \"\n + \"comes from reading one row past the page. Refused (400) alongside \"\n + \"`searchString` or a vector search, which attach a per-row score that makes every row \"\n + \"distinct by construction, and (400 DISTINCT_ORDER_BY_NOT_SELECTED) when `orderBy` \"\n + \"names a column `fields` does not return.\",\n example: \"true\"\n },\n {\n name: \"count\",\n in: \"query\",\n schema: { type: \"string\", enum: [\"exact\", \"none\"], default: \"exact\" },\n description:\n \"Whether to count every matching row for `meta.total`. `exact` (the default) runs a \"\n + \"`COUNT(*)` beside the page, which on a large table costs more than the page itself; \"\n + \"`none` skips it, and the response then has no `meta.total`. `meta.hasMore` is exact \"\n + \"either way — without a count it comes from reading one row past the page — so a walk \"\n + \"that pages until `hasMore` is false needs no count. Anything else is a 400 INVALID_COUNT.\",\n example: \"none\"\n },\n {\n name: \"searchString\",\n in: \"query\",\n schema: { type: \"string\" },\n description:\n \"Text search. By default a case-insensitive substring match OR-ed across the \" +\n \"collection's top-level string properties. A collection declaring a `search` block \" +\n \"gets ranked full-text matching over the fields it names, and rows carry a `_score`.\"\n },\n // Vector search has been served here since vectors landed and was\n // documented nowhere, so the only way to find it was to read the query\n // parser. All four are needed together; `vector_search` and `vector`\n // are ignored unless both are present.\n {\n name: \"vector_search\",\n in: \"query\",\n schema: { type: \"string\" },\n description: \"Name of the `vector` property to run a nearest-neighbour search against. Requires `vector`.\",\n example: \"embedding\"\n },\n {\n name: \"vector\",\n in: \"query\",\n schema: { type: \"string\" },\n description: \"The query embedding, as a JSON array of numbers. Its length must match the property's declared `dimensions`.\",\n example: \"[0.12,-0.04,0.98]\"\n },\n {\n name: \"vector_distance\",\n in: \"query\",\n schema: { type: \"string\", enum: [\"cosine\", \"l2\", \"inner_product\"], default: \"cosine\" },\n description: \"Distance function used for ordering.\"\n },\n {\n name: \"vector_threshold\",\n in: \"query\",\n schema: { type: \"number\" },\n description: \"Drop rows farther than this distance. Rows are returned closest-first with a `_distance` field. \"\n + \"The response then has no `meta.total`: counting the rows within a distance computes it for every row \"\n + \"of the table. `meta.hasMore` is exact, from reading one row past the page.\"\n }\n ];\n\n const spec: Record<string, unknown> = {\n openapi: \"3.0.3\",\n info: {\n title: \"Rebase API\",\n version: \"1.0.0\",\n description:\n \"Auto-generated REST API from Rebase collection definitions. \" +\n \"This is the same API consumed by the `@rebasepro/client` SDK.\"\n },\n servers: [\n {\n url: basePath,\n description: \"API Server\"\n }\n ],\n paths: {} as Record<string, unknown>,\n components: {\n schemas: {\n ErrorResponse: {\n type: \"object\",\n properties: {\n error: {\n type: \"object\",\n required: [\"message\", \"code\"],\n properties: {\n message: { type: \"string\" },\n code: { type: \"string\" },\n details: {}\n }\n }\n }\n },\n PaginationMeta: {\n type: \"object\",\n properties: {\n total: { type: \"integer\",\ndescription: \"Total number of matching records. Absent on a `distinct` read, which has no count of the rows it returns, on a vector search with a `vector_threshold`, whose count would compute a distance for every row, and when the request asked for `count=none`.\" },\n limit: { type: \"integer\",\ndescription: \"Page size used for this query\" },\n offset: { type: \"integer\",\ndescription: \"Number of records skipped\" },\n hasMore: { type: \"boolean\",\ndescription: \"Whether more records exist beyond this page\" },\n nextCursor: {\n type: \"string\",\n description:\n \"Opaque keyset cursor continuing this listing — pass it back as `?after=`. \"\n + \"Present when `hasMore` is true and the page returned at least one row; \"\n + \"absent on the last page and on an ordering no cursor can describe \"\n + \"(relevance, whose scores are computed per query and not stored). Do not \"\n + \"parse it: the encoding exists to be changed.\"\n }\n }\n }\n } as Record<string, unknown>,\n securitySchemes: {} as Record<string, unknown>\n },\n tags: [] as Array<{ name: string; description?: string }>\n };\n\n // ── Security Schemes ─────────────────────────────────────────────────\n if (requireAuth) {\n (spec.components as Record<string, unknown>).securitySchemes = {\n bearerAuth: {\n type: \"http\",\n scheme: \"bearer\",\n bearerFormat: \"JWT\",\n description:\n \"JWT access token obtained from `POST /auth/login` or `POST /auth/register`. \" +\n \"Can also be a static service key for server-to-server authentication.\"\n }\n // No `?token=` scheme. It was declared here — globally, so on every\n // operation — and no data route has ever accepted one: both\n // `createAuthMiddleware` and `createAdapterAuthMiddleware` read the\n // `Authorization` header and nothing else, deliberately, because\n // URLs leak into access logs, proxies, Referer headers and browser\n // history (`auth/middleware.ts`). Following it cost a caller twice:\n // unauthenticated, *and* a 400, since `token` is not in the query\n // parser's `reservedQueryKeys` and so compiles as a filter on a\n // column named `token`. `queryTokenAuth` is real but is mounted\n // only on storage file serving, for `<img src>`; if those routes\n // are ever documented, the scheme belongs on them, per-operation.\n };\n (spec as Record<string, unknown>).security = [\n { bearerAuth: [] }\n ];\n }\n\n const paths = spec.paths as Record<string, unknown>;\n const schemas = (spec.components as Record<string, unknown>).schemas as Record<string, unknown>;\n const tags = spec.tags as Array<{ name: string; description?: string }>;\n\n // The names a listing has already spent. A collection is free to have a\n // `limit` or a `fields` column, and the query parser reads those names as\n // pagination and field selection before any filter is compiled — so the\n // per-field filter could never fire, and documenting it a second time put\n // two parameters with the same (`name`, `in`) pair on one operation, which\n // is invalid OpenAPI: Swagger UI renders a duplicate and several generators\n // abort. Taken from the parameter list itself so the two cannot drift.\n //\n // `deleted` too: the parser reads it as the soft-delete parameter on every\n // collection (a value other than `include`/`only` is a 400), so a filter\n // on a column of that name never runs either.\n const reservedParameterNames = new Set([...listQueryParameters().map(p => p.name), DELETED_QUERY_PARAM]);\n\n /**\n * `?deleted=` on the reads of a `softDelete` collection. A delete there\n * moves the row to the trash, and every read hides trashed rows unless it\n * asks for them; the list, the single row, the count and the aggregate all\n * honour this. Empty for any other collection.\n */\n const deletedParameters = (collection: CollectionConfig) => {\n const field = softDeleteFieldOf(collection);\n if (!field) return [];\n return [{\n name: DELETED_QUERY_PARAM,\n in: \"query\",\n required: false,\n schema: { type: \"string\", enum: [\"include\", \"only\"] },\n description:\n `Rows a delete moved to the trash (\\`${field}\\` set) are left out unless this asks for them: ` +\n \"`include` reads them with the live rows, `only` reads them alone. Any other value is a \" +\n \"400 INVALID_DELETED_PARAM.\"\n }];\n };\n\n /**\n * `?hard=` on the deletes of a `softDelete` collection: delete for good\n * rather than move to the trash. Empty for any other collection.\n */\n const hardDeleteParameters = (collection: CollectionConfig) => {\n if (!softDeleteFieldOf(collection)) return [];\n return [{\n name: HARD_DELETE_QUERY_PARAM,\n in: \"query\",\n required: false,\n schema: { type: \"boolean\", default: false },\n description:\n \"`true` deletes for good instead of moving to the trash — a row already in the trash \" +\n \"included, which is how the trash is emptied. Only `true`/`1` and `false`/`0` are \" +\n \"accepted; anything else is a 400 INVALID_HARD_PARAM.\"\n }];\n };\n\n /** What a delete does to a `softDelete` collection's row, for the delete operations' descriptions. */\n const softDeleteBehaviour = (collection: CollectionConfig): string | undefined => {\n const field = softDeleteFieldOf(collection);\n if (!field) return undefined;\n return `This collection soft-deletes: a delete moves the row to the trash by stamping \\`${field}\\`, ` +\n \"and every read leaves it out unless it passes `?deleted=include` or `?deleted=only`. A PATCH \" +\n `setting \\`${field}\\` back to \\`null\\` restores it. Pass \\`?hard=true\\` to delete for good.`;\n };\n\n // Every component name this document will carry, known before the first\n // schema is built: a relation may point at a collection that appears later\n // in the list, or at one that is not documented here at all, and a `$ref`\n // at a component that does not exist is a document Swagger UI renders empty\n // and a strict generator refuses.\n const registeredSchemas = new Set((collections || []).map(schemaNameFor));\n\n /**\n * `Prefer: return=minimal`, on every route that would otherwise send a row\n * back. Documented rather than left implicit because a client generated\n * from this spec cannot send a header the spec does not mention.\n */\n const preferHeader = {\n name: \"Prefer\",\n in: \"header\",\n required: false,\n schema: { type: \"string\", enum: [\"return=minimal\"] },\n description:\n \"`return=minimal` asks the server not to send the written row back. Single writes then \" +\n \"answer `204 No Content`; bulk and batch writes answer `200` carrying the ids only. \" +\n \"The response repeats it in `Preference-Applied` when it was honoured.\"\n };\n\n const ifMatchHeader = {\n name: \"If-Match\",\n in: \"header\",\n required: false,\n schema: { type: \"string\" },\n description:\n \"The `ETag` this edit was made against, from the `GET` that read the row. The write is \" +\n \"refused with `412` if the row has changed since — which is the difference between \" +\n \"\\\"update the row I read\\\" and \\\"overwrite whatever is there now\\\". `*` means only that \" +\n \"the row must exist.\"\n };\n\n const preconditionFailed = {\n 412: {\n description:\n \"The row changed since the ETag in `If-Match` was issued. Nothing was written: \" +\n \"re-read the row, re-apply the change, and send the new ETag\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n }\n };\n\n const minimalResponse = {\n 204: {\n description: \"Written. `Prefer: return=minimal` was honoured, so there is no body\",\n headers: {\n \"Preference-Applied\": { schema: { type: \"string\" }, description: \"`return=minimal`\" }\n }\n }\n };\n\n // ── POST /data/_batch — writes across collections, one transaction ────\n //\n // Registered before the per-collection paths for the same reason the route\n // is: `_batch` is not a collection, and reading it as one would document a\n // table that does not exist.\n if ((collections || []).length > 0) {\n paths[\"/data/_batch\"] = {\n post: {\n tags: [\"Data\"],\n summary: \"Write across collections in one transaction\",\n description:\n \"All-or-nothing across collections — an order and its line items, a user and \" +\n \"their membership row. `/bulk` is one collection at a time, and sending the two \" +\n \"halves as separate requests is exactly the sequence that can half-succeed.\\n\\n\" +\n \"Operations run in order, each through the same pipeline its single-row route \" +\n \"uses: the same validation, callbacks and row-level security, as the same role. \" +\n \"An operation may name itself with `ref`, and a later one may stand \" +\n \"`{ \\\"$ref\\\": \\\"order.id\\\" }` wherever a value goes — in `values`, at any depth, \" +\n \"or as an `id`. Only backward references resolve.\\n\\n\" +\n \"Capped at the same number of entries as a bulk write, because one batch is one \" +\n \"transaction and holds its locks for the whole of it.\",\n operationId: \"batchWrite\",\n parameters: [\n {\n name: \"Idempotency-Key\",\n in: \"header\",\n required: false,\n schema: { type: \"string\" },\n description:\n \"Names this batch so a retry is recognised instead of repeated. Without it a \" +\n \"client that lost the response cannot tell a replay from a second batch, and \" +\n \"the whole batch is written twice.\"\n },\n preferHeader\n ],\n requestBody: {\n required: true,\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n required: [\"operations\"],\n properties: {\n operations: {\n type: \"array\",\n items: { $ref: \"#/components/schemas/BatchOperation\" }\n }\n }\n }\n }\n }\n },\n responses: {\n 200: {\n description:\n \"One entry per operation, in order: the written row for a create, update or \" +\n \"upsert, and `null` for a delete\",\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n properties: {\n data: { type: \"array\", items: { type: \"object\", nullable: true } },\n meta: { type: \"object\", properties: { operations: { type: \"integer\" } } }\n }\n }\n }\n }\n },\n 400: {\n description:\n \"Malformed body, an unknown collection or field, an illegal field operation \" +\n \"or conflict target, a forward `$ref`, or more operations than the limit. \" +\n \"Nothing was written\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n 404: {\n description: \"An `update` or `delete` names a row that does not exist; nothing was written\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n 409: {\n description: \"A request with the same Idempotency-Key is still in flight\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n 422: {\n description: \"The Idempotency-Key was already used for a different request\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n ...errorResponses(requireAuth)\n }\n }\n };\n\n schemas.FieldOperation = {\n type: \"object\",\n description:\n \"A change to the column's current value rather than the value to store. Exactly one \" +\n \"operator per field. Only on an update: an operation over a value that does not exist \" +\n \"yet is refused with a 400.\",\n properties: {\n $inc: { type: \"number\", description: \"Add to a `number` column; negative to subtract.\" },\n $push: { description: \"Append a value, or each of an array of values, to an `array` column.\" },\n $pull: { description: \"Remove every occurrence of a value from an `array` column.\" },\n $merge: { type: \"object\", description: \"Shallow-merge an object into a `map` column.\" }\n }\n };\n\n schemas.BatchOperation = {\n type: \"object\",\n required: [\"op\", \"collection\"],\n properties: {\n op: { type: \"string\", enum: [\"create\", \"update\", \"upsert\", \"delete\"] },\n collection: { type: \"string\", description: \"The collection slug this operation writes to.\" },\n id: {\n description:\n \"Required for `update` and `delete`. May instead be a reference marker — an \" +\n \"object whose single key is `$ref` and whose value is `<ref name>.<field>`, \" +\n \"e.g. `{ \\\"$ref\\\": \\\"order.id\\\" }` — naming a column of the row an earlier \" +\n \"operation wrote. (Described rather than declared as a schema: `$ref` is a \" +\n \"reserved word to every OpenAPI reader, and a property named `$ref` is read \" +\n \"as a reference and mangled.)\",\n oneOf: [{ type: \"string\" }, { type: \"integer\" }, { type: \"object\" }]\n },\n values: {\n type: \"object\",\n description:\n \"The row's fields. Values may be `$ref` markers; an `update` may also carry \" +\n \"field operations.\",\n additionalProperties: true\n },\n onConflict: {\n type: \"array\",\n items: { type: \"string\" },\n description:\n \"`upsert` only: the columns the conflict is matched on. Must carry a declared \" +\n \"uniqueness guarantee. Defaults to the primary key.\"\n },\n ref: {\n type: \"string\",\n description: \"Names this operation's result, so a later one can `$ref` its columns.\"\n }\n }\n };\n }\n\n // ── Collection routes ────────────────────────────────────────────────\n for (const collection of (collections || [])) {\n const schemaName = schemaNameFor(collection);\n const slug = collection.slug;\n\n tags.push({\n name: collection.name,\n description: collection.description || `CRUD operations for ${collection.name}`\n });\n\n // Build component schema for this collection\n schemas[schemaName] = buildCollectionSchema(collection, registeredSchemas);\n\n // Build an \"input\" schema (no read-only/auto fields like autoValue dates)\n schemas[`${schemaName}Input`] = buildCollectionInputSchema(collection);\n\n // The update body — same columns, no `required`. PATCH and PUT both\n // merge, so a field left out means \"unchanged\", not \"omitted by mistake\".\n schemas[`${schemaName}Update`] = buildCollectionUpdateSchema(collection);\n\n const dataPath = `/data/${slug}`;\n const softDeleteNote = softDeleteBehaviour(collection);\n\n // ── GET /data/{slug}/count — How many rows match ──────────────\n //\n // Served for every collection since the route existed and described\n // here for the first time. A spec is what a generated client can see,\n // so an endpoint missing from it is an endpoint that client does not\n // have — and this is the one a paginating UI needs to know how many\n // pages there are.\n //\n // Registered before the list path so its literal segment cannot be read\n // as an `{id}`, which is the same ordering the router uses.\n //\n // Its parameters are the listing's that narrow the rows — the filters,\n // the logical groups, the search and the vector search, all of which\n // the route reads (`countRawEntities`) — and not the ones that page or\n // shape them. The vector search and the groups were read and not\n // described, which left a generated client unable to count what its\n // own `find()` had asked for.\n const countNarrowing = new Set([\n \"where\", \"or\", \"and\", \"not\", \"searchString\",\n \"vector_search\", \"vector\", \"vector_distance\", \"vector_threshold\"\n ]);\n const countParameters = [\n ...listQueryParameters()\n .filter(p => countNarrowing.has(p.name))\n .map(p => p.name === \"vector_threshold\"\n ? { ...p, description: \"Count only the rows within this distance of `vector`.\" }\n : p),\n ...deletedParameters(collection),\n ...buildFilterParameters(collection, reservedParameterNames)\n ];\n const countResponses = {\n 200: {\n description: \"The number of matching rows\",\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n required: [\"count\"],\n properties: {\n count: { type: \"integer\", description: \"Rows matching the filters\" }\n }\n }\n }\n }\n },\n ...errorResponses(requireAuth)\n };\n paths[`${dataPath}/count`] = {\n get: {\n tags: [collection.name],\n summary: `Count ${collection.name}`,\n description:\n \"The number of rows the same filters would return, without returning them. \" +\n \"Takes the filter and search parameters of the list endpoint; `limit`, `offset` \" +\n \"and `orderBy` are not part of the question and are ignored.\",\n operationId: `count${schemaName}`,\n parameters: countParameters,\n responses: countResponses\n },\n // The same count, its parameters in the body — documented from\n // the GET's own list, as `POST …/query` is from the listing's.\n post: queryOperation({\n tags: [collection.name],\n summary: `Count ${collection.name}, parameters in the body`,\n description:\n \"The count `GET` answers, with its query parameters in a JSON body instead of the URL: \" +\n \"the same filters, search and vector search, the same row-level security, and the same \" +\n \"errors. It reads and never writes, so a credential needs `data:read` for it. Use it when \" +\n \"the URL would not fit: a vector search's embedding, a long `in` list. A parameter also on \" +\n \"the URL is a 400 INVALID_QUERY_BODY.\",\n operationId: `count${schemaName}Query`,\n parameters: countParameters,\n responses: countResponses,\n reserved: reservedParameterNames\n })\n };\n\n // ── GET /data/{slug}/aggregate — count/sum/avg/min/max ────────\n //\n // Before the list path for the same reason `/count` is: a literal\n // segment that a generated client would otherwise be told is an `{id}`.\n paths[`${dataPath}/aggregate`] = {\n get: {\n tags: [collection.name],\n summary: `Aggregate ${collection.name}`,\n description:\n \"Aggregate values over the rows the same filters would return. Takes the filter and \" +\n \"search parameters of the list endpoint. Row-level security applies to the rows \" +\n \"being aggregated, so a caller who can read nothing counts nothing.\",\n operationId: `aggregate${schemaName}`,\n parameters: [\n {\n name: \"select\",\n in: \"query\",\n required: true,\n description:\n \"Comma-separated aggregates, e.g. `count()`, `sum(total)`, `avg(total),max(total)`. \" +\n \"Results are keyed `count`, `sum_total`, `avg_total` and so on.\",\n schema: { type: \"string\" },\n example: \"count(),sum(total)\"\n },\n {\n name: \"groupBy\",\n in: \"query\",\n required: false,\n description: \"Comma-separated fields to group by. Each grouped field is returned alongside the aggregates.\",\n schema: { type: \"string\" },\n example: \"status\"\n },\n // `limit`, `offset` and `page` bound and page the groups,\n // exactly as they do a listing's rows.\n ...listQueryParameters().filter(p =>\n p.name === \"searchString\" || p.name === \"limit\" || p.name === \"offset\" || p.name === \"page\"),\n {\n name: \"orderBy\",\n in: \"query\",\n required: false,\n description:\n \"Sort for the groups, by a `groupBy` field or an aggregate's result key (`count`, \" +\n \"`sum_total`): `key:asc`, `key:desc` or `key:desc:last`. Groups it leaves tied are \" +\n \"ordered by the `groupBy` fields, so every page boundary falls in the same place. \" +\n \"`offset`, `page` and `orderBy` need `groupBy` — without it the result is one row — \" +\n \"and are otherwise a 400 INVALID_AGGREGATE_WINDOW.\",\n schema: { type: \"string\" },\n example: \"count:desc\"\n },\n ...deletedParameters(collection),\n ...buildFilterParameters(collection, reservedParameterNames)\n ],\n responses: {\n 200: {\n description: \"One row per group, or a single row when `groupBy` is absent\",\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n required: [\"data\"],\n properties: {\n data: {\n type: \"array\",\n items: { type: \"object\", additionalProperties: true }\n },\n meta: {\n type: \"object\",\n description: \"The page of groups. Present when `groupBy` is.\",\n properties: {\n limit: { type: \"integer\" },\n offset: { type: \"integer\" },\n hasMore: { type: \"boolean\", description: \"Whether a group follows this page.\" }\n }\n }\n }\n }\n }\n }\n },\n 501: { description: \"This backend's data driver does not implement aggregates\" },\n ...errorResponses(requireAuth)\n }\n }\n };\n\n // ── GET /data/{slug} — List entities ──────────────────────────\n const listParameters = [\n ...listQueryParameters(),\n ...deletedParameters(collection),\n ...buildFilterParameters(collection, reservedParameterNames)\n ];\n const listResponses = {\n 200: {\n description: \"Paginated list of entities\",\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n properties: {\n data: {\n type: \"array\",\n items: { $ref: `#/components/schemas/${schemaName}` }\n },\n meta: { $ref: \"#/components/schemas/PaginationMeta\" }\n }\n }\n }\n }\n },\n ...errorResponses(requireAuth)\n };\n\n // ── POST /data/{slug}/query — the same listing, parameters in the body\n //\n // Before the list path, as `/count` is. Documented from the list's own\n // parameter list, so the two cannot describe different listings.\n paths[`${dataPath}/query`] = {\n post: queryOperation({\n tags: [collection.name],\n summary: `List ${collection.name}, parameters in the body`,\n operationId: `query${schemaName}`,\n parameters: listParameters,\n responses: listResponses,\n reserved: reservedParameterNames\n })\n };\n\n paths[dataPath] = {\n get: {\n tags: [collection.name],\n summary: `List ${collection.name}`,\n operationId: `list${schemaName}`,\n parameters: listParameters,\n responses: listResponses\n },\n post: {\n tags: [collection.name],\n summary: `Create ${collection.singularName || collection.name}`,\n operationId: `create${schemaName}`,\n parameters: [\n {\n name: \"on_conflict\",\n in: \"query\",\n required: false,\n schema: { type: \"string\" },\n description:\n \"Comma-separated columns to upsert on, turning the create into \" +\n \"INSERT ... ON CONFLICT DO UPDATE. They must carry a declared uniqueness \" +\n \"guarantee — `validation.unique`, a `unique` index, or the primary key — \" +\n \"or the request is refused with a 400 naming the targets that do exist. \" +\n \"Left off, this is a plain insert and a duplicate key still raises. \" +\n \"Refused (400 `INVALID_CONFLICT_TARGET`) on the auth collection, whose rows are \" +\n \"the users: create them with a plain POST and change them with PATCH.\",\n example: \"sku\"\n },\n preferHeader\n ],\n requestBody: {\n required: true,\n content: {\n \"application/json\": {\n schema: { $ref: `#/components/schemas/${schemaName}Input` }\n }\n }\n },\n responses: {\n 201: {\n description: \"Created entity\",\n content: {\n \"application/json\": {\n schema: { $ref: `#/components/schemas/${schemaName}` }\n }\n }\n },\n ...minimalResponse,\n ...errorResponses(requireAuth)\n }\n }\n };\n\n // ── Bulk: one transaction, all-or-nothing ─────────────────────\n //\n // These went undocumented while they existed, which is the same defect\n // the update verb had: an endpoint the server serves and the spec does\n // not mention cannot be reached by a generated client at all.\n const idempotencyHeader = {\n name: \"Idempotency-Key\",\n in: \"header\",\n required: false,\n schema: { type: \"string\" },\n description:\n \"Names this write so a retry is recognised instead of repeated. Without it a \" +\n \"client that lost the response cannot distinguish a replay from a second \" +\n \"genuine batch, and the whole batch is written twice. A key names one request: \" +\n \"re-send the identical request to replay its answer, and use a new key for a \" +\n \"different one.\"\n };\n\n const bulkErrors = {\n 400: {\n description: \"Malformed body, an unknown field, or more rows than the per-batch limit\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n 409: {\n description:\n \"A request with the same Idempotency-Key is still in flight. Retry it: the \" +\n \"first attempt's result is replayed once it lands\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n 422: {\n description: \"The Idempotency-Key was already used for a different request\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n ...errorResponses(requireAuth)\n };\n\n paths[`/data/${slug}/bulk`] = {\n post: {\n tags: [collection.name],\n summary: `Create many ${collection.name} in one transaction`,\n description:\n \"All-or-nothing: if any row is rejected none of them land, and the error names \" +\n \"the offending index. Every row still runs callbacks, relations and row-level \" +\n \"security. Capped server-side because one batch holds its locks for its whole \" +\n \"duration.\",\n operationId: `createMany${schemaName}`,\n parameters: [idempotencyHeader, preferHeader],\n requestBody: {\n required: true,\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n required: [\"rows\"],\n properties: {\n rows: { type: \"array\", items: { $ref: `#/components/schemas/${schemaName}Input` } },\n upsert: {\n type: \"boolean\",\n description:\n \"Write each row as INSERT ... ON CONFLICT DO UPDATE. Refused \" +\n \"(400 `INVALID_CONFLICT_TARGET`) on the auth collection, whose rows are the users.\"\n },\n onConflict: {\n type: \"array\",\n items: { type: \"string\" },\n description:\n \"The columns the conflict is matched on, instead of the primary key. \" +\n \"They must carry a declared uniqueness guarantee. Naming them without \" +\n \"`upsert: true` is a 400 rather than a silently ignored field.\"\n }\n }\n }\n }\n }\n },\n responses: {\n 200: {\n description: \"The written rows, in the order given\",\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n properties: {\n data: { type: \"array\", items: { $ref: `#/components/schemas/${schemaName}` } },\n meta: { type: \"object\", properties: { written: { type: \"integer\" } } }\n }\n }\n }\n }\n },\n ...bulkErrors\n }\n },\n patch: {\n tags: [collection.name],\n summary: `Update many ${collection.name} in one transaction`,\n description:\n \"Each entry names its row and the fields to change. `{ id, data }` rather than \" +\n \"flat rows carrying their own key, because on a table keyed on something other \" +\n \"than `id` a flat row cannot say whether a column is the address or a value to \" +\n \"write. An id matching no row fails the batch.\",\n operationId: `updateMany${schemaName}`,\n parameters: [idempotencyHeader, preferHeader],\n requestBody: {\n required: true,\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n required: [\"updates\"],\n properties: {\n updates: {\n type: \"array\",\n items: {\n type: \"object\",\n required: [\"id\", \"data\"],\n properties: {\n id: { type: \"string\", description: \"The row to update\" },\n data: { $ref: `#/components/schemas/${schemaName}Update` }\n }\n }\n }\n }\n }\n }\n }\n },\n responses: {\n 200: {\n description: \"The updated rows, in the order given\",\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n properties: {\n data: { type: \"array\", items: { $ref: `#/components/schemas/${schemaName}` } },\n meta: { type: \"object\", properties: { written: { type: \"integer\" } } }\n }\n }\n }\n }\n },\n 404: {\n description: \"One of the ids matches no row; nothing was written\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n ...bulkErrors\n }\n }\n };\n\n paths[`/data/${slug}/bulk/delete`] = {\n post: {\n tags: [collection.name],\n summary: `Delete many ${collection.name} in one transaction`,\n description:\n \"A POST, not `DELETE /bulk` with a body. Bodies on DELETE are permitted but \" +\n \"widely dropped by proxies and CDNs, and several generators ignore \" +\n \"`requestBody` on a DELETE operation — a generated client would send the \" +\n \"request with no ids at all. Takes ids rather than a filter: a mistyped \" +\n \"condition that empties a table cannot be reviewed at the call site the way \" +\n \"an explicit list can. `beforeDelete`/`afterDelete` fire per row.\" +\n (softDeleteNote ? `\\n\\n${softDeleteNote}` : \"\"),\n operationId: `deleteMany${schemaName}`,\n parameters: [idempotencyHeader, ...hardDeleteParameters(collection)],\n requestBody: {\n required: true,\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n required: [\"ids\"],\n properties: {\n ids: {\n type: \"array\",\n items: { oneOf: [{ type: \"string\" }, { type: \"integer\" }] }\n }\n }\n }\n }\n }\n },\n responses: {\n 200: {\n description: softDeleteNote\n ? \"How many rows were moved to the trash, or deleted for good with `?hard=true`\"\n : \"How many rows were deleted\",\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n properties: {\n meta: { type: \"object\", properties: { deleted: { type: \"integer\" } } }\n }\n }\n }\n }\n },\n 404: {\n description: \"One of the ids matches no row; nothing was deleted\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n ...bulkErrors\n }\n }\n };\n\n // ── GET/PUT/DELETE /data/{slug}/{id} ──────────────────────────\n const entityPath = `/data/${slug}/{id}`;\n paths[entityPath] = {\n get: {\n tags: [collection.name],\n summary: `Get ${collection.singularName || collection.name} by ID`,\n operationId: `get${schemaName}ById`,\n parameters: [\n { name: \"id\",\nin: \"path\",\nrequired: true,\nschema: { type: \"string\" },\ndescription: \"Entity ID\" },\n // The same two parameters the list route documents, and the\n // same code serves them — one row and a page of them go\n // through one pipeline, so anything true of `include` or\n // `fields` there is true here.\n {\n name: \"include\",\n in: \"query\",\n schema: { type: \"string\" },\n description:\n \"Relations to load: comma-separated names or dotted paths \"\n + \"(`author,comments.author`, up to 3 hops), `*` for all one hop deep, or the \"\n + \"JSON form for per-relation `limit`/`where`/`orderBy`/`fields`. An unknown \"\n + \"name is a 400 UNKNOWN_RELATION.\",\n example: \"author,comments.author\"\n },\n {\n name: \"fields\",\n in: \"query\",\n schema: { type: \"string\" },\n description:\n \"Comma-separated columns to return, as a SELECT projection. The primary key \"\n + \"always survives and `excludeFromApi` columns stay hidden.\",\n example: \"id,title\"\n },\n ...deletedParameters(collection)\n ],\n responses: {\n 200: {\n description: \"Entity found\",\n headers: {\n ETag: {\n schema: { type: \"string\" },\n description:\n \"This row's version. Send it back as `If-Match` on a later PATCH or \" +\n \"DELETE to have the write refused if the row has changed in between.\"\n }\n },\n content: {\n \"application/json\": {\n schema: { $ref: `#/components/schemas/${schemaName}` }\n }\n }\n },\n 404: { description: \"Entity not found\",\ncontent: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } } },\n ...errorResponses(requireAuth)\n }\n },\n patch: updateOperation(collection, schemaName, requireAuth, {\n ifMatchHeader,\n preferHeader,\n preconditionFailed,\n minimalResponse\n }),\n delete: {\n tags: [collection.name],\n summary: `Delete ${collection.singularName || collection.name}`,\n ...(softDeleteNote ? { description: softDeleteNote } : {}),\n operationId: `delete${schemaName}`,\n parameters: [\n { name: \"id\",\nin: \"path\",\nrequired: true,\nschema: { type: \"string\" },\ndescription: \"Entity ID\" },\n ifMatchHeader,\n {\n name: \"Idempotency-Key\",\n in: \"header\",\n required: false,\n schema: { type: \"string\" },\n description:\n \"Names this delete so a retry replays its answer. A delete replayed after \" +\n \"the first attempt committed would otherwise answer 404 — which an offline \" +\n \"queue reads as a permanent failure for a delete that in fact succeeded.\"\n },\n ...hardDeleteParameters(collection)\n ],\n responses: {\n 204: {\n description: softDeleteNote\n ? \"Moved to the trash: the row still exists and can be restored. With `?hard=true`, deleted for good\"\n : \"Deleted successfully\"\n },\n 404: { description: \"Entity not found\",\ncontent: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } } },\n 409: {\n description: \"A request with the same Idempotency-Key is still in flight\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n 422: {\n description: \"The Idempotency-Key was already used for a different request\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n ...preconditionFailed,\n ...errorResponses(requireAuth)\n }\n }\n };\n\n }\n\n // ── Subcollection routes ─────────────────────────────────────────────\n //\n // A second pass, after every collection's component schema exists. These\n // routes `$ref` the *target's* schema, and the first pass builds schemas in\n // array order — so doing this inline meant a subcollection whose target\n // appeared later in the list silently degraded to an untyped `object`.\n //\n // The names come from the *resolved* relations, not from the authored\n // `relations` array. `relationName` is optional at the authoring surface —\n // it defaults to the property key, or to the target's slug — so reading the\n // raw field skipped every relation that relied on the default, and missed\n // relations declared inline on a property entirely, since those are not in\n // the array. These are the same resolved names the nested-path router\n // matches, so the spec and the routes cannot drift apart.\n //\n // A to-one relation is left out. `posts/1/author` resolves, but it\n // addresses a single row, and documenting it as a paginated list would\n // describe a response shape the client never gets.\n for (const collection of (collections || [])) {\n const slug = collection.slug;\n const schemaName = schemaNameFor(collection);\n const relations = Object.values(resolveCollectionRelations(collection))\n .filter(isToMany);\n for (const relation of relations) {\n const relationName = relation.relationName;\n const targetCollection = relation.target();\n const targetSchema = schemaNameFor(targetCollection);\n\n const subPath = `/data/${slug}/{parentId}/${relationName}`;\n const nestedParameters = [\n { name: \"parentId\",\nin: \"path\",\nrequired: true,\nschema: { type: \"string\" },\ndescription: `${collection.singularName || collection.name} ID` },\n // The nested list handler goes through the same\n // `parseQueryOptions` and the same fetch as the root\n // one, so it honours the same parameters. It documented\n // four of them.\n ...listQueryParameters()\n ];\n const nestedResponses = {\n 200: {\n description: `List of related ${relationName}`,\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n properties: {\n data: {\n type: \"array\",\n items: schemas[targetSchema]\n ? { $ref: `#/components/schemas/${targetSchema}` }\n : { type: \"object\" }\n },\n meta: { $ref: \"#/components/schemas/PaginationMeta\" }\n }\n }\n }\n }\n },\n ...errorResponses(requireAuth)\n };\n\n // Only add if the schema exists (target collection is also registered)\n paths[subPath] = {\n get: {\n tags: [collection.name],\n summary: `List ${relationName} for ${withIndefiniteArticle(collection.singularName || collection.name)}`,\n operationId: `list${schemaName}${toPascalCase(relationName)}`,\n parameters: nestedParameters,\n responses: nestedResponses\n }\n };\n paths[`${subPath}/query`] = {\n post: queryOperation({\n tags: [collection.name],\n summary: `List ${relationName} for ${withIndefiniteArticle(collection.singularName || collection.name)}, parameters in the body`,\n operationId: `query${schemaName}${toPascalCase(relationName)}`,\n parameters: nestedParameters,\n responses: nestedResponses,\n reserved: reservedParameterNames\n })\n };\n }\n }\n\n return spec;\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────────\n\n/**\n * Is this property part of the shape the document may describe?\n *\n * One exclusion, and it is the document telling the truth about what the server\n * does: `excludeFromApi` is a server-side guarantee that the column \"is\n * stripped from every row the API serves, for every caller, including admins\n * and service keys\" — `stripExcluded` in the row pipeline enforces it. A schema\n * that lists such a column describes a field that is never present, and it\n * describes it to everyone: `/docs` is mounted on the app, not on the data\n * router, so it carries none of the auth middleware `{basePath}/data` does.\n * Every project scaffolded by `rebase init` published its `users` collection's\n * `passwordHash` and `emailVerificationToken` this way.\n *\n * A `relation` property used to be excluded here too, on the reasoning that the\n * property is virtual. It is — but the row is not empty where it stands: the\n * owning side carries a foreign key under a *wire* name (`authorId`), and a\n * read that includes the relation carries the target's row under the relation's\n * own name. Excluding both left `/api/docs` describing a strictly smaller\n * collection than `generated/sdk/database.types.ts` did for the same config.\n * Relations are now emitted by {@link emitRelationProperties}, under the same\n * keys and in the same order as the SDK's `Row`; the direct-property loops skip\n * them so the two passes cannot emit one key twice.\n *\n * Written as one predicate rather than a `continue` per loop because it kept\n * being fixed in one loop at a time: this is the same rule the SDK generator\n * applies to its `Row` type (`packages/codegen/src/generate-types.ts`).\n */\nfunction isDocumentedProperty(property: Property, direction: \"read\" | \"write\" = \"read\"): boolean {\n const access = effectiveAccess(property);\n return access?.[direction]?.length !== 0;\n}\n\n/**\n * The `x-rebase-access` annotation, or nothing for a field with no rule.\n *\n * A vendor extension rather than a schema keyword because OpenAPI has no way to\n * say \"this property is present for some callers\" — `readOnly` is about the\n * direction of a field, not about who. Generators ignore what they do not know,\n * so a client built from this spec still compiles; a *human* reading `/docs`, or\n * a gateway that wants to enforce the same rule at the edge, gets the role lists\n * verbatim. Emitted for the role case only: a field closed to everybody is\n * absent from the document entirely, which is a stronger statement.\n */\nfunction accessAnnotation(property: Property): Record<string, unknown> | undefined {\n const access = effectiveAccess(property);\n if (!access) return undefined;\n const annotation: Record<string, unknown> = {};\n if (access.read !== undefined) annotation.read = [...access.read];\n if (access.write !== undefined) annotation.write = [...access.write];\n return Object.keys(annotation).length > 0 ? annotation : undefined;\n}\n\n/** The sentence `x-rebase-access` deserves in prose, for a reader of `/docs`. */\nfunction accessDescription(property: Property): string | undefined {\n const access = effectiveAccess(property);\n if (!access) return undefined;\n const parts: string[] = [];\n const phrase = (roles: readonly string[]) =>\n roles.length === 0 ? \"nobody through the API\" : roles.map(r => `\\`${r}\\``).join(\", \") + \" (and `admin`)\";\n if (access.read !== undefined) parts.push(`readable by ${phrase(access.read)}`);\n if (access.write !== undefined) parts.push(`writable by ${phrase(access.write)}`);\n if (parts.length === 0) return undefined;\n return `Field access: ${parts.join(\"; \")}. A caller without the role does not receive the field at all — it is absent, not null.`;\n}\n\n/**\n * The keys `stripExcluded` deletes: the property name *and* its column name.\n *\n * Seeding the emitted set with both is what stops a foreign key derived from a\n * relation putting an `excludeFromApi` column back on the surface under its\n * other name. Same pair, same reason, as `excludedApiKeys` in the SDK\n * generator.\n */\nfunction excludedApiKeys(collection: CollectionConfig, direction: \"read\" | \"write\" = \"read\"): Set<string> {\n const excluded = new Set<string>();\n for (const [key, property] of Object.entries(collection.properties ?? {})) {\n if (isDocumentedProperty(property as Property, direction)) continue;\n excluded.add(key);\n const columnName = (property as { columnName?: unknown }).columnName;\n if (typeof columnName === \"string\") excluded.add(columnName);\n }\n return excluded;\n}\n\n/**\n * The collection's primary key, as the key it is addressed by on the wire and\n * the property that declares it.\n *\n * `id` was a literal in three places — seeded before the read loop, assigned\n * after the input loop, inherited by the update schema from the input one — and\n * the two spellings disagreed: a declared `id: { type: \"number\" }` overwrote\n * the read seed and was overwritten by the input assignment, so the same field\n * was `integer` in `Post` and `string` in `PostInput`. One helper now answers\n * the question for all three.\n */\nfunction idPropertyEntry(collection: CollectionConfig | undefined): [string, Property] | undefined {\n for (const [key, property] of Object.entries(collection?.properties ?? {})) {\n if ((property as Record<string, unknown>)?.isId) return [key, property as Property];\n }\n return undefined;\n}\n\n/**\n * The schema of a primary key or of a foreign key pointing at one: the declared\n * property's own type, stripped of the field-level facts (description,\n * validation bounds) that belong to the column and not to a reference to it.\n *\n * Falls back to `string` for a collection that declares no primary key, which\n * is what every schema here assumed unconditionally before.\n */\nfunction idSchemaFor(collection: CollectionConfig | undefined): Record<string, unknown> {\n const declared = idPropertyEntry(collection);\n if (!declared) return { type: \"string\" };\n const converted = convertPropertyToSchema(declared[1]);\n const schema: Record<string, unknown> = { type: converted.type ?? \"string\" };\n if (converted.format) schema.format = converted.format;\n return schema;\n}\n\n/**\n * Relations resolve or they do not; a document is still owed for a collection\n * whose target thunk throws (a circular import, usually). The SDK generator\n * warns and carries on with no relation fields, and so does this — the\n * alternative is `/api/docs` 500ing for the whole project.\n */\nfunction resolveRelationsForDocument(collection: CollectionConfig): Record<string, ResolvedRelation> {\n try {\n return resolveCollectionRelations(collection);\n } catch {\n return {};\n }\n}\n\n/** Unwrap a target handed back as a module namespace — `() => import(\"./authors\")`. */\nfunction relationTarget(relation: ResolvedRelation): CollectionConfig | undefined {\n try {\n let target = relation.target() as CollectionConfig & { default?: CollectionConfig; __esModule?: boolean };\n if (target && (target.default || target.__esModule)) {\n target = (target.default ?? target) as typeof target;\n }\n return target;\n } catch {\n return undefined;\n }\n}\n\n/**\n * The schema an *included* relation arrives as: the target's own row.\n *\n * `$ref` when the target is one of the collections this document describes, and\n * an open object when it is not — a dangling pointer makes Swagger UI render an\n * empty model and makes a strict generator abort, which is worse than a vague\n * one.\n */\nfunction includedRelationSchema(\n relation: ResolvedRelation,\n registeredSchemas: ReadonlySet<string>\n): Record<string, unknown> {\n const target = relationTarget(relation);\n const targetSchema = target ? schemaNameFor(target) : undefined;\n const item: Record<string, unknown> = targetSchema && registeredSchemas.has(targetSchema)\n ? { $ref: `#/components/schemas/${targetSchema}` }\n : { type: \"object\" };\n return relation.cardinality === \"many\" ? { type: \"array\", items: item } : item;\n}\n\n/**\n * Emit a collection's relations onto a read schema: the foreign keys first,\n * then the relations themselves.\n *\n * The order and the keys are the SDK `Row`'s, deliberately — the parity test\n * next door compares the two key sets, and the only way that stays true is for\n * both to be derived the same way rather than kept in step by hand.\n *\n * A `belongsTo` reaches the wire under the *field* name of its local column\n * (`author_id` → `authorId`), which is what `fieldKeyForColumn` answers; a\n * relation addressed by the same name as its own foreign key is served as\n * either the scalar or the nested row depending on `include`, so it is\n * documented as both.\n */\nfunction emitRelationProperties(\n collection: CollectionConfig,\n properties: Record<string, unknown>,\n required: string[],\n emitted: Set<string>,\n registeredSchemas: ReadonlySet<string>\n): void {\n const resolved = resolveRelationsForDocument(collection);\n\n for (const [relationKey, relation] of Object.entries(resolved)) {\n if (relation.kind !== \"belongsTo\" || !relation.localKey) continue;\n const fieldKey = fieldKeyForColumn(collection, relation.localKey);\n if (emitted.has(fieldKey)) continue;\n\n const foreignKey = idSchemaFor(relationTarget(relation));\n const shadowedByInclude = relationKey === fieldKey;\n properties[fieldKey] = shadowedByInclude\n ? { oneOf: [foreignKey, includedRelationSchema(relation, registeredSchemas)] }\n : { ...foreignKey, description: `Foreign key into \\`${relation.targetSlug}\\`` };\n emitted.add(fieldKey);\n\n if (isRelationRequired(collection, relation) && !shadowedByInclude) required.push(fieldKey);\n }\n\n for (const [key, relation] of Object.entries(resolved)) {\n if (emitted.has(key)) continue;\n properties[key] = includedRelationSchema(relation, registeredSchemas);\n emitted.add(key);\n }\n\n // A `relation` property whose relation did not resolve. Still a field of the\n // row, just not a precisely describable one.\n for (const [key, property] of Object.entries(collection.properties ?? {})) {\n if ((property as Property)?.type !== \"relation\") continue;\n if (emitted.has(key)) continue;\n properties[key] = { type: \"object\" };\n emitted.add(key);\n }\n}\n\n/**\n * Build the component schema for a collection (output / read shape).\n *\n * Every declared property except the ones {@link isDocumentedProperty} rules\n * out, plus the foreign keys and relations {@link emitRelationProperties} adds.\n */\nexport function buildCollectionSchema(\n collection: CollectionConfig,\n registeredSchemas: ReadonlySet<string>\n): Record<string, unknown> {\n const idKey = idPropertyEntry(collection)?.[0] ?? \"id\";\n const properties: Record<string, unknown> = {\n [idKey]: { ...idSchemaFor(collection), description: \"Unique identifier\" }\n };\n const required: string[] = [idKey];\n const excluded = excludedApiKeys(collection);\n const emitted = new Set<string>(excluded);\n emitted.add(idKey);\n\n for (const [key, property] of Object.entries(collection.properties)) {\n if (property.type === \"relation\") continue;\n if (!isDocumentedProperty(property)) continue;\n\n properties[key] = convertPropertyToSchema(property);\n emitted.add(key);\n\n if (property.validation?.required && key !== idKey) {\n required.push(key);\n }\n }\n\n emitRelationProperties(collection, properties, required, emitted, registeredSchemas);\n annotateTenantField(collection, properties, \"read\");\n\n return {\n type: \"object\",\n required: required.length > 0 ? required : undefined,\n properties\n };\n}\n\n/**\n * Mark the tenant field, on whichever schema is being built.\n *\n * A vendor extension for the same reason `x-rebase-access` is one: OpenAPI has\n * no keyword for \"the server fills this in from who you are, and refuses a\n * value that is not yours\". `readOnly` is the closest and it is wrong — the\n * field *is* writable, by a caller sending their own tenant, and a bypass role\n * may send any. Generators ignore what they do not know, so a client built from\n * this document still compiles; a human reading `/docs`, or a gateway wanting\n * to enforce the same boundary at the edge, learns the field is special and\n * why.\n *\n * Applied after the property loops rather than inside `convertPropertyToSchema`\n * because tenancy is a fact about the *collection*, and that function is handed\n * a property with no idea which collection it came from.\n */\nfunction annotateTenantField(\n collection: CollectionConfig,\n properties: Record<string, unknown>,\n direction: \"read\" | \"write\"\n): void {\n const tenant = getTenantConfig(collection);\n if (!tenant) return;\n const schema = properties[tenant.field];\n if (!schema || typeof schema !== \"object\") return;\n\n const existing = (schema as { description?: unknown }).description;\n const sentence = direction === \"write\"\n ? \"The tenant this row belongs to. Omit it and the server stamps the tenant you are calling as; \" +\n \"send another tenant's and the write is refused with `TENANT_MISMATCH`. It cannot be changed \" +\n \"on an update (`TENANT_IMMUTABLE`).\"\n : \"The tenant this row belongs to. Rows of other tenants are not returned at all.\";\n\n properties[tenant.field] = {\n ...(schema as Record<string, unknown>),\n description: typeof existing === \"string\" && existing ? `${existing} — ${sentence}` : sentence,\n \"x-rebase-tenant\": true\n };\n}\n\n/**\n * The PATCH/PUT operation for `/data/{slug}/{id}`.\n *\n * Split out because both verbs serve it and they must not drift: the update\n * body is a **partial**, and describing it with the create schema was the bug\n * this replaces. `<Name>Input` marks every `validation.required` property as\n * required — correct for POST, wrong for an update, where omitting a field\n * means \"leave it alone\" rather than \"I forgot it\". A client generated from\n * that spec demanded fields the server does not, and a spec-validating gateway\n * would have rejected partial updates the server accepts.\n */\nfunction updateOperation(\n collection: CollectionConfig,\n schemaName: string,\n requireAuth: boolean,\n shared: {\n ifMatchHeader: Record<string, unknown>;\n preferHeader: Record<string, unknown>;\n preconditionFailed: Record<string, unknown>;\n minimalResponse: Record<string, unknown>;\n }\n): Record<string, unknown> {\n return {\n tags: [collection.name],\n summary: `Update ${collection.singularName || collection.name}`,\n description:\n \"Partial update: only the properties present in the body are written; the rest are left \" +\n \"unchanged.\\n\\n\" +\n \"A property's value may instead be a field operation — `{ \\\"views\\\": { \\\"$inc\\\": 1 } }`, \" +\n \"`{ \\\"tags\\\": { \\\"$push\\\": \\\"new\\\" } }`, `{ \\\"tags\\\": { \\\"$pull\\\": \\\"old\\\" } }`, \" +\n \"`{ \\\"meta\\\": { \\\"$merge\\\": { \\\"seen\\\": true } } }` — which is applied inside the \" +\n \"statement holding the row lock. That is the difference between a counter that is correct \" +\n \"under concurrency and one that silently loses increments, because expressing the same \" +\n \"change as a value means reading it first. `$inc` needs a `number` property, `$push`/`$pull` \" +\n \"an `array`, `$merge` a `map`; anything else is a 400. Pushed elements and merged keys answer \" +\n \"to the property's own rules, as a value would. See the `FieldOperation` schema.\",\n operationId: `update${schemaName}`,\n parameters: [\n { name: \"id\", in: \"path\", required: true, schema: { type: \"string\" }, description: \"Entity ID\" },\n shared.ifMatchHeader,\n shared.preferHeader,\n {\n name: \"Idempotency-Key\",\n in: \"header\",\n required: false,\n schema: { type: \"string\" },\n description:\n \"Names this update so a retry replays its answer instead of applying the edit \" +\n \"again. A PATCH is not naturally idempotent — a field operation emphatically is \" +\n \"not — so a retry after a lost response applies it twice.\"\n }\n ],\n requestBody: {\n required: true,\n content: {\n \"application/json\": {\n schema: { $ref: `#/components/schemas/${schemaName}Update` }\n }\n }\n },\n responses: {\n 200: {\n description: \"Updated entity\",\n headers: {\n ETag: {\n schema: { type: \"string\" },\n description:\n \"The version this write produced — the tag a `GET` of the row now answers. Send \" +\n \"it as `If-Match` on the next write to chain conditional edits without reading \" +\n \"the row back. Absent on an answer replayed from an `Idempotency-Key`.\"\n }\n },\n content: {\n \"application/json\": {\n schema: { $ref: `#/components/schemas/${schemaName}` }\n }\n }\n },\n 404: {\n description: \"Entity not found\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n 409: {\n description: \"A request with the same Idempotency-Key is still in flight\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n 422: {\n description: \"The Idempotency-Key was already used for a different request\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n ...shared.preconditionFailed,\n ...shared.minimalResponse,\n ...errorResponses(requireAuth)\n }\n };\n}\n\n/**\n * The update body: the create schema with `required` dropped.\n *\n * Derived rather than rebuilt so the two cannot describe different columns —\n * the only difference between creating and updating is which fields you must\n * supply, and that is exactly the one thing removed here.\n */\nfunction buildCollectionUpdateSchema(collection: CollectionConfig): Record<string, unknown> {\n const { required: _required, ...rest } = buildCollectionInputSchema(collection);\n return rest;\n}\n\n/**\n * Build an input schema (for POST/PUT) — excludes auto-generated fields.\n *\n * `excludeFromApi` columns are left out too, and the server now agrees: a write\n * naming one is refused (`write-validation.ts`), which is what the flag's name\n * and the generated SDK's own documentation always said. This comment used to\n * record that such a write was \"still accepted\" — the document was right and\n * the server was the thing that had not caught up.\n */\nexport function buildCollectionInputSchema(collection: CollectionConfig): Record<string, unknown> {\n const properties: Record<string, unknown> = {};\n const required: string[] = [];\n // The write half, not the read half: a field readable by nobody but\n // writable by an `admin` belongs in the input body and not in the row, and\n // the two schemas used to share one exclusion set and so got both wrong.\n const excluded = excludedApiKeys(collection, \"write\");\n const emitted = new Set<string>(excluded);\n const idKey = idPropertyEntry(collection)?.[0] ?? \"id\";\n\n for (const [key, property] of Object.entries(collection.properties)) {\n if (property.type === \"relation\") continue;\n if (!isDocumentedProperty(property, \"write\")) continue;\n\n // Skip auto-value date fields from the input schema\n if (property.type === \"date\" && property.autoValue) continue;\n\n // Skip auto-generated ID fields\n if (\"isId\" in property && property.isId && property.isId !== \"manual\" && property.isId !== true) continue;\n\n properties[key] = convertPropertyToSchema(property);\n emitted.add(key);\n\n // A column the database fills may be left out of a create.\n if (property.validation?.required && !property.databaseDefault) {\n required.push(key);\n }\n }\n\n // Allow explicit ID for create (optional). Typed from the declared primary\n // key rather than as a `string` literal: a serial `id` was `integer` on the\n // read schema and `string` here, for the same column, in every document the\n // generator has ever produced.\n if (!emitted.has(idKey)) {\n properties[idKey] = {\n ...idSchemaFor(collection),\n description: \"Optional: client-assigned ID. If omitted, the server generates one.\"\n };\n emitted.add(idKey);\n }\n\n // The two ways a write may name a `belongsTo` target, both of which the\n // server accepts and the generated SDK's `Insert` already offered: the\n // foreign key under its own wire name (`authorId`), and the relation\n // property (`author`), which the write transformer maps onto that column.\n // Neither reached the document, so the spec described a create that could\n // not set a relation at all.\n emitWritableRelations(collection, properties, emitted);\n annotateTenantField(collection, properties, \"write\");\n\n // The tenant field is never required on input, whatever the property\n // declares: the column is NOT NULL, and the server is what fills it.\n // Listing it would tell every generated client to demand a value its caller\n // is not supposed to compute.\n const tenantField = getTenantConfig(collection)?.field;\n const requiredOnInput = tenantField ? required.filter(key => key !== tenantField) : required;\n\n return {\n type: \"object\",\n required: requiredOnInput.length > 0 ? requiredOnInput : undefined,\n properties\n };\n}\n\n/**\n * The writable half of a collection's relations: `belongsTo` only.\n *\n * A to-many is not writable through the body — the server links rows through\n * the nested routes — so offering `tags: [...]` on a create would describe a\n * write that does nothing. Same rule, same reason, as `emitWritableRelations`\n * in the SDK generator, whose `Insert` type this mirrors key for key.\n *\n * Neither spelling is listed in `required`, even for a relation the collection\n * declares `validation: { required: true }` on: the two keys are alternatives,\n * and a schema naming both would tell a spec-validating gateway to reject a\n * create that the server accepts. (The SDK's `Insert` marks both non-optional\n * for the same relation, which is the same fact stated less carefully; a\n * document is the half that a gateway enforces.)\n */\nfunction emitWritableRelations(\n collection: CollectionConfig,\n properties: Record<string, unknown>,\n emitted: Set<string>\n): void {\n const resolved = resolveRelationsForDocument(collection);\n\n const emit = (key: string, relation: ResolvedRelation): void => {\n if (emitted.has(key)) return;\n properties[key] = {\n ...idSchemaFor(relationTarget(relation)),\n description: `The \\`${relation.targetSlug}\\` row this belongs to.`\n };\n emitted.add(key);\n };\n\n for (const relation of Object.values(resolved)) {\n if (relation.kind === \"belongsTo\" && relation.localKey) {\n emit(fieldKeyForColumn(collection, relation.localKey), relation);\n }\n }\n\n for (const [key, property] of Object.entries(collection.properties ?? {})) {\n if ((property as Property)?.type !== \"relation\") continue;\n const relation = findRelation(resolved, key);\n if (relation?.kind === \"belongsTo\" && relation.localKey) emit(key, relation);\n }\n}\n\n/**\n * Convert a Rebase Property to an OpenAPI 3.0 schema object.\n */\nfunction convertPropertyToSchema(property: Property): Record<string, unknown> {\n const schema = convertPropertyTypeToSchema(property);\n const annotation = accessAnnotation(property);\n if (!annotation) return schema;\n\n const sentence = accessDescription(property);\n return {\n ...schema,\n ...(sentence\n ? { description: schema.description ? `${schema.description} — ${sentence}` : sentence }\n : {}),\n \"x-rebase-access\": annotation\n };\n}\n\n/** The JSON Schema a property's *type* compiles to, before any access annotation. */\nfunction convertPropertyTypeToSchema(property: Property): Record<string, unknown> {\n const base: Record<string, unknown> = {};\n\n if (property.name) {\n base.description = property.name;\n }\n\n switch (property.type) {\n case \"string\": {\n const sp = property as StringProperty;\n base.type = \"string\";\n\n if (sp.enum) {\n const enumValues = resolveEnumValues(sp.enum);\n if (enumValues.length > 0) {\n base.enum = enumValues;\n }\n }\n\n if (sp.validation) {\n if (sp.validation.min !== undefined) base.minLength = sp.validation.min;\n if (sp.validation.max !== undefined) base.maxLength = sp.validation.max;\n if (sp.validation.length !== undefined) {\n base.minLength = sp.validation.length;\n base.maxLength = sp.validation.length;\n }\n if (sp.validation.matches !== undefined) {\n base.pattern = String(sp.validation.matches);\n }\n }\n\n if (sp.email) base.format = \"email\";\n if (sp.url) base.format = \"uri\";\n if (sp.storage) base.format = \"uri\";\n\n return base;\n }\n\n case \"number\": {\n const np = property as NumberProperty;\n // `isId` is on this list because the DDL generator puts it there:\n // a numeric primary key is `INTEGER GENERATED BY DEFAULT AS\n // IDENTITY` for `\"increment\"` and `INTEGER` for every other form\n // (`generate-postgres-ddl-logic.ts`), so the column the scaffold's\n // `posts.id` creates is an integer and the document called it a\n // `number` — which lets a generated client send `1.5` for a row id.\n const isInteger = np.validation?.integer\n || Boolean(np.isId)\n || np.columnType === \"integer\" || np.columnType === \"serial\"\n || np.columnType === \"bigserial\" || np.columnType === \"bigint\";\n base.type = isInteger ? \"integer\" : \"number\";\n\n if (np.enum) {\n const enumValues = resolveEnumValues(np.enum);\n if (enumValues.length > 0) {\n base.enum = enumValues;\n }\n }\n\n if (np.validation) {\n if (np.validation.min !== undefined) base.minimum = np.validation.min;\n if (np.validation.max !== undefined) base.maximum = np.validation.max;\n if (np.validation.moreThan !== undefined) {\n base.minimum = np.validation.moreThan;\n base.exclusiveMinimum = true;\n }\n if (np.validation.lessThan !== undefined) {\n base.maximum = np.validation.lessThan;\n base.exclusiveMaximum = true;\n }\n }\n\n return base;\n }\n\n case \"boolean\":\n base.type = \"boolean\";\n return base;\n\n case \"date\": {\n base.type = \"string\";\n if (property.mode === \"date\") {\n base.format = \"date\";\n } else {\n base.format = \"date-time\";\n }\n if (property.autoValue) {\n base.readOnly = true;\n base.description = (base.description || \"\") +\n (property.autoValue === \"on_create\" ? \" (Auto-set on creation)\" : \" (Auto-updated)\");\n }\n return base;\n }\n\n case \"geopoint\":\n base.type = \"object\";\n base.properties = {\n latitude: { type: \"number\" },\n longitude: { type: \"number\" }\n };\n base.required = [\"latitude\", \"longitude\"];\n return base;\n\n case \"reference\":\n base.type = \"string\";\n base.description = (base.description || \"\") + \" (Reference ID)\";\n return base;\n\n case \"array\": {\n const ap = property as ArrayProperty;\n base.type = \"array\";\n\n if (ap.oneOf) {\n // Discriminated union (e.g., content blocks)\n const typeField = ap.oneOf.typeField || \"type\";\n const valueField = ap.oneOf.valueField || \"value\";\n const variants: Record<string, unknown>[] = [];\n\n for (const [variantKey, variantProp] of Object.entries(ap.oneOf.properties)) {\n variants.push({\n type: \"object\",\n properties: {\n [typeField]: { type: \"string\",\nenum: [variantKey] },\n [valueField]: convertPropertyToSchema(variantProp)\n },\n required: [typeField, valueField]\n });\n }\n\n base.items = { oneOf: variants };\n } else if (ap.of) {\n if (Array.isArray(ap.of)) {\n base.items = { oneOf: ap.of.map(p => convertPropertyToSchema(p)) };\n } else {\n base.items = convertPropertyToSchema(ap.of);\n }\n } else {\n base.items = {};\n }\n\n if (ap.validation) {\n if (ap.validation.min !== undefined) base.minItems = ap.validation.min;\n if (ap.validation.max !== undefined) base.maxItems = ap.validation.max;\n }\n\n return base;\n }\n\n case \"map\": {\n const mp = property as MapProperty;\n base.type = \"object\";\n\n if (mp.properties) {\n const props: Record<string, unknown> = {};\n const req: string[] = [];\n\n for (const [key, subProp] of Object.entries(mp.properties)) {\n props[key] = convertPropertyToSchema(subProp);\n if (subProp.validation?.required) {\n req.push(key);\n }\n }\n\n base.properties = props;\n if (req.length > 0) base.required = req;\n } else if (mp.keyValue) {\n base.additionalProperties = true;\n }\n\n return base;\n }\n\n case \"vector\": {\n const vp = property as VectorProperty;\n base.type = \"array\";\n base.items = { type: \"number\" };\n base.description = (base.description || \"\") + ` (Vector(${vp.dimensions}))`;\n return base;\n }\n case \"binary\": {\n base.type = \"string\";\n base.description = (base.description || \"\") + \" (Binary/Base64)\";\n return base;\n }\n default:\n base.type = \"string\";\n return base;\n }\n}\n\n/**\n * Resolve EnumValues (array or record) into a flat array of enum values.\n */\nfunction resolveEnumValues(enumDef: Record<string | number, unknown> | Array<{ id: string | number }>): Array<string | number> {\n if (Array.isArray(enumDef)) {\n return enumDef.map(e => (typeof e === \"object\" && e !== null && \"id\" in e) ? e.id : e as string | number);\n }\n return Object.keys(enumDef).map(k => {\n // Preserve numeric keys as numbers\n const num = Number(k);\n return isNaN(num) ? k : num;\n });\n}\n\n/**\n * Build PostgREST-style filter parameters for a collection.\n * These are additional query parameters like `?status=eq.active&price=gte.100`.\n *\n * `excludeFromApi` columns are not offered: the server does filter on them, and\n * that is exactly the problem — a filter on a column no response can contain\n * answers questions about the value one row at a time, which is a worse\n * disclosure than the column name alone.\n */\nfunction buildFilterParameters(\n collection: CollectionConfig,\n reservedNames: ReadonlySet<string> = new Set()\n): Array<Record<string, unknown>> {\n const params: Array<Record<string, unknown>> = [];\n\n for (const [key, property] of Object.entries(collection.properties)) {\n if (!isDocumentedProperty(property)) continue;\n // A `relation` property is not a column, so there is nothing to compare\n // against. The foreign key beside it is filterable and is still not\n // offered here — a separate gap from the schema one, and one the query\n // layer has to answer first.\n if (property.type === \"relation\") continue;\n if (property.type === \"map\" || property.type === \"array\" || property.type === \"geopoint\") {\n continue;\n }\n // A column whose name a list parameter already owns is unfilterable\n // over the wire — see `reservedParameterNames`.\n if (reservedNames.has(key)) continue;\n\n params.push({\n name: key,\n in: \"query\",\n required: false,\n schema: { type: \"string\" },\n description:\n `Filter by \\`${key}\\`. Supports PostgREST operators: ` +\n \"`eq.value`, `neq.value`, `gt.value`, `gte.value`, `lt.value`, `lte.value`, \" +\n \"`in.(a,b,c)`, `nin.(a,b,c)`, `cs.value` (array-contains), `csa.(a,b)` (array-contains-any). \" +\n \"Plain values imply equality.\",\n example: property.type === \"string\" ? \"eq.active\" : property.type === \"number\" ? \"gte.100\" : undefined\n });\n }\n\n return params;\n}\n\n/**\n * A `POST …/query` operation: the listing whose `parameters` are given, with\n * its query parameters moved into a JSON body.\n *\n * Built from the `GET`'s own parameter list, so the body documents exactly\n * what the route accepts — it turns the body back into those parameters and\n * runs the `GET`'s parser (`query-body.ts`). A filter parameter takes a list\n * where the URL would repeat it, and the four parameters whose URL value is\n * JSON take the JSON value itself.\n */\nfunction queryOperation(operation: {\n tags: string[];\n summary: string;\n /** What the operation answers; the listing's description when unset. */\n description?: string;\n operationId: string;\n parameters: Array<Record<string, unknown>>;\n responses: Record<string, unknown>;\n /** The listing's own parameter names; every other query parameter is a per-field filter. */\n reserved: ReadonlySet<string>;\n}): Record<string, unknown> {\n const jsonValued: Record<string, Record<string, unknown>> = {\n where: { oneOf: [{ type: \"object\", additionalProperties: true }, { type: \"string\" }] },\n include: { oneOf: [{ type: \"object\", additionalProperties: true }, { type: \"string\" }] },\n orderBy: { oneOf: [{ type: \"array\", items: {} }, { type: \"string\" }] },\n vector: { oneOf: [{ type: \"array\", items: { type: \"number\" } }, { type: \"string\" }] }\n };\n const properties: Record<string, unknown> = {};\n for (const parameter of operation.parameters) {\n if (parameter.in !== \"query\") continue;\n const name = String(parameter.name);\n const schema = parameter.schema as Record<string, unknown>;\n const isFilter = !operation.reserved.has(name);\n properties[name] = {\n ...(jsonValued[name]\n ?? (isFilter ? { oneOf: [schema, { type: \"array\", items: schema }] } : schema)),\n ...(parameter.description ? { description: parameter.description } : {})\n };\n }\n return {\n tags: operation.tags,\n summary: operation.summary,\n description: operation.description ??\n \"The listing `GET` answers, with its query parameters in a JSON body instead of the URL: \" +\n \"the same filters, page, count, cursor and includes, the same row-level security, and the \" +\n \"same errors. It reads and never writes, so a credential needs `data:read` for it. Use it \" +\n \"when the URL would not fit: a vector search's embedding is a JSON array in `vector`, and \" +\n \"768 dimensions of it is 17 KB of URL, past what Node and most proxies accept. A parameter \" +\n \"also on the URL is a 400 INVALID_QUERY_BODY.\",\n operationId: operation.operationId,\n parameters: operation.parameters.filter(parameter => parameter.in !== \"query\"),\n requestBody: {\n required: true,\n content: {\n \"application/json\": {\n schema: { type: \"object\", properties }\n }\n }\n },\n responses: operation.responses\n };\n}\n\n/**\n * Standard error responses included on every endpoint.\n */\nfunction errorResponses(requireAuth: boolean): Record<string, unknown> {\n const responses: Record<string, unknown> = {\n 400: {\n description: \"Bad request\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n 500: {\n description: \"Internal server error\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n }\n };\n\n if (requireAuth) {\n responses[401] = {\n description: \"Authentication required or invalid token\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n };\n responses[403] = {\n description: \"Insufficient permissions\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n };\n }\n\n return responses;\n}\n\n/**\n * Prefix a noun with \"a\" or \"an\" based on its leading sound.\n */\nfunction withIndefiniteArticle(noun: string): string {\n return `${/^[aeiou]/i.test(noun) ? \"an\" : \"a\"} ${noun}`;\n}\n\n/**\n * The component-schema name for a collection — and the stem of every\n * `operationId` and `$ref` that mentions it.\n *\n * `toPascalCase` keeps ASCII letters and digits and nothing else, so a name\n * written in a script that has none of them — a Cyrillic or Japanese\n * `singularName`, which the docs' six locales make ordinary rather than exotic\n * — reduced to the empty string. The schema was then stored under `\"\"` and\n * every reference to it read `#/components/schemas/`, an unresolvable pointer:\n * Swagger UI renders the model empty and a strict generator fails outright. So\n * fall through the names until one survives, and keep a constant as the floor.\n *\n * Two collections whose names PascalCase identically still share one component;\n * that needs a disambiguation rule, not a fallback.\n */\nfunction schemaNameFor(collection: CollectionConfig): string {\n return toPascalCase(collection.singularName || \"\")\n || toPascalCase(collection.name || \"\")\n || toPascalCase(collection.slug || \"\")\n || \"Collection\";\n}\n\n/**\n * Convert a string to PascalCase for schema names.\n */\nfunction toPascalCase(str: string): string {\n return str\n .replace(/[^a-zA-Z0-9]+/g, \" \")\n .split(\" \")\n .filter(Boolean)\n .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(\"\");\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"proxy-BEKbsil6.js","names":[],"sources":["../src/functions/proxy.ts"],"sourcesContent":["/**\n * Forwarding `/api/functions/*` to the process that serves it.\n *\n * A split deployment normally puts a reverse proxy in front and routes the path\n * there. This exists so it does not have to: set `REBASE_FUNCTIONS_UPSTREAM` on\n * the API process and the two-container topology presents the identical URL\n * surface as the one-container one, with nothing new to install and nothing for\n * a client or an SDK to know about.\n *\n * It is a transport hop, not a trust boundary. Nothing is authenticated or\n * authorised here: the upstream runs the same auth middleware against the same\n * `JWT_SECRET` and must see the original `Authorization` header, so re-deciding\n * anything on the way through would put two answers where the system has one.\n */\nimport { Hono, type Context } from \"hono\";\nimport { getConnInfo } from \"@hono/node-server/conninfo\";\nimport type { HonoEnv } from \"../api/types\";\nimport { logger } from \"../utils/logger\";\n\n/**\n * Headers that describe *this* connection rather than the request, and so must\n * not be copied onto a new one.\n *\n * `host` is the important one: forwarded unchanged it names the API process, and\n * an upstream doing any virtual-host routing would answer for the wrong service.\n * `content-length` and `transfer-encoding` are recomputed by `fetch` from the\n * body it is actually given, and a stale value is a hang or a truncation.\n */\nconst HOP_BY_HOP = new Set([\n \"connection\",\n \"keep-alive\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"te\",\n \"trailer\",\n \"transfer-encoding\",\n \"upgrade\",\n \"host\",\n \"content-length\"\n]);\n\n/**\n * Additionally dropped from the *response*, because `fetch` has already acted on\n * them by the time we see the body.\n *\n * `Content-Encoding` is the one that matters and the one that is easy to miss:\n * undici decompresses a `gzip` response transparently, but leaves the header on\n * the `Response` object. Copy it onto the body we hand back and the client is\n * told to gunzip bytes that are already plain — which does not fail cleanly. It\n * hangs, or it surfaces as a decode error several layers away from the proxy\n * that caused it. (This is not hypothetical: the runtime compresses its own\n * responses, so *every* forwarded response carries the header.)\n */\nconst RESPONSE_ONLY_DROP = new Set([\"content-encoding\", \"content-length\"]);\n\nexport interface FunctionsProxyOptions {\n /** Base URL of the process serving the functions, e.g. `http://functions:8080`. */\n upstream: string;\n /**\n * The mount path being forwarded, e.g. `/api/functions`. Omitted, it is the\n * `mountPath` the functions group recorded for the request — the path the\n * application mounted it at — so the upstream is asked for the same path.\n */\n basePath?: string;\n}\n\n/**\n * A router that forwards everything under its mount point to `upstream`.\n *\n * Method, path, query string, body and headers travel verbatim, with two\n * deliberate exceptions:\n *\n * 1. Hop-by-hop headers are dropped ({@link HOP_BY_HOP}).\n * 2. `X-Forwarded-For` gains the address this process saw the request come from.\n * This is not decoration — the upstream's rate limiters and the auth routes'\n * IP logging read that chain, and without the append every forwarded request\n * arrives from one address: the API pod's. Every caller would share a single\n * rate-limit bucket and every login would be logged from the same IP.\n *\n * **The upstream must therefore count one more proxy hop than the API process\n * does.** `TRUSTED_PROXY_HOPS` on the functions process is the API's value\n * plus one. Left unset it defaults to 0, and the upstream falls back to the\n * socket address — again, the API pod's — which is safe but useless.\n */\nexport function createFunctionsProxy(options: FunctionsProxyOptions): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n // Trailing slash trimmed once, here: joining it per request is how a proxy\n // ends up asking for `//functions/x`, which some servers route and some 404.\n const upstream = options.upstream.replace(/\\/+$/, \"\");\n\n router.all(\"/*\", async (c) => {\n const incoming = new URL(c.req.url);\n const basePath = options.basePath ?? c.get(\"mountPath\") ?? \"\";\n // The path *below* the mount point. Hono gives the full request path, so\n // taking the suffix is what keeps the upstream's own mount identical:\n // `/api/functions/send-invoice` here becomes `/api/functions/send-invoice`\n // there, not `/api/functions/api/functions/send-invoice`.\n const suffix = incoming.pathname.startsWith(basePath)\n ? incoming.pathname.slice(basePath.length)\n : incoming.pathname;\n const target = `${upstream}${basePath}${suffix}${incoming.search}`;\n\n const headers = new Headers();\n for (const [name, value] of c.req.raw.headers) {\n if (!HOP_BY_HOP.has(name.toLowerCase())) headers.set(name, value);\n }\n\n const seenFrom = socketAddress(c);\n if (seenFrom) {\n const chain = c.req.header(\"x-forwarded-for\");\n headers.set(\"x-forwarded-for\", chain ? `${chain}, ${seenFrom}` : seenFrom);\n }\n\n try {\n const response = await fetch(target, {\n method: c.req.method,\n headers,\n // GET and HEAD may not carry one; everything else streams\n // through rather than being buffered, so a large upload does not\n // have to fit in this process's memory on its way past.\n body: c.req.method === \"GET\" || c.req.method === \"HEAD\" ? undefined : c.req.raw.body,\n // Required by undici whenever the body is a stream. Without it\n // the fetch rejects with a message about duplex that says\n // nothing about what to do.\n duplex: \"half\",\n redirect: \"manual\"\n } as RequestInit);\n\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers: responseHeaders(response.headers)\n });\n } catch (err) {\n // The upstream being down is an infrastructure fact, not a bug in\n // the caller's request, and 502 is the one status that says so.\n // Named explicitly in the body because the alternative — a generic\n // 500 from the API process — sends people reading the wrong logs.\n logger.error(\"Functions upstream unreachable\", {\n upstream,\n path: incoming.pathname,\n error: err instanceof Error ? err : new Error(String(err))\n });\n return c.json({\n error: {\n code: \"FUNCTIONS_UPSTREAM_UNREACHABLE\",\n message: \"The process serving custom functions could not be reached from this one.\"\n }\n }, 502);\n }\n });\n\n return router;\n}\n\n/**\n * Response headers, minus the ones that describe the upstream's connection and\n * the ones `fetch` has already consumed on our behalf.\n */\nfunction responseHeaders(headers: Headers): Headers {\n const out = new Headers();\n for (const [name, value] of headers) {\n const lower = name.toLowerCase();\n if (HOP_BY_HOP.has(lower) || RESPONSE_ONLY_DROP.has(lower)) continue;\n out.set(name, value);\n }\n return out;\n}\n\n/** The address this process saw, or undefined on a runtime that cannot say. */\nfunction socketAddress(c: Context<HonoEnv>): string | undefined {\n try {\n return getConnInfo(c).remote.address;\n } catch {\n return undefined;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,IAAM,6BAAa,IAAI,IAAI;CACvB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ,CAAC;;;;;;;;;;;;;AAcD,IAAM,qCAAqB,IAAI,IAAI,CAAC,oBAAoB,gBAAgB,CAAC;;;;;;;;;;;;;;;;;;;AA+BzE,SAAgB,qBAAqB,SAA+C;CAChF,MAAM,SAAS,IAAI,KAAc;CAGjC,MAAM,WAAW,QAAQ,SAAS,QAAQ,QAAQ,EAAE;CAEpD,OAAO,IAAI,MAAM,OAAO,MAAM;EAC1B,MAAM,WAAW,IAAI,IAAI,EAAE,IAAI,GAAG;EAClC,MAAM,WAAW,QAAQ,YAAY,EAAE,IAAI,WAAW,KAAK;EAK3D,MAAM,SAAS,SAAS,SAAS,WAAW,QAAQ,IAC9C,SAAS,SAAS,MAAM,SAAS,MAAM,IACvC,SAAS;EACf,MAAM,SAAS,GAAG,WAAW,WAAW,SAAS,SAAS;EAE1D,MAAM,UAAU,IAAI,QAAQ;EAC5B,KAAK,MAAM,CAAC,MAAM,UAAU,EAAE,IAAI,IAAI,SAClC,IAAI,CAAC,WAAW,IAAI,KAAK,YAAY,CAAC,GAAG,QAAQ,IAAI,MAAM,KAAK;EAGpE,MAAM,WAAW,cAAc,CAAC;EAChC,IAAI,UAAU;GACV,MAAM,QAAQ,EAAE,IAAI,OAAO,iBAAiB;GAC5C,QAAQ,IAAI,mBAAmB,QAAQ,GAAG,MAAM,IAAI,aAAa,QAAQ;EAC7E;EAEA,IAAI;GACA,MAAM,WAAW,MAAM,MAAM,QAAQ;IACjC,QAAQ,EAAE,IAAI;IACd;IAIA,MAAM,EAAE,IAAI,WAAW,SAAS,EAAE,IAAI,WAAW,SAAS,KAAA,IAAY,EAAE,IAAI,IAAI;IAIhF,QAAQ;IACR,UAAU;GACd,CAAgB;GAEhB,OAAO,IAAI,SAAS,SAAS,MAAM;IAC/B,QAAQ,SAAS;IACjB,YAAY,SAAS;IACrB,SAAS,gBAAgB,SAAS,OAAO;GAC7C,CAAC;EACL,SAAS,KAAK;GAKV,OAAO,MAAM,kCAAkC;IAC3C;IACA,MAAM,SAAS;IACf,OAAO,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;GAC7D,CAAC;GACD,OAAO,EAAE,KAAK,EACV,OAAO;IACH,MAAM;IACN,SAAS;GACb,EACJ,GAAG,GAAG;EACV;CACJ,CAAC;CAED,OAAO;AACX;;;;;AAMA,SAAS,gBAAgB,SAA2B;CAChD,MAAM,MAAM,IAAI,QAAQ;CACxB,KAAK,MAAM,CAAC,MAAM,UAAU,SAAS;EACjC,MAAM,QAAQ,KAAK,YAAY;EAC/B,IAAI,WAAW,IAAI,KAAK,KAAK,mBAAmB,IAAI,KAAK,GAAG;EAC5D,IAAI,IAAI,MAAM,KAAK;CACvB;CACA,OAAO;AACX;;AAGA,SAAS,cAAc,GAAyC;CAC5D,IAAI;EACA,OAAO,YAAY,CAAC,CAAC,CAAC,OAAO;CACjC,QAAQ;EACJ;CACJ;AACJ"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"request-timeout-CRny5KD0.js","names":[],"sources":["../src/functions/request-timeout.ts"],"sourcesContent":["import type { MiddlewareHandler } from \"hono\";\nimport type { HonoEnv } from \"../api/types\";\nimport { logger } from \"../utils/logger\";\nimport { ApiError, errorHandler } from \"../api/errors\";\n\n/** Default ceiling for a custom function request, in milliseconds. */\nexport const DEFAULT_FUNCTIONS_TIMEOUT_MS = 30_000;\n\n/**\n * Resolve the functions request timeout from config, then env, then the default.\n *\n * `0` (or any non-positive number) disables the ceiling — for a deployment\n * whose proxy already imposes one, or a function that legitimately streams for\n * minutes.\n */\nexport function resolveFunctionsTimeoutMs(configured?: number): number {\n if (typeof configured === \"number\" && Number.isFinite(configured)) {\n return Math.max(0, Math.floor(configured));\n }\n // Blank means unset, not zero. `Number(\"\")` and `Number(\" \")` are both 0,\n // and 0 here means \"no ceiling\" — so a compose file with\n // `REBASE_FUNCTIONS_TIMEOUT_MS=${SOMETHING}` and `SOMETHING` undefined, or\n // a `.env` line with the name and no value, silently switched off the one\n // bound on how long code the framework did not write may hold a socket.\n // Declaring a variable without setting it is the ordinary way to write\n // both of those files, and the failure is invisible: nothing logs, and the\n // deployment behaves exactly as it did before the ceiling existed.\n const raw = process.env.REBASE_FUNCTIONS_TIMEOUT_MS?.trim();\n if (raw) {\n const fromEnv = Number(raw);\n if (Number.isFinite(fromEnv) && fromEnv >= 0) {\n return Math.floor(fromEnv);\n }\n }\n return DEFAULT_FUNCTIONS_TIMEOUT_MS;\n}\n\n/**\n * A per-request ceiling for the custom functions router.\n *\n * Custom functions are the one router that runs code the framework did not\n * write, and nothing else in the stack bounds how long that code takes: the\n * Node server is constructed without `requestTimeout`/`headersTimeout`, so a\n * handler awaiting a promise that never settles — a `fetch` to an unreachable\n * third party with no `AbortSignal`, a query on a wedged connection — holds its\n * socket and its request object until the client gives up. On the managed\n * runtime the process is shared between tenants, so that is not only the slow\n * caller's problem.\n *\n * The handler is **not** killed — JavaScript cannot stop a function from the\n * outside. What is bounded is the client-visible request: after `ms` the caller\n * gets a 504 and the socket is released, and the handler's eventual result is\n * dropped. The handler is *told*, though: `getSignal(c)` is aborted at the same\n * moment, so a `fetch` or a driver call it was passed to stops too. It is a\n * ceiling, not a kill switch, and the exact number matters far less than its\n * existence.\n *\n * \"The handler keeps running\" is a **Node** guarantee, not a property of the\n * contract. It follows from the process outliving the request, which is not\n * true on an isolate-based host: there, work still in flight when the response\n * resolves is terminated rather than orphaned. So a handler must not depend on\n * finishing after its 504 — anything that has to complete belongs in\n * `waitUntil()`, which is the one construct both hosts honour. See\n * `./wait-until.ts`.\n *\n * Mounted in front of the auth middleware rather than behind it, so a wedged\n * driver — the failure that also hangs `withAuth()` — is covered too.\n */\nexport function createFunctionsRequestTimeout(ms: number): MiddlewareHandler<HonoEnv> {\n return async (c, next) => {\n // Set either way, so `getSignal(c)` hands every handler a signal it can\n // pass on without checking whether a ceiling is configured.\n const controller = new AbortController();\n c.set(\"functionSignal\", controller.signal);\n if (ms <= 0) return next();\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timedOut = new Promise<\"timeout\">((resolve) => {\n timer = setTimeout(() => {\n // Settled first, so the race is decided before a handler that\n // listens on the signal can return in reaction to it; then the\n // abort, so that handler — or a `fetch` it was handed to —\n // stops with the request.\n resolve(\"timeout\");\n controller.abort(new Error(`Function timed out after ${ms}ms`));\n }, ms);\n });\n\n try {\n const outcome = await Promise.race([next().then(() => \"done\" as const), timedOut]);\n if (outcome === \"timeout\") {\n logger.warn(\n `[functions] ${c.req.method} ${c.req.path} exceeded the ${ms}ms request timeout — ` +\n \"answering 504. Its signal was aborted; work not handed `getSignal(c)` keeps running. \" +\n \"Pass it to outbound calls, or raise `functionsTimeoutMs` / REBASE_FUNCTIONS_TIMEOUT_MS.\"\n );\n // Through `errorHandler`, not `c.json`: that is what puts the\n // `requestId` in the body and hands the outcome to the request\n // log, so the Studio Logs entry for a timeout carries\n // `errorCode` like every other failure. Hand-built, it was the\n // one 504 nobody could join to a log line.\n return errorHandler(\n new ApiError(504, \"FUNCTION_TIMEOUT\", \"Function timed out\"),\n c\n ) as Response;\n }\n } finally {\n if (timer) clearTimeout(timer);\n }\n return undefined;\n };\n}\n"],"mappings":";;;;;;;;;;;;;;AAMA,IAAa,+BAA+B;;;;;;;;AAS5C,SAAgB,0BAA0B,YAA6B;CACnE,IAAI,OAAO,eAAe,YAAY,OAAO,SAAS,UAAU,GAC5D,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,UAAU,CAAC;CAU7C,MAAM,MAAM,QAAQ,IAAI,6BAA6B,KAAK;CAC1D,IAAI,KAAK;EACL,MAAM,UAAU,OAAO,GAAG;EAC1B,IAAI,OAAO,SAAS,OAAO,KAAK,WAAW,GACvC,OAAO,KAAK,MAAM,OAAO;CAEjC;CACA,OAAO;AACX;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,SAAgB,8BAA8B,IAAwC;CAClF,OAAO,OAAO,GAAG,SAAS;EAGtB,MAAM,aAAa,IAAI,gBAAgB;EACvC,EAAE,IAAI,kBAAkB,WAAW,MAAM;EACzC,IAAI,MAAM,GAAG,OAAO,KAAK;EAEzB,IAAI;EACJ,MAAM,WAAW,IAAI,SAAoB,YAAY;GACjD,QAAQ,iBAAiB;IAKrB,QAAQ,SAAS;IACjB,WAAW,sBAAM,IAAI,MAAM,4BAA4B,GAAG,GAAG,CAAC;GAClE,GAAG,EAAE;EACT,CAAC;EAED,IAAI;GAEA,IAAI,MADkB,QAAQ,KAAK,CAAC,KAAK,CAAC,CAAC,WAAW,MAAe,GAAG,QAAQ,CAAC,MACjE,WAAW;IACvB,OAAO,KACH,eAAe,EAAE,IAAI,OAAO,GAAG,EAAE,IAAI,KAAK,gBAAgB,GAAG,sMAGjE;IAMA,OAAO,aACH,IAAI,SAAS,KAAK,oBAAoB,oBAAoB,GAC1D,CACJ;GACJ;EACJ,UAAU;GACN,IAAI,OAAO,aAAa,KAAK;EACjC;CAEJ;AACJ"}
|