@rebasepro/server 0.25.0 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/{GCSStorageController-C7ARrDAp.js → GCSStorageController-CRd9bFrc.js} +3 -3
  2. package/dist/{GCSStorageController-C7ARrDAp.js.map → GCSStorageController-CRd9bFrc.js.map} +1 -1
  3. package/dist/{S3StorageController-qevJ0pVE.js → S3StorageController-D00EFa5P.js} +3 -3
  4. package/dist/{S3StorageController-qevJ0pVE.js.map → S3StorageController-D00EFa5P.js.map} +1 -1
  5. package/dist/api/closed-database.d.ts +25 -0
  6. package/dist/{auth-CteGWxsL.js → auth-Dkvo2tTJ.js} +26 -18
  7. package/dist/auth-Dkvo2tTJ.js.map +1 -0
  8. package/dist/backend/boot-announcements.d.ts +13 -0
  9. package/dist/backend/create-backend.d.ts +4 -0
  10. package/dist/{backup-Dq0lyJ_D.js → backup-B9m3cb3X.js} +7 -7
  11. package/dist/{backup-Dq0lyJ_D.js.map → backup-B9m3cb3X.js.map} +1 -1
  12. package/dist/{contract-routes-DiFwiLg2.js → contract-routes-CsFXW7j4.js} +2 -2
  13. package/dist/{contract-routes-DiFwiLg2.js.map → contract-routes-CsFXW7j4.js.map} +1 -1
  14. package/dist/{cron-loader-NHqRfKF0.js → cron-loader-Dj6lA4HQ.js} +2 -2
  15. package/dist/{cron-loader-NHqRfKF0.js.map → cron-loader-Dj6lA4HQ.js.map} +1 -1
  16. package/dist/{cron-routes-Dhf82HzT.js → cron-routes-CwbLZguO.js} +5 -5
  17. package/dist/{cron-routes-Dhf82HzT.js.map → cron-routes-CwbLZguO.js.map} +1 -1
  18. package/dist/{cron-scheduler-D1DT0nw5.js → cron-scheduler-B04ACoUE.js} +2 -2
  19. package/dist/{cron-scheduler-D1DT0nw5.js.map → cron-scheduler-B04ACoUE.js.map} +1 -1
  20. package/dist/{cron-store-DEpVstUU.js → cron-store-DkyfuLOH.js} +3 -3
  21. package/dist/{cron-store-DEpVstUU.js.map → cron-store-DkyfuLOH.js.map} +1 -1
  22. package/dist/{ddl-bootstrap-BNqi2NZe.js → ddl-bootstrap-Ck20GSIX.js} +2 -2
  23. package/dist/{ddl-bootstrap-BNqi2NZe.js.map → ddl-bootstrap-Ck20GSIX.js.map} +1 -1
  24. package/dist/{errors-_ytoGi8A.js → errors-OMWkKSH-.js} +66 -5
  25. package/dist/errors-OMWkKSH-.js.map +1 -0
  26. package/dist/{function-loader-DbCSoNJj.js → function-loader-KKDqTcIr.js} +2 -2
  27. package/dist/{function-loader-DbCSoNJj.js.map → function-loader-KKDqTcIr.js.map} +1 -1
  28. package/dist/{function-routes-DNV4RUF0.js → function-routes-RxrCHNuM.js} +2 -2
  29. package/dist/{function-routes-DNV4RUF0.js.map → function-routes-RxrCHNuM.js.map} +1 -1
  30. package/dist/functions/index.js +18 -1
  31. package/dist/functions/index.js.map +1 -1
  32. package/dist/{history-recorder-RZ1Ow4YY.js → history-recorder-B2rkuhuy.js} +2 -2
  33. package/dist/{history-recorder-RZ1Ow4YY.js.map → history-recorder-B2rkuhuy.js.map} +1 -1
  34. package/dist/index.es.js +529 -113
  35. package/dist/index.es.js.map +1 -1
  36. package/dist/{jobs-8G-_agld.js → jobs-h1WRq3cp.js} +3 -3
  37. package/dist/{jobs-8G-_agld.js.map → jobs-h1WRq3cp.js.map} +1 -1
  38. package/dist/{jwt-YhciR93H.js → jwt-D3oFauZI.js} +2 -2
  39. package/dist/{jwt-YhciR93H.js.map → jwt-D3oFauZI.js.map} +1 -1
  40. package/dist/{logger-Bt32sHFw.js → logger-CW8pmTS6.js} +35 -3
  41. package/dist/logger-CW8pmTS6.js.map +1 -0
  42. package/dist/{logs-routes-CelTiS7C.js → logs-routes-CBt7mMIL.js} +3 -3
  43. package/dist/{logs-routes-CelTiS7C.js.map → logs-routes-CBt7mMIL.js.map} +1 -1
  44. package/dist/{openapi-generator-BamY6-ga.js → openapi-generator-BjHr6upl.js} +2 -2
  45. package/dist/{openapi-generator-BamY6-ga.js.map → openapi-generator-BjHr6upl.js.map} +1 -1
  46. package/dist/{proxy-BEKbsil6.js → proxy-CH8Jsovg.js} +2 -2
  47. package/dist/{proxy-BEKbsil6.js.map → proxy-CH8Jsovg.js.map} +1 -1
  48. package/dist/{query-parser-CGohjyWI.js → query-parser-otFZqJsC.js} +3 -3
  49. package/dist/{query-parser-CGohjyWI.js.map → query-parser-otFZqJsC.js.map} +1 -1
  50. package/dist/{request-timeout-CRny5KD0.js → request-timeout-59crS-9u.js} +3 -3
  51. package/dist/{request-timeout-CRny5KD0.js.map → request-timeout-59crS-9u.js.map} +1 -1
  52. package/dist/{schema-editor-routes-CnNGCayU.js → schema-editor-routes-Bsut0bCC.js} +2 -2
  53. package/dist/{schema-editor-routes-CnNGCayU.js.map → schema-editor-routes-Bsut0bCC.js.map} +1 -1
  54. package/dist/{soft-delete-params-BAgpmBvp.js → soft-delete-params-BmlVwecC.js} +2 -2
  55. package/dist/{soft-delete-params-BAgpmBvp.js.map → soft-delete-params-BmlVwecC.js.map} +1 -1
  56. package/dist/{sql-export-routes-BgTJ7K3S.js → sql-export-routes-DmB_uZ4l.js} +3 -3
  57. package/dist/{sql-export-routes-BgTJ7K3S.js.map → sql-export-routes-DmB_uZ4l.js.map} +1 -1
  58. package/dist/{upload-validation-I9cCUBIW.js → upload-validation-Bgo626uY.js} +2 -2
  59. package/dist/{upload-validation-I9cCUBIW.js.map → upload-validation-Bgo626uY.js.map} +1 -1
  60. package/dist/utils/dev-dependency-command.d.ts +15 -0
  61. package/dist/utils/dev-port.d.ts +8 -0
  62. package/dist/utils/logger.d.ts +24 -0
  63. package/package.json +5 -5
  64. package/dist/auth-CteGWxsL.js.map +0 -1
  65. package/dist/errors-_ytoGi8A.js.map +0 -1
  66. package/dist/logger-Bt32sHFw.js.map +0 -1
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"request-timeout-59crS-9u.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"}
@@ -4,7 +4,7 @@ globalThis.process ??= __rebaseProcess;
4
4
  __rebaseCreateRequire(import.meta.url);
5
5
  import "./src-D5Y3N-S4.js";
6
6
  import { i as isCollectionPatch, n as collectionPatchProblems } from "./collection_patch-BRu-BvDv.js";
7
- import { i as errorHandler, t as ApiError } from "./errors-_ytoGi8A.js";
7
+ import { i as errorHandler, t as ApiError } from "./errors-OMWkKSH-.js";
8
8
  import { AstSchemaEditor } from "./ast-schema-editor-mCeSC03x.js";
9
9
  import { Hono } from "hono";
10
10
  import { z } from "zod";
@@ -133,4 +133,4 @@ function createSchemaEditorRoutes(source) {
133
133
  //#endregion
134
134
  export { createSchemaEditorRoutes, createSchemaSourceEditor };
135
135
 
136
- //# sourceMappingURL=schema-editor-routes-CnNGCayU.js.map
136
+ //# sourceMappingURL=schema-editor-routes-Bsut0bCC.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema-editor-routes-CnNGCayU.js","names":[],"sources":["../src/schema-edit/source-editor.ts","../src/api/schema-editor-routes.ts"],"sourcesContent":["import type { AstSchemaEditor } from \"../api/ast-schema-editor\";\n\n/**\n * The process's editor of the collection source files, one edit at a time.\n *\n * Two routers rewrite those files: the source-only editor\n * (`/api/schema-editor/*`) and live schema editing's `/apply`. Both go through\n * this one {@link AstSchemaEditor}, so the ts-morph project over the\n * collections directory is parsed once per process rather than once per\n * apply — 180–250 ms of blocked event loop at 200 collection files, on every\n * save. The editor re-reads the disk at the start of each edit\n * (`syncWithDisk`): a file changed since — by the IDE, by a commit being put\n * back — is refreshed, a new one is added, a deleted one is dropped.\n *\n * One project cannot take two edits at once. An edit holds unsaved changes in\n * memory across `await project.save()`, and a second edit arriving meanwhile\n * would refresh those files from disk under it, or save them with its own.\n * So every edit, from either router, waits in one queue for the edits before\n * it to settle — a refused one included — and anything an edit does with the\n * files it wrote (reading them back for a commit) belongs inside the same\n * {@link run}, before the next edit can touch them.\n */\nexport class SchemaSourceEditor {\n private settled: Promise<unknown> = Promise.resolve();\n\n constructor(private readonly editor: AstSchemaEditor) {\n }\n\n /** Run `edit` once every edit queued before it has finished. */\n run<T>(edit: (editor: AstSchemaEditor) => Promise<T>): Promise<T> {\n const result = this.settled.then(() => edit(this.editor));\n this.settled = result.then(() => undefined, () => undefined);\n return result;\n }\n}\n","import { Hono } from \"hono\";\nimport { z } from \"zod\";\nimport { collectionPatchProblems, isCollectionPatch } from \"@rebasepro/types\";\nimport { AstSchemaEditor } from \"./ast-schema-editor\";\nimport { SchemaSourceEditor } from \"../schema-edit/source-editor\";\nimport { ApiError, errorHandler } from \"./errors\";\nimport { HonoEnv } from \"./types\";\n\n/**\n * Rewriting collection source from the admin panel.\n *\n * Every refusal in `AstSchemaEditor` is written for the person who will read it\n * — \"Collection X has no file at …\", \"Relation Y has no target collection. Pick\n * one before saving.\" — and every one of them was a plain `Error`, which\n * `errorHandler` treats as an unexpected failure: 500, and a body that says\n * \"Internal Server Error\". The messages never left the server. They are 400s\n * now, so the panel can show what it was told.\n */\nfunction refusalsAsBadRequest<T>(run: () => Promise<T>): Promise<T> {\n return run().catch((error: unknown) => {\n if (error instanceof ApiError) throw error;\n throw ApiError.badRequest(\n error instanceof Error ? error.message : String(error),\n \"SCHEMA_EDIT_REFUSED\"\n );\n });\n}\n\n/** The identifier half of every payload here, checked once. */\nconst collectionIdSchema = z.string().min(1, \"`collectionId` is required\");\nconst propertyKeySchema = z.string().min(1, \"`propertyKey` is required\");\n\nconst propertySaveSchema = z.object({\n collectionId: collectionIdSchema,\n propertyKey: propertyKeySchema,\n propertyConfig: z.record(z.string(), z.unknown())\n});\nconst propertyDeleteSchema = z.object({\n collectionId: collectionIdSchema,\n propertyKey: propertyKeySchema\n});\nconst collectionSaveSchema = z.object({\n collectionId: collectionIdSchema,\n collectionData: z.record(z.string(), z.unknown()).optional(),\n /** What changed about an existing collection — see `collection_patch.ts`. */\n patch: z.array(z.unknown()).optional(),\n partial: z.boolean().optional()\n}).refine(\n save => (save.collectionData === undefined) !== (save.patch === undefined),\n { message: \"send `patch` (what changed) or `collectionData` (a new collection), not both\" }\n);\nconst collectionDeleteSchema = z.object({\n collectionId: collectionIdSchema\n});\n\n/** Parse a body against a schema, or 400 naming the field. */\nasync function body<S extends z.ZodType>(c: { req: { json: () => Promise<unknown> } }, schema: S): Promise<z.infer<S>> {\n const raw = await c.req.json().catch(() => undefined);\n const parsed = schema.safeParse(raw);\n if (!parsed.success) {\n throw ApiError.badRequest(\n parsed.error.issues.map(i => `${i.path.join(\".\") || \"body\"}: ${i.message}`).join(\"; \"),\n \"INVALID_INPUT\"\n );\n }\n return parsed.data;\n}\n\n/**\n * The editor for `collectionsDir`: one per process, shared by these routes\n * and live schema editing's `/apply`. See {@link SchemaSourceEditor}.\n *\n * Here rather than beside the class because building it loads `ts-morph`, an\n * optional peer dependency this module is imported to probe for.\n */\nexport function createSchemaSourceEditor(collectionsDir: string): SchemaSourceEditor {\n return new SchemaSourceEditor(new AstSchemaEditor(collectionsDir));\n}\n\nexport function createSchemaEditorRoutes(source: SchemaSourceEditor): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n router.onError(errorHandler);\n\n router.post(\"/property/save\", async (c) => {\n const { collectionId, propertyKey, propertyConfig } = await body(c, propertySaveSchema);\n await refusalsAsBadRequest(() => source.run(editor => editor.saveProperty(collectionId, propertyKey, propertyConfig)));\n return c.json({ success: true });\n });\n\n router.post(\"/property/delete\", async (c) => {\n const { collectionId, propertyKey } = await body(c, propertyDeleteSchema);\n await refusalsAsBadRequest(() => source.run(editor => editor.deleteProperty(collectionId, propertyKey)));\n return c.json({ success: true });\n });\n\n /**\n * `partial: true` means \"this payload is what changed\", not \"this is the\n * collection\". Without it a one-key patch — which is what adding a column\n * posts — is read as a whole-collection save and deletes everything it does\n * not mention, `securityRules` included. Absent, it defaults to a full save,\n * so an older panel keeps the behaviour it was written against.\n */\n router.post(\"/collection/save\", async (c) => {\n const { collectionId, collectionData, patch, partial } = await body(c, collectionSaveSchema);\n if (patch !== undefined && !isCollectionPatch(patch)) {\n throw ApiError.badRequest(collectionPatchProblems(patch).join(\" \"), \"INVALID_INPUT\");\n }\n await refusalsAsBadRequest(() => source.run(editor => patch\n ? editor.applyPatch(collectionId, patch)\n : editor.saveCollection(collectionId, collectionData ?? {}, { partial: partial === true })));\n return c.json({ success: true });\n });\n\n router.post(\"/collection/delete\", async (c) => {\n const { collectionId } = await body(c, collectionDeleteSchema);\n await refusalsAsBadRequest(() => source.run(editor => editor.deleteCollection(collectionId)));\n return c.json({ success: true });\n });\n\n return router;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"schema-editor-routes-Bsut0bCC.js","names":[],"sources":["../src/schema-edit/source-editor.ts","../src/api/schema-editor-routes.ts"],"sourcesContent":["import type { AstSchemaEditor } from \"../api/ast-schema-editor\";\n\n/**\n * The process's editor of the collection source files, one edit at a time.\n *\n * Two routers rewrite those files: the source-only editor\n * (`/api/schema-editor/*`) and live schema editing's `/apply`. Both go through\n * this one {@link AstSchemaEditor}, so the ts-morph project over the\n * collections directory is parsed once per process rather than once per\n * apply — 180–250 ms of blocked event loop at 200 collection files, on every\n * save. The editor re-reads the disk at the start of each edit\n * (`syncWithDisk`): a file changed since — by the IDE, by a commit being put\n * back — is refreshed, a new one is added, a deleted one is dropped.\n *\n * One project cannot take two edits at once. An edit holds unsaved changes in\n * memory across `await project.save()`, and a second edit arriving meanwhile\n * would refresh those files from disk under it, or save them with its own.\n * So every edit, from either router, waits in one queue for the edits before\n * it to settle — a refused one included — and anything an edit does with the\n * files it wrote (reading them back for a commit) belongs inside the same\n * {@link run}, before the next edit can touch them.\n */\nexport class SchemaSourceEditor {\n private settled: Promise<unknown> = Promise.resolve();\n\n constructor(private readonly editor: AstSchemaEditor) {\n }\n\n /** Run `edit` once every edit queued before it has finished. */\n run<T>(edit: (editor: AstSchemaEditor) => Promise<T>): Promise<T> {\n const result = this.settled.then(() => edit(this.editor));\n this.settled = result.then(() => undefined, () => undefined);\n return result;\n }\n}\n","import { Hono } from \"hono\";\nimport { z } from \"zod\";\nimport { collectionPatchProblems, isCollectionPatch } from \"@rebasepro/types\";\nimport { AstSchemaEditor } from \"./ast-schema-editor\";\nimport { SchemaSourceEditor } from \"../schema-edit/source-editor\";\nimport { ApiError, errorHandler } from \"./errors\";\nimport { HonoEnv } from \"./types\";\n\n/**\n * Rewriting collection source from the admin panel.\n *\n * Every refusal in `AstSchemaEditor` is written for the person who will read it\n * — \"Collection X has no file at …\", \"Relation Y has no target collection. Pick\n * one before saving.\" — and every one of them was a plain `Error`, which\n * `errorHandler` treats as an unexpected failure: 500, and a body that says\n * \"Internal Server Error\". The messages never left the server. They are 400s\n * now, so the panel can show what it was told.\n */\nfunction refusalsAsBadRequest<T>(run: () => Promise<T>): Promise<T> {\n return run().catch((error: unknown) => {\n if (error instanceof ApiError) throw error;\n throw ApiError.badRequest(\n error instanceof Error ? error.message : String(error),\n \"SCHEMA_EDIT_REFUSED\"\n );\n });\n}\n\n/** The identifier half of every payload here, checked once. */\nconst collectionIdSchema = z.string().min(1, \"`collectionId` is required\");\nconst propertyKeySchema = z.string().min(1, \"`propertyKey` is required\");\n\nconst propertySaveSchema = z.object({\n collectionId: collectionIdSchema,\n propertyKey: propertyKeySchema,\n propertyConfig: z.record(z.string(), z.unknown())\n});\nconst propertyDeleteSchema = z.object({\n collectionId: collectionIdSchema,\n propertyKey: propertyKeySchema\n});\nconst collectionSaveSchema = z.object({\n collectionId: collectionIdSchema,\n collectionData: z.record(z.string(), z.unknown()).optional(),\n /** What changed about an existing collection — see `collection_patch.ts`. */\n patch: z.array(z.unknown()).optional(),\n partial: z.boolean().optional()\n}).refine(\n save => (save.collectionData === undefined) !== (save.patch === undefined),\n { message: \"send `patch` (what changed) or `collectionData` (a new collection), not both\" }\n);\nconst collectionDeleteSchema = z.object({\n collectionId: collectionIdSchema\n});\n\n/** Parse a body against a schema, or 400 naming the field. */\nasync function body<S extends z.ZodType>(c: { req: { json: () => Promise<unknown> } }, schema: S): Promise<z.infer<S>> {\n const raw = await c.req.json().catch(() => undefined);\n const parsed = schema.safeParse(raw);\n if (!parsed.success) {\n throw ApiError.badRequest(\n parsed.error.issues.map(i => `${i.path.join(\".\") || \"body\"}: ${i.message}`).join(\"; \"),\n \"INVALID_INPUT\"\n );\n }\n return parsed.data;\n}\n\n/**\n * The editor for `collectionsDir`: one per process, shared by these routes\n * and live schema editing's `/apply`. See {@link SchemaSourceEditor}.\n *\n * Here rather than beside the class because building it loads `ts-morph`, an\n * optional peer dependency this module is imported to probe for.\n */\nexport function createSchemaSourceEditor(collectionsDir: string): SchemaSourceEditor {\n return new SchemaSourceEditor(new AstSchemaEditor(collectionsDir));\n}\n\nexport function createSchemaEditorRoutes(source: SchemaSourceEditor): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n router.onError(errorHandler);\n\n router.post(\"/property/save\", async (c) => {\n const { collectionId, propertyKey, propertyConfig } = await body(c, propertySaveSchema);\n await refusalsAsBadRequest(() => source.run(editor => editor.saveProperty(collectionId, propertyKey, propertyConfig)));\n return c.json({ success: true });\n });\n\n router.post(\"/property/delete\", async (c) => {\n const { collectionId, propertyKey } = await body(c, propertyDeleteSchema);\n await refusalsAsBadRequest(() => source.run(editor => editor.deleteProperty(collectionId, propertyKey)));\n return c.json({ success: true });\n });\n\n /**\n * `partial: true` means \"this payload is what changed\", not \"this is the\n * collection\". Without it a one-key patch — which is what adding a column\n * posts — is read as a whole-collection save and deletes everything it does\n * not mention, `securityRules` included. Absent, it defaults to a full save,\n * so an older panel keeps the behaviour it was written against.\n */\n router.post(\"/collection/save\", async (c) => {\n const { collectionId, collectionData, patch, partial } = await body(c, collectionSaveSchema);\n if (patch !== undefined && !isCollectionPatch(patch)) {\n throw ApiError.badRequest(collectionPatchProblems(patch).join(\" \"), \"INVALID_INPUT\");\n }\n await refusalsAsBadRequest(() => source.run(editor => patch\n ? editor.applyPatch(collectionId, patch)\n : editor.saveCollection(collectionId, collectionData ?? {}, { partial: partial === true })));\n return c.json({ success: true });\n });\n\n router.post(\"/collection/delete\", async (c) => {\n const { collectionId } = await body(c, collectionDeleteSchema);\n await refusalsAsBadRequest(() => source.run(editor => editor.deleteCollection(collectionId)));\n return c.json({ success: true });\n });\n\n return router;\n}\n"],"mappings":"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"}
@@ -4,7 +4,7 @@ globalThis.process ??= __rebaseProcess;
4
4
  __rebaseCreateRequire(import.meta.url);
5
5
  import { r as toSnakeCase } from "./strings.es-BeZWAodn.js";
6
6
  import { W as isRelationalCollectionConfig } from "./src-D5Y3N-S4.js";
7
- import { t as ApiError } from "./errors-_ytoGi8A.js";
7
+ import { t as ApiError } from "./errors-OMWkKSH-.js";
8
8
  //#region ../utils/dist/plurals.es.js
9
9
  /**
10
10
  * Returns the singular of an English word.
@@ -665,4 +665,4 @@ function parseHardDelete(raw) {
665
665
  //#endregion
666
666
  export { apiHiddenFieldNames as a, fieldKeyForColumn as c, isJunctionBackedRelation as d, isRelationRequired as f, toWireKey as g, resolveRelation as h, parseWithDeleted as i, findRelation as l, resolveCollectionRelations as m, HARD_DELETE_QUERY_PARAM as n, effectiveAccess as o, relationDeclaringProperty as p, parseHardDelete as r, restrictedFieldNames as s, DELETED_QUERY_PARAM as t, getTableName as u };
667
667
 
668
- //# sourceMappingURL=soft-delete-params-BAgpmBvp.js.map
668
+ //# sourceMappingURL=soft-delete-params-BmlVwecC.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"soft-delete-params-BAgpmBvp.js","names":[],"sources":["../../utils/dist/plurals.es.js","../../utils/dist/names.es.js","../../common/src/util/resolve-relation.ts","../../common/src/util/relations.ts","../../common/src/collections/field-access.ts","../src/api/rest/soft-delete-params.ts"],"sourcesContent":["//#region src/plurals.ts\n/**\n* Returns the plural of an English word.\n*\n* @param {string} word\n* @param {number} [amount]\n* @returns {string}\n*/\nfunction plural(word, amount) {\n\tif (amount !== void 0 && amount === 1) return word;\n\tconst plurals = {\n\t\t\"(quiz)$\": \"$1zes\",\n\t\t\"^(ox)$\": \"$1en\",\n\t\t\"([m|l])ouse$\": \"$1ice\",\n\t\t\"(matr|vert|ind)ix|ex$\": \"$1ices\",\n\t\t\"(x|ch|ss|sh)$\": \"$1es\",\n\t\t\"([^aeiouy]|qu)y$\": \"$1ies\",\n\t\t\"(hive)$\": \"$1s\",\n\t\t\"(?:([^f])fe|([lr])f)$\": \"$1$2ves\",\n\t\t\"(shea|lea|loa|thie)f$\": \"$1ves\",\n\t\tsis$: \"ses\",\n\t\t\"([ti])um$\": \"$1a\",\n\t\t\"(tomat|potat|ech|her|vet)o$\": \"$1oes\",\n\t\t\"(bu)s$\": \"$1ses\",\n\t\t\"(alias)$\": \"$1es\",\n\t\t\"(octop)us$\": \"$1i\",\n\t\t\"(ax|test)is$\": \"$1es\",\n\t\t\"(us)$\": \"$1es\",\n\t\t\"([^s]+)$\": \"$1s\"\n\t};\n\tconst irregular = {\n\t\tmove: \"moves\",\n\t\tfoot: \"feet\",\n\t\tgoose: \"geese\",\n\t\tsex: \"sexes\",\n\t\tchild: \"children\",\n\t\tman: \"men\",\n\t\ttooth: \"teeth\",\n\t\tperson: \"people\"\n\t};\n\tif ([\n\t\t\"sheep\",\n\t\t\"fish\",\n\t\t\"deer\",\n\t\t\"moose\",\n\t\t\"series\",\n\t\t\"species\",\n\t\t\"money\",\n\t\t\"rice\",\n\t\t\"information\",\n\t\t\"equipment\",\n\t\t\"bison\",\n\t\t\"cod\",\n\t\t\"offspring\",\n\t\t\"pike\",\n\t\t\"salmon\",\n\t\t\"shrimp\",\n\t\t\"swine\",\n\t\t\"trout\",\n\t\t\"aircraft\",\n\t\t\"hovercraft\",\n\t\t\"spacecraft\",\n\t\t\"sugar\",\n\t\t\"tuna\",\n\t\t\"you\",\n\t\t\"wood\"\n\t].indexOf(word.toLowerCase()) >= 0) return word;\n\tfor (const w in irregular) {\n\t\tconst pattern = new RegExp(`${w}$`, \"i\");\n\t\tconst replace = irregular[w];\n\t\tif (pattern.test(word)) return word.replace(pattern, replace);\n\t}\n\tfor (const reg in plurals) {\n\t\tconst pattern = new RegExp(reg, \"i\");\n\t\tif (pattern.test(word)) return word.replace(pattern, plurals[reg]);\n\t}\n\treturn word;\n}\n/**\n* Returns the singular of an English word.\n*\n* @param {string} word\n* @param {number} [amount]\n* @returns {string}\n*/\nfunction singular(word, amount) {\n\tif (amount !== void 0 && amount !== 1) return word;\n\tconst singulars = {\n\t\t\"(quiz)zes$\": \"$1\",\n\t\t\"(matr)ices$\": \"$1ix\",\n\t\t\"(vert|ind)ices$\": \"$1ex\",\n\t\t\"^(ox)en$\": \"$1\",\n\t\t\"(alias)es$\": \"$1\",\n\t\t\"(octop|vir)i$\": \"$1us\",\n\t\t\"(cris|ax|test)es$\": \"$1is\",\n\t\t\"(shoe)s$\": \"$1\",\n\t\t\"(o)es$\": \"$1\",\n\t\t\"(bus)es$\": \"$1\",\n\t\t\"([m|l])ice$\": \"$1ouse\",\n\t\t\"(x|ch|ss|sh)es$\": \"$1\",\n\t\t\"(m)ovies$\": \"$1ovie\",\n\t\t\"(s)eries$\": \"$1eries\",\n\t\t\"([^aeiouy]|qu)ies$\": \"$1y\",\n\t\t\"([lr])ves$\": \"$1f\",\n\t\t\"(tive)s$\": \"$1\",\n\t\t\"(hive)s$\": \"$1\",\n\t\t\"(li|wi|kni)ves$\": \"$1fe\",\n\t\t\"(shea|loa|lea|thie)ves$\": \"$1f\",\n\t\t\"(^analy)ses$\": \"$1sis\",\n\t\t\"((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$\": \"$1$2sis\",\n\t\t\"([ti])a$\": \"$1um\",\n\t\t\"(n)ews$\": \"$1ews\",\n\t\t\"(h|bl)ouses$\": \"$1ouse\",\n\t\t\"(corpse)s$\": \"$1\",\n\t\t\"(us)es$\": \"$1\",\n\t\ts$: \"\"\n\t};\n\tconst irregular = {\n\t\tmove: \"moves\",\n\t\tfoot: \"feet\",\n\t\tgoose: \"geese\",\n\t\tsex: \"sexes\",\n\t\tchild: \"children\",\n\t\tman: \"men\",\n\t\ttooth: \"teeth\",\n\t\tperson: \"people\"\n\t};\n\tif ([\n\t\t\"sheep\",\n\t\t\"fish\",\n\t\t\"deer\",\n\t\t\"moose\",\n\t\t\"series\",\n\t\t\"species\",\n\t\t\"money\",\n\t\t\"rice\",\n\t\t\"information\",\n\t\t\"equipment\",\n\t\t\"bison\",\n\t\t\"cod\",\n\t\t\"offspring\",\n\t\t\"pike\",\n\t\t\"salmon\",\n\t\t\"shrimp\",\n\t\t\"swine\",\n\t\t\"trout\",\n\t\t\"aircraft\",\n\t\t\"hovercraft\",\n\t\t\"spacecraft\",\n\t\t\"sugar\",\n\t\t\"tuna\",\n\t\t\"you\",\n\t\t\"wood\"\n\t].indexOf(word.toLowerCase()) >= 0) return word;\n\tfor (const w in irregular) {\n\t\tconst pattern = new RegExp(`${irregular[w]}$`, \"i\");\n\t\tif (pattern.test(word)) return word.replace(pattern, w);\n\t}\n\tfor (const reg in singulars) {\n\t\tconst pattern = new RegExp(reg, \"i\");\n\t\tif (pattern.test(word)) return word.replace(pattern, singulars[reg]);\n\t}\n\treturn word;\n}\n//#endregion\nexport { plural, singular };\n\n//# sourceMappingURL=plurals.es.js.map","import { toSnakeCase } from \"./strings.es.js\";\nimport { singular } from \"./plurals.es.js\";\n//#region src/names.ts\n/**\n* Generates a foreign key column name from a given string, typically a collection slug or name.\n* It singularizes the name, converts it to snake_case and appends '_id'.\n*\n* Singularization runs *before* snake-casing so that acronyms survive: `toSnakeCase`\n* splits on every capital, which turned \"URLs\" into \"ur_ls\" and then \"ur_l_id\".\n*\n* @param name The base name to convert to a foreign key.\n* @returns A foreign key name in the format 'singular_name_id'.\n*\n* @example\n* // returns \"user_id\"\n* generateForeignKeyName(\"users\")\n*\n* @example\n* // returns \"category_id\"\n* generateForeignKeyName(\"categories\")\n*\n* @example\n* // returns \"product_id\"\n* generateForeignKeyName(\"Product\")\n*\n*/\nfunction generateForeignKeyName(name) {\n\treturn `${toSnakeCase(singularizeForKey(name))}_id`;\n}\n/**\n* `singular()` handles real English plurals, but its final catch-all rule strips\n* any trailing \"s\", which mangles words that only look plural. Guard the two\n* cases that produce a column name nobody would recognise:\n*\n* - a double \"s\" ending is never a plural marker (\"address\", \"class\", \"process\"),\n* so stripping it yields \"addres\";\n* - a name that singularizes to nothing (the literal \"s\") would yield \"_id\".\n*/\nfunction singularizeForKey(name) {\n\tif (/ss$/i.test(name)) return name;\n\tconst result = singular(name);\n\treturn result.length > 0 ? result : name;\n}\n/**\n* What `generateForeignKeyName` returned before it learned to singularize:\n* snake-case the name, then chop one trailing \"s\".\n*\n* This is here to be *detected*, never to be generated. A database provisioned\n* under the old rule carries `categorie_id`, `addresse_id`, `children_id` or\n* `ur_l_id` where the current rule expects `category_id`, `address_id`,\n* `child_id` and `url_id` — and the boot-time schema ensure is additive, so it\n* would create the new column empty beside the populated old one and leave the\n* relation reading nothing. No error, no missing table: the failure is silent,\n* which is the only reason this function still exists.\n*\n* `ensureCollectionSchema` calls it to recognise that shape and say so.\n* Returns the same string as `generateForeignKeyName` for every regular plural,\n* so a caller can compare the two and act only when they differ.\n*/\nfunction legacyForeignKeyName(name) {\n\tconst snake = toSnakeCase(name);\n\treturn `${snake.endsWith(\"s\") ? snake.slice(0, -1) : snake}_id`;\n}\n/**\n* Truncate an identifier to what Postgres will actually store.\n*\n* Postgres silently truncates identifiers at NAMEDATALEN-1 = 63 **bytes**, so a\n* name generated longer than that is not the name the database ends up holding.\n* Anything that later looks the object up by the name it generated then misses.\n*\n* Byte length, not string length: NAMEDATALEN is a byte bound, and a multi-byte\n* character straddling the boundary would be cut mid-sequence by `slice(0, 63)`.\n*\n* `TextEncoder` rather than `Buffer`, which is not a matter of taste: `Buffer`\n* is a Node global, and this package is imported by browser-facing ones. It\n* typechecked only where `@types/node` happened to be in scope, so\n* `packages/codegen` — whose tsconfig is `lib: [\"ESNext\", \"dom\"]` — could not\n* compile the file at all, and both of its suites failed to run. `TextEncoder`\n* and `TextDecoder` are standard in both runtimes and need no ambient types.\n*/\nfunction toPostgresIdentifier(name) {\n\treturn truncateToBytes(name, 63);\n}\n/**\n* {@link toPostgresIdentifier} with the bound lifted to a parameter.\n*\n* Exists for names that end in something load-bearing. Truncating at 63 keeps\n* the *head* of a name and discards the tail, which is right for a descriptive\n* identifier and wrong for a hashed one: the hash is the part that makes it\n* unique, and it is at the end. A caller that appends a fingerprint truncates\n* the readable head to `63 - <tail>` itself and then appends, so the bound is\n* still 63 and the hash always survives.\n*\n* `contracts/derived-names.txt` records what the alternative costs — a foreign\n* key frozen as `..._corres`, its `_fkey` suffix truncated away, so a second\n* foreign key on that table would derive a byte-identical name.\n*\n* One truncation rule, in one function, so the two cannot drift.\n*/\nfunction truncateToBytes(name, maxBytes) {\n\tconst bytes = new TextEncoder().encode(name);\n\tif (bytes.byteLength <= maxBytes) return name;\n\treturn new TextDecoder(\"utf-8\").decode(bytes.subarray(0, maxBytes)).replace(/�+$/, \"\");\n}\n/**\n* The API name a database column is served under.\n*\n* The wire name of a field is its property key, and Rebase's property keys are\n* camelCase — `displayName`, `createdAt`, `photoURL`. Columns are snake_case,\n* because an unquoted Postgres identifier folds to lower case and a camelCase\n* column is therefore reachable only as `\"authorId\"` forever: in hand-written\n* SQL, in psql, in an RLS policy body, in a dump, and in every third-party tool\n* that ever touches the database. So the two conventions are both right, and\n* this is the function that crosses between them.\n*\n* It exists because two sources of field names never crossed: a foreign key\n* derived from a relation (`author_id`) and a column read back by introspection\n* (`user_id`) both landed on the wire under their column name, while every\n* hand-authored collection next to them used camelCase. One API, two\n* conventions, and no rule a caller could infer from outside — those names are\n* also the `where` and `orderBy` keys, so it was not a matter of taste.\n*\n* Rules, in the order they matter:\n*\n* - **A name with no separator is returned unchanged.** `photoURL` stays\n* `photoURL` and `id` stays `id`. Lower-casing a single token is what makes\n* a \"camelCase\" helper destructive — `camelCase(\"photoURL\")` is `photourl` —\n* and this function is applied to names that are *already* keys.\n* - **Each following segment keeps its own casing** apart from an upper-cased\n* first letter, so `photo_URL` → `photoURL` rather than `photoUrl`.\n* - **The result may still not be a JavaScript identifier.** `2fa_enabled`\n* becomes `2faEnabled`, which is a perfectly good object key and still needs\n* quoting where one is written into generated source.\n*\n* Not the inverse of {@link toSnakeCase}: `toSnakeCase` tokenises on case\n* boundaries and would turn `photoURL` into `photo_url`. Round-tripping is not\n* a property either function promises, which is why a column name that a\n* property maps explicitly is always read off `columnName` rather than derived.\n*/\nfunction toWireKey(columnName) {\n\tif (!columnName) return columnName;\n\tconst segments = columnName.split(/[-_ ]+/).filter(Boolean);\n\tif (segments.length <= 1) return columnName;\n\treturn segments.map((segment, index) => index === 0 ? segment.charAt(0).toLowerCase() + segment.slice(1) : segment.charAt(0).toUpperCase() + segment.slice(1)).join(\"\");\n}\n/**\n* The first candidate key not already used, or a numbered fallback.\n*\n* Introspection turns a set of column names into a set of object keys, and the\n* mapping is not injective: `user_id` and `userId` are two columns and one\n* {@link toWireKey}, and two foreign keys can strip to the same relation name.\n* A duplicate key in a generated object literal is a TypeScript error, so the\n* whole collection stops compiling — and a duplicate key in a `Record` built at\n* runtime is worse, because it silently drops a column instead.\n*\n* The numbered tail is what makes this total: a function that returns a key it\n* cannot guarantee is free has only moved the duplicate one line down.\n*\n* Structurally typed on `has` so a `Map` of emitted blocks and a `Set` of taken\n* names both satisfy it. Lives here, in the package both introspection\n* producers and the admin's table import can reach, because they must resolve a\n* collision the same way or one database describes itself three ways.\n*/\nfunction firstFreeKey(candidates, taken) {\n\tfor (const candidate of candidates) if (!taken.has(candidate)) return candidate;\n\tconst base = candidates[candidates.length - 1];\n\tfor (let suffix = 2;; suffix++) {\n\t\tconst candidate = `${base}_${suffix}`;\n\t\tif (!taken.has(candidate)) return candidate;\n\t}\n}\n//#endregion\nexport { firstFreeKey, generateForeignKeyName, legacyForeignKeyName, toPostgresIdentifier, toWireKey, truncateToBytes };\n\n//# sourceMappingURL=names.es.js.map","import {\n CollectionConfig,\n Relation,\n ResolvedRelation\n} from \"@rebasepro/types\";\nimport { generateForeignKeyName, toSnakeCase } from \"@rebasepro/utils\";\n\nimport { getTableName } from \"./relations\";\n\n/**\n * Fill in a relation's defaults.\n *\n * This replaces `sanitizeRelation`, which had to work out *which kind of link\n * you meant* from whichever optional fields happened to be set — 194 lines of\n * it, including a pass that inspected the target collection's own relations to\n * decide whether a `many`/`inverse` pair was a one-to-many or the far side of a\n * many-to-many, wrapped in a `try/catch` that fell through to the wrong answer\n * when it could not tell. Two consumers running that logic at different moments\n * could reach different conclusions about the same relation.\n *\n * With the kind declared there is nothing to work out. What remains is\n * defaulting — a table name, a column name — which is deterministic, depends\n * only on the relation and its two endpoints, and cannot fail. That is why this\n * function returns rather than throws, and why it needs no cache to be\n * consistent.\n */\nexport function resolveRelation(\n relation: Relation,\n sourceCollection: CollectionConfig,\n propertyKey?: string\n): ResolvedRelation {\n const target = relation.target;\n if (typeof target !== \"function\") {\n throw new Error(\n `Relation${relation.relationName ? ` '${relation.relationName}'` : \"\"} on ` +\n `'${sourceCollection.slug}' has no \\`target\\`. Give it a thunk: \\`target: () => otherCollection\\`.`\n );\n }\n\n const targetCollection = callTarget(relation, sourceCollection, propertyKey, target);\n\n // The name is the address: the `include` key, the admin tab, and the\n // segment of a nested path. Declared name wins, then the declaring\n // property's key, then the target's slug.\n const relationName = relation.relationName ?? propertyKey ?? toSnakeCase(targetCollection.slug);\n\n const shared: Pick<ResolvedRelation, \"relationName\" | \"target\" | \"targetSlug\" | \"onUpdate\" | \"onDelete\" | \"overrides\"> = {\n relationName,\n // Normalised, not the thunk as written. Resolution reads the target once\n // and every later consumer calls it again — the driver building a join,\n // the DDL and policy generators, the admin's relation fields — so\n // handing back the raw thunk would give all of them the module namespace\n // `callTarget` just looked past, and the fix would hold only for the\n // fields resolution happens to read here. Still lazy: same call at the\n // same moment, one unwrap on the way out.\n target: () => unwrapModuleNamespace(target()) as CollectionConfig,\n targetSlug: targetCollection.slug,\n onUpdate: relation.onUpdate,\n onDelete: relation.onDelete,\n overrides: relation.overrides\n // No `validation`. Whether the link is required is a fact about the\n // *property*, and copying it onto the resolved relation gave the\n // question two answers that were free to disagree. Ask\n // `isRelationRequired(collection, relation)`, which reads the one.\n };\n\n const sourceName = toSnakeCase(sourceCollection.slug ?? sourceCollection.name);\n\n switch (relation.kind) {\n case \"belongsTo\":\n return {\n ...shared,\n kind: \"belongsTo\",\n cardinality: \"one\",\n writable: true,\n shared: false,\n localKey: relation.localKey ?? generateForeignKeyName(relationName),\n index: relation.index !== false\n };\n\n case \"hasOne\":\n return {\n ...shared,\n kind: \"hasOne\",\n cardinality: \"one\",\n writable: true,\n shared: false,\n foreignKeyOnTarget: relation.foreignKeyOnTarget ?? generateForeignKeyName(sourceName),\n sourceKey: relation.sourceKey\n };\n\n case \"hasMany\":\n return {\n ...shared,\n kind: \"hasMany\",\n cardinality: \"many\",\n writable: true,\n shared: false,\n foreignKeyOnTarget: relation.foreignKeyOnTarget ?? generateForeignKeyName(sourceName),\n // Not defaulted: the source's primary key needs the driver's\n // schema to resolve, which resolution does not have. `undefined`\n // means \"the primary key\" — see `ResolvedHasMany.sourceKey`.\n sourceKey: relation.sourceKey\n };\n\n case \"manyToMany\": {\n const sourceTable = getTableName(sourceCollection);\n const targetTable = getTableName(targetCollection);\n return {\n ...shared,\n kind: \"manyToMany\",\n cardinality: \"many\",\n writable: true,\n shared: true,\n through: {\n // Sorted so both sides of the same link derive the same\n // table without having to agree in advance.\n table: relation.through?.table ?? [sourceTable, targetTable].sort().join(\"_\"),\n sourceColumn: relation.through?.sourceColumn ?? generateForeignKeyName(sourceName),\n targetColumn: relation.through?.targetColumn ?? generateForeignKeyName(relationName),\n // `{}` rather than `undefined`, for the reason every other\n // field here is filled in: a consumer reads one shape and\n // does not have to decide what an absent payload means.\n properties: relation.through?.properties ?? {}\n }\n };\n }\n\n case \"via\":\n return {\n ...shared,\n kind: \"via\",\n cardinality: relation.cardinality,\n writable: false,\n // A join chain reaches rows that other parents reach too, and\n // Rebase does not know which hop, if any, is a link it owns.\n shared: true,\n joinPath: relation.joinPath\n };\n\n default: {\n // Exhaustive: a new kind is a compile error here, not a silent\n // fall-through to whatever shape happened to match first.\n const exhaustive: never = relation;\n throw new Error(`Unknown relation kind: ${JSON.stringify(exhaustive)}`);\n }\n }\n}\n\n/** How this relation is addressed in an error message, before it has a resolved name. */\nfunction describe(relation: Relation, sourceCollection: CollectionConfig, propertyKey?: string): string {\n const name = relation.relationName ?? propertyKey;\n return `Relation${name ? ` '${name}'` : \"\"} on '${sourceCollection.slug}'`;\n}\n\n/**\n * A module namespace, unwrapped to the collection it exports.\n *\n * A cycle transpiled to CommonJS does not hand the importing module the\n * *default export* — it hands it the module object, `{ __esModule: true,\n * default: … }`, captured before the exporting module finished evaluating. The\n * `default` slot fills in later, so by the time a lazy `target` thunk runs the\n * collection is sitting right there, one level down. Returning the namespace is\n * never a thing a thunk means to do, and there is exactly one reading of it.\n *\n * Only unwrapped when the inner value is itself a collection: a `default` that\n * is not one is a genuinely wrong thunk, and it should reach the error below\n * rather than be quietly swapped in.\n */\nfunction unwrapModuleNamespace(value: unknown): unknown {\n if (!value || typeof value !== \"object\") return value;\n if ((value as { slug?: unknown }).slug) return value;\n const inner = (value as { default?: unknown }).default;\n return inner && typeof inner === \"object\" && (inner as { slug?: unknown }).slug ? inner : value;\n}\n\n/**\n * Call the `target` thunk, and translate the ways an import cycle breaks it into\n * an error that names the cause — or, where the value is recoverable, into the\n * collection the thunk meant.\n *\n * The thunk exists to defer the reference until every module has finished\n * evaluating, and for a cycle that closes at import time it does. Two cycles\n * leave the binding permanently unusable:\n *\n * - **ESM/TDZ.** `const` and `class` bindings in a not-yet-evaluated module are\n * in the temporal dead zone, so reading one throws `ReferenceError: x is not\n * defined`. The stack points at the thunk — a one-line arrow function that is\n * obviously fine — and says nothing about the cycle that made it throw.\n * - **CJS interop, unresolved.** The half-initialised module object has no\n * `default` yet, the import resolves to `undefined`, and the thunk returns it\n * without complaint. That one used to surface here as \"did not resolve to a\n * collection\", which is true and unhelpful.\n *\n * Both mean the same thing, and the fix for both is the same: break the cycle,\n * or move the relation into the collection that does not close it.\n *\n * A third shape is *not* an error, and used to be reported as one. A loader that\n * transpiles ESM to CJS — jiti, which is what `rebase generate-sdk` and\n * `rebase build` load collections with — gives the module entered second in a\n * cycle a namespace object rather than the default export, and never replaces it\n * with a live binding. The thunk then returns `{ __esModule: true, default: … }`\n * holding the fully-initialised collection. Native ESM resolves the same thunk\n * to the collection directly, so this was a loader artefact reported as an\n * authoring mistake, and the advice it gave — make the target a lazy thunk — was\n * already satisfied by the code it was rejecting. Bidirectional relations make\n * these cycles unavoidable, and the lazy thunk is this framework's own answer to\n * them, so {@link unwrapModuleNamespace} takes the collection and moves on.\n */\nfunction callTarget(\n relation: Relation,\n sourceCollection: CollectionConfig,\n propertyKey: string | undefined,\n target: Relation[\"target\"]\n): ReturnType<Relation[\"target\"]> {\n let targetCollection: ReturnType<Relation[\"target\"]> | undefined;\n try {\n targetCollection = unwrapModuleNamespace(target()) as ReturnType<Relation[\"target\"]>;\n } catch (error) {\n // A ReferenceError from inside the thunk is a binding that was never\n // initialised — nothing else in a one-expression arrow can raise one.\n if (error instanceof ReferenceError) {\n throw new Error(\n `${describe(relation, sourceCollection, propertyKey)} targets a collection that is not ` +\n `initialized yet — almost always an import cycle between the two collection files. ` +\n `Break the cycle (move the shared piece into a third module, or import the target ` +\n `lazily) so the target's module finishes evaluating before the registry is built.`,\n { cause: error }\n );\n }\n throw error;\n }\n\n if (!targetCollection?.slug) {\n throw new Error(\n `${describe(relation, sourceCollection, propertyKey)} has a \\`target\\` that resolved to ` +\n `${targetCollection === undefined ? \"`undefined`\" : \"something that is not a collection\"}. ` +\n (targetCollection === undefined\n ? \"Under CommonJS interop an import cycle resolves the default import to `undefined`, \" +\n \"so check whether this collection and its target import each other. Otherwise the thunk \" +\n \"is returning the wrong value — it must return the collection itself, not a promise or a module.\"\n : typeof (targetCollection as { then?: unknown }).then === \"function\"\n ? \"The thunk returned a promise — `target: () => import(\\\"./other\\\")` is asynchronous. \" +\n \"Import the collection at the top of the file and return the binding: \" +\n \"`target: () => otherCollection`.\"\n : \"The thunk must return a collection config with a `slug`.\")\n );\n }\n\n return targetCollection;\n}\n","import { CollectionConfig, isRelationalCollectionConfig, Property, ResolvedRelation, RelationProperty } from \"@rebasepro/types\";\nimport { toSnakeCase, toWireKey } from \"@rebasepro/utils\";\n\nimport { resolveRelation } from \"./resolve-relation\";\n\n/**\n * Whether the target rows are shared with other parents — a many-to-many, or a\n * multi-hop `via` chain.\n *\n * Decides what a write \"through\" the relation may touch: a shared target\n * belongs to every parent that links it, so the parent owns the *link* and not\n * the row. The backend enforces that (an unlink rather than a delete) and the\n * admin renders it (remove-from-parent rather than delete).\n *\n * Now a field on the resolved relation rather than a re-derivation, so both\n * sides read the same answer instead of each computing one.\n */\nexport function isJunctionBackedRelation(relation: ResolvedRelation): boolean {\n return relation.shared;\n}\n\n/** WeakMap cache — same collection instance always yields the same relation map. */\nconst _resolvedRelationsCache = new WeakMap<CollectionConfig, Record<string, ResolvedRelation>>();\n\n/**\n * Every relation a collection declares, keyed by the name it is addressed by.\n *\n * A relation reaches the map from either of two places — the collection's\n * `relations` array, or a `relation` property that declares one inline — and is\n * keyed by its resolved `relationName`, which is what a nested path segment,\n * an `include` key and an admin tab all match against.\n *\n * Resolution no longer swallows failures. It used to wrap each relation in a\n * `try/catch` that dropped anything it could not work out, so a\n * mis-declared relation silently vanished instead of being reported; with the\n * kind declared, the only remaining failure is a `target` that does not resolve,\n * which is worth hearing about.\n */\nexport function resolveCollectionRelations(\n collection: CollectionConfig\n): Record<string, ResolvedRelation> {\n const cached = _resolvedRelationsCache.get(collection);\n if (cached) return cached;\n\n if (!isRelationalCollectionConfig(collection)) return {};\n\n const relations: Record<string, ResolvedRelation> = {};\n\n for (const relation of collection.relations ?? []) {\n const resolved = resolveRelation(relation, collection);\n relations[resolved.relationName] = resolved;\n }\n\n // A property declaring a relation inline is registered under the property\n // key as well: the fetch layer hydrates the result back onto that key, and\n // it is the name the admin addresses the field by.\n for (const [propertyKey, property] of Object.entries(collection.properties ?? {})) {\n if ((property as Property)?.type !== \"relation\") continue;\n const declared = (property as RelationProperty).relation;\n if (!declared || relations[propertyKey]) continue;\n\n relations[propertyKey] = resolveRelation(declared, collection, propertyKey);\n }\n\n _resolvedRelationsCache.set(collection, relations);\n return relations;\n}\n\n/**\n * The `type: \"relation\"` property that declares a link, or `undefined` for one\n * that only exists in the collection's `relations` array.\n *\n * Both declaration sites end up in {@link resolveCollectionRelations}, and only\n * one of them has a property to carry field-level facts — `name`, `admin`, and\n * the one this exists for, `validation.required`.\n */\nexport function relationDeclaringProperty(\n collection: CollectionConfig,\n relation: ResolvedRelation\n): RelationProperty | undefined {\n const resolved = resolveCollectionRelations(collection);\n for (const [key, raw] of Object.entries(collection.properties ?? {})) {\n const prop = raw as Property | undefined;\n if (prop?.type !== \"relation\") continue;\n // A relation declared inline is keyed by the property; one declared in\n // `relations` is keyed by its name, which the property addresses.\n if (resolved[key] === relation) return prop as RelationProperty;\n const addressed = (prop as RelationProperty).relation?.relationName;\n if (addressed && findRelation(resolved, addressed) === relation) return prop as RelationProperty;\n }\n return undefined;\n}\n\n/**\n * Must every row of this collection point at a target through this link?\n *\n * Read from the declaring property's `validation.required` — the same key every\n * other field uses, and the only place it lives.\n *\n * `RelationBase` carried its own `validation.required` until 0.18, which made\n * this two questions rather than one. They were answered by different readers:\n * the Postgres DDL generator asked the property (so the foreign-key column was\n * `NOT NULL`) and the SDK type generator asked the relation (so the generated\n * `Insert` type made the field optional). A `create()` that left the relation\n * out therefore typechecked and then failed at the database with a not-null\n * violation, and the two `required`s had to be written twice, identically, for\n * the pair to agree.\n *\n * A relation with no declaring property — an entry in `relations` nothing\n * points at — is not required. There is no field to fill in.\n */\nexport function isRelationRequired(collection: CollectionConfig, relation: ResolvedRelation): boolean {\n return Boolean(relationDeclaringProperty(collection, relation)?.validation?.required);\n}\n\n/**\n * The path of the collection a relation property points at, derived from the\n * property alone.\n *\n * A preview holds a property and a value and no collection, so it cannot call\n * `resolveRelationProperty`. It does not need to: both forms that carry a\n * target — the stamped `resolvedRelation` and the inline `relation` — name it\n * directly. Only the third form, a relation declared by name in the\n * collection's `relations` array, is out of reach, and that one has no target\n * to read without the collection anyway.\n *\n * This is what lets a preview render a relation column that arrived as a bare\n * foreign key: the id says *which* row, the declared target says *which\n * collection*, and `RelationPreview` fetches the rest. Without it a scalar id\n * is indistinguishable from a value of the wrong type.\n */\nexport function getRelationTargetPath(property: RelationProperty): string | undefined {\n const stamped = property.resolvedRelation?.targetSlug;\n if (stamped) return stamped;\n\n const target = property.relation?.target;\n if (typeof target !== \"function\") return undefined;\n try {\n return target()?.slug;\n } catch (_e) {\n // A thunk reaching into a module that has not finished initialising:\n // there is no target to name yet, and a preview is not worth throwing over.\n return undefined;\n }\n}\n\n/**\n * The table a collection reads and writes.\n *\n * `table` when it is set, otherwise `toSnakeCase(slug)` — which is what made it\n * safe to drop `table` from the required fields on the config type: the runtime\n * had always derived it, and the type was demanding a value it did not need.\n *\n * The `||` chain is load-bearing. `toSnakeCase(undefined)` returns `\"\"`, not\n * `undefined`, so the previous `??` chain short-circuited on the empty string\n * and the name fallback could never run — a safety net that read like one and\n * caught nothing. It was unreachable while `slug` was required; it stops being\n * unreachable the moment anything constructs a config without one.\n */\nexport function getTableName(collection: CollectionConfig): string {\n const declared = isRelationalCollectionConfig(collection) ? collection.table : undefined;\n return declared || toSnakeCase(collection.slug) || toSnakeCase(collection.name);\n}\n\n/**\n * A JavaScript identifier: what a generated `export const <name> =` needs.\n *\n * Deliberately the same shape the two schema generators already define\n * privately — this is the third place that needed it, and the first two guard\n * property keys and member accesses while nothing guarded the variable name\n * itself.\n */\nconst JS_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n\n/**\n * The variable name a generated table is bound to.\n *\n * Camel-cases underscores, and then guarantees the result is a legal\n * identifier. It did only the first, so a table name that is legal in Postgres\n * and not in JavaScript produced a `schema.generated.ts` that does not parse:\n *\n * `2024_archive` → `export const 2024Archive = pgTable(…)`\n * \"An identifier or keyword cannot immediately follow\n * a numeric literal\"\n * `reporting.events` → `export const reporting.events = pgTable(…)`\n * \"',' expected\"\n *\n * That file is imported by the server, so the failure is not one broken\n * collection — `rebase build` and `db push` fail at tsc for the whole\n * directory. And it is reachable from a documented flow: `rebase init` against\n * a database holding a table called `2024_archive` writes a collection file\n * that parses and a schema file that does not.\n *\n * **A no-op for every name that already worked**, which is what makes changing\n * a derived name safe here: the only inputs whose output changes are the ones\n * that produced a syntax error, and nothing can be running against those.\n * Separators become camel case rather than disappearing, so `reporting.events`\n * and `reporting_events` do not collide into one variable.\n */\nexport function getTableVarName(tableName: string): string {\n const camel = tableName.replace(/_([a-z])/g, (_, char: string) => char.toUpperCase());\n if (JS_IDENTIFIER.test(camel)) return camel;\n\n const sanitised = camel\n // Any other separator gets the same treatment `_` did, so two tables\n // differing only by separator keep differing.\n .replace(/[^A-Za-z0-9_$]+([A-Za-z0-9])?/g, (_, char?: string) =>\n (char ? char.toUpperCase() : \"\"))\n // A leading digit is legal in Postgres and not in JavaScript. Prefixed\n // rather than stripped, so `2024_archive` and `archive` stay distinct.\n .replace(/^([0-9])/, \"t$1\");\n\n return JS_IDENTIFIER.test(sanitised) ? sanitised : `t${sanitised}`;\n}\n\nexport function getEnumVarName(tableName: string, propName: string): string {\n const tableVar = getTableVarName(tableName);\n const propVar = propName.charAt(0).toUpperCase() + propName.slice(1);\n return `${tableVar}${propVar}`;\n}\n\nexport function getColumnName(fullColumn: string): string {\n return fullColumn.includes(\".\") ? fullColumn.split(\".\").pop()! : fullColumn;\n}\n\n/**\n * The field key a database column is served and addressed under.\n *\n * A column has two names and they are not the same name. `author_id` is what\n * Postgres stores; `authorId` is the key on the JSON row, the key in the\n * generated Drizzle table, and the key a caller writes in `where` and\n * `orderBy`. Every place that starts from a column and has to reach a row, a\n * Drizzle table or a payload goes through here, so there is one answer rather\n * than one per call site — the two that disagreed put `displayName` and\n * `author_id` on the same API.\n *\n * A declared property is the authority when there is one, because its key *is*\n * the wire name and `columnName` is the only thing that ever renamed the\n * column:\n *\n * 1. an explicit `columnName` equal to this column;\n * 2. a property whose key is literally the column (an author who wrote\n * `author_id:` meant `author_id` on the wire, and gets it);\n * 3. a property whose key snake-cases to the column, which is the default\n * mapping — `authorId` → `author_id`.\n *\n * With no property in the way — a foreign key derived from a relation, which\n * usually has none — the name is derived: {@link toWireKey}.\n *\n * Note the fallback is *not* the column verbatim. That was the old behaviour\n * and it is precisely the defect: a derived foreign key reached the wire under\n * its column name while every hand-authored field beside it was camelCase.\n */\nexport function fieldKeyForColumn(collection: CollectionConfig | undefined, column: string): string {\n const properties = collection?.properties;\n if (properties) {\n for (const [key, prop] of Object.entries(properties)) {\n const columnName = (prop as { columnName?: unknown } | undefined)?.columnName;\n if (typeof columnName === \"string\" && columnName === column) return key;\n }\n for (const key of Object.keys(properties)) {\n if (key === column) return key;\n if (toSnakeCase(key) === column) return key;\n }\n }\n return toWireKey(column);\n}\n\n/**\n * Look up a relation by key with forgiving normalization.\n *\n * `resolveCollectionRelations` stores each relation under a single canonical\n * key (no aliases). This helper tries the given key as-is, then falls back to\n * slug form (underscores → hyphens) and snake_case form (hyphens → underscores)\n * so that callers that receive a key from external input (URL path segments,\n * user-provided config, etc.) can still find the right entry.\n */\nexport function findRelation(\n resolvedRelations: Record<string, ResolvedRelation>,\n key: string\n): ResolvedRelation | undefined {\n // Exact match first\n if (resolvedRelations[key]) return resolvedRelations[key];\n\n // Try slug form (e.g. \"company_id\" → \"company-id\")\n const slugKey = key.replace(/_/g, \"-\");\n if (slugKey !== key && resolvedRelations[slugKey]) return resolvedRelations[slugKey];\n\n // Try snake_case form (e.g. \"company-id\" → \"company_id\")\n const snakeKey = key.replace(/-/g, \"_\");\n if (snakeKey !== key && resolvedRelations[snakeKey]) return resolvedRelations[snakeKey];\n\n return undefined;\n}\n","import type { CollectionConfig, FieldAccess, Property } from \"@rebasepro/types\";\nimport { fieldKeyForColumn, findRelation, resolveCollectionRelations } from \"../util/relations\";\n\n/**\n * Field-level access control: one mechanism, read by every enforcement point.\n *\n * A collection's `securityRules` decide which *rows* a caller reaches;\n * `property.access` decides which *fields* of a reached row they see and may\n * set. The two are independent — a field rule never widens row access, and a\n * row a caller cannot read has no fields to talk about.\n *\n * `excludeFromApi` is sugar for `access: { read: [], write: [] }` and is\n * normalised into it by {@link effectiveAccess}, which is the only place either\n * spelling is read. It used to be its own code path in five files — the read\n * strip, the write refusal, the SDK generator, the OpenAPI schema builder and\n * the filter-parameter builder — and the second rule would have made ten.\n * There is one predicate now, and the flag is a shorthand for it.\n *\n * @module\n */\n\n/**\n * The caller a field rule is judged against: whatever the call context carries\n * as the user's application roles.\n *\n * `undefined` is the trusted server plane — an in-process `rebase.data` call\n * with no request behind it, the auth adapter writing a password hash, a\n * migration. Every API boundary has a viewer: an unauthenticated REST request is\n * scoped as `{ uid: ANONYMOUS_USER_ID, roles: [\"anon\"] }` before it reaches a\n * driver, so \"no viewer\" cannot be reached from outside.\n */\nexport interface FieldViewer {\n roles?: readonly string[];\n}\n\n/**\n * The role that satisfies any non-empty list.\n *\n * The same arm every baseline policy carries: `security_rules` injects\n * `rolesOverlap(['admin'])` into the default read and write policies, and\n * `rebase.dataAsAdmin` is scoped with `{ uid: \"service\", roles: [\"admin\"] }`.\n * Without this an author could declare `access: { read: [\"hr\"] }` and lock the\n * administrator out of a column of their own database — and lock the Studio out\n * of rendering it.\n */\nexport const ADMIN_ROLE = \"admin\";\n\n/**\n * What a property's access rules actually are, with `excludeFromApi` expanded.\n *\n * Returns `undefined` when the property constrains nothing, so callers can skip\n * the whole check for the overwhelmingly common case.\n */\nexport function effectiveAccess(property: Property | undefined): FieldAccess | undefined {\n if (!property) return undefined;\n if (property.excludeFromApi) return EXCLUDED_ACCESS;\n const access = property.access;\n if (!access) return undefined;\n if (access.read === undefined && access.write === undefined) return undefined;\n return access;\n}\n\n/** The rule `excludeFromApi: true` expands to. Frozen: it is shared by every caller. */\nconst EXCLUDED_ACCESS: FieldAccess = Object.freeze({ read: Object.freeze([]), write: Object.freeze([]) });\n\n/**\n * Does a caller holding `roles` satisfy `allowed`?\n *\n * Three cases, and the middle one is the one worth stating out loud:\n *\n * - `allowed` omitted — the field carries no rule of its own, so the row's\n * policies have already answered. True.\n * - `allowed` empty — nobody, at any privilege, through any API. Not the admin,\n * not the service key, not the trusted plane reading on a caller's behalf.\n * This is what `excludeFromApi` has always meant on the read side, and\n * collapsing the two spellings means the empty list has to keep meaning it.\n * - `allowed` non-empty — one of the named roles, or `admin`, or no viewer at\n * all (the trusted server plane, which is not an API caller).\n */\nfunction satisfies(allowed: readonly string[] | undefined, viewer: FieldViewer | undefined): boolean {\n if (allowed === undefined) return true;\n if (allowed.length === 0) return false;\n if (!viewer) return true;\n const roles = viewer.roles;\n if (!roles || roles.length === 0) return false;\n return roles.includes(ADMIN_ROLE) || allowed.some(role => roles.includes(role));\n}\n\n/** May this caller receive this field's value? */\nexport function canReadField(property: Property | undefined, viewer: FieldViewer | undefined): boolean {\n const access = effectiveAccess(property);\n return access ? satisfies(access.read, viewer) : true;\n}\n\n/** May this caller set this field's value? */\nexport function canWriteField(property: Property | undefined, viewer: FieldViewer | undefined): boolean {\n const access = effectiveAccess(property);\n return access ? satisfies(access.write, viewer) : true;\n}\n\n/**\n * The names on this collection a caller may not touch, in the two spellings a\n * caller can write them in.\n *\n * `declared` is the property keys, which is what has to leave a *known-fields*\n * set. `refused` is those plus the physical column names behind them: a caller\n * who knows the table can send `password_hash` as readily as `passwordHash`, and\n * a rule that only knew the wire name would be one rename away from useless.\n *\n * A restricted to-one relation takes its foreign key with it. `bandId: 7` names\n * the band exactly as `band: { id: 7 }` does, and the row carries the column\n * beside the relation — so a rule on the relation that left the key alone\n * withheld the value from the row and still let `?bandId=7` ask for it.\n *\n * `kind` picks which half of the rule is read; nothing else differs.\n */\nexport function restrictedFieldNames(\n collection: CollectionConfig,\n viewer: FieldViewer | undefined,\n kind: \"read\" | \"write\"\n): { declared: string[]; refused: Set<string> } {\n const allowed = kind === \"read\" ? canReadField : canWriteField;\n return namesWhere(collection, property => !allowed(property, viewer));\n}\n\n/**\n * The names on this collection that the API does not have, in every spelling\n * a caller could write them in — the `excludeFromApi` properties, their column\n * names, and the foreign key of an excluded to-one relation.\n *\n * Not the same set as {@link restrictedFieldNames}. A field restricted by role\n * is public by name: OpenAPI and `list_collections` describe it, and refusing a\n * query on it with \"not readable\" tells the caller nothing they could not read\n * there. An `excludeFromApi` field is described nowhere, so a query naming one\n * has to be answered as a query naming a column that does not exist — anything\n * else confirms that it does.\n */\nexport function apiHiddenFieldNames(collection: CollectionConfig): Set<string> {\n return namesWhere(collection, property => Boolean(property?.excludeFromApi)).refused;\n}\n\nfunction namesWhere(\n collection: CollectionConfig,\n restricted: (property: Property) => boolean\n): { declared: string[]; refused: Set<string> } {\n const declared: string[] = [];\n const refused = new Set<string>();\n const properties = collection.properties ?? {};\n // Resolved only when a restricted property is a relation, which is rare:\n // the read strip runs this on every row.\n let relations: ReturnType<typeof resolveCollectionRelations> | undefined;\n\n const restrict = (name: string) => {\n if (name in properties && !declared.includes(name)) declared.push(name);\n refused.add(name);\n };\n\n for (const [name, property] of Object.entries(properties)) {\n if (!restricted(property as Property)) continue;\n restrict(name);\n const columnName = (property as Property).columnName;\n if (columnName) refused.add(columnName);\n if ((property as Property).type === \"relation\") {\n relations ??= resolveCollectionRelations(collection);\n const relation = findRelation(relations, name);\n if (relation?.kind === \"belongsTo\") {\n restrict(fieldKeyForColumn(collection, relation.localKey));\n refused.add(relation.localKey);\n }\n }\n }\n return { declared, refused };\n}\n\n/**\n * True when nothing on this collection restricts a field, for either direction.\n *\n * Every read of every row runs through the strip, so the collection that has no\n * rules — which is almost all of them — has to cost one property walk and no\n * allocation.\n */\nexport function hasFieldAccessRules(collection: CollectionConfig): boolean {\n for (const property of Object.values(collection.properties ?? {})) {\n if (effectiveAccess(property as Property)) return true;\n }\n return false;\n}\n","import { ApiError } from \"../errors\";\n\n/**\n * The two query parameters soft delete adds to the REST surface.\n *\n * Kept in their own module so the call sites in `query-parser.ts` and the\n * delete routes are a single line each: the parsing rules belong to soft\n * delete, not to the parser, and a rule spread across the two files that read\n * it is a rule that drifts.\n */\n\n/** `?deleted=` — what to do about rows a soft delete has stamped. */\nexport const DELETED_QUERY_PARAM = \"deleted\";\n/** `?hard=` — ask for a real `DELETE` on a soft-delete collection. */\nexport const HARD_DELETE_QUERY_PARAM = \"hard\";\n\n/**\n * `?deleted=include|only` → the driver's `withDeleted`.\n *\n * Spelled `deleted` on the wire and `withDeleted` in the driver, deliberately:\n * the URL reads as a question about the rows (`?deleted=only` — \"only the\n * deleted ones\"), and the driver option reads as an instruction about the query.\n *\n * A value neither word is a 400 rather than a silent fallback to the default.\n * `?deleted=true` quietly hiding every deleted row is the worst of both: it\n * looks like it worked and answers the opposite question. Absent is the\n * default, which is \"hide them\".\n */\nexport function parseWithDeleted(raw: unknown): boolean | \"only\" | undefined {\n if (raw === undefined || raw === null || raw === \"\") return undefined;\n const value = String(raw).trim().toLowerCase();\n if (value === \"include\") return true;\n if (value === \"only\") return \"only\";\n throw ApiError.badRequest(\n `Invalid \\`?${DELETED_QUERY_PARAM}=${String(raw)}\\`. It takes 'include' (live rows and deleted ones) ` +\n \"or 'only' (deleted rows alone). Omit it to see only the live rows.\",\n \"INVALID_DELETED_PARAM\"\n );\n}\n\n/**\n * `?hard=true` → a real `DELETE` on a collection that soft-deletes.\n *\n * Needs no permission beyond the delete it replaces: it is the same verb, and a\n * second access-control surface for one operation is a second thing to get\n * wrong. What it changes is whether the row can be restored.\n *\n * Only the exact words `true` and `1` mean yes. Anything else is a 400, not a\n * \"no\" — a typo that silently soft-deletes when the caller asked to purge is a\n * caller who believes the data is gone.\n */\nexport function parseHardDelete(raw: unknown): boolean {\n if (raw === undefined || raw === null || raw === \"\") return false;\n const value = String(raw).trim().toLowerCase();\n if (value === \"true\" || value === \"1\") return true;\n if (value === \"false\" || value === \"0\") return false;\n throw ApiError.badRequest(\n `Invalid \\`?${HARD_DELETE_QUERY_PARAM}=${String(raw)}\\`. It takes 'true' or 'false'.`,\n \"INVALID_HARD_PARAM\"\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"soft-delete-params-BmlVwecC.js","names":[],"sources":["../../utils/dist/plurals.es.js","../../utils/dist/names.es.js","../../common/src/util/resolve-relation.ts","../../common/src/util/relations.ts","../../common/src/collections/field-access.ts","../src/api/rest/soft-delete-params.ts"],"sourcesContent":["//#region src/plurals.ts\n/**\n* Returns the plural of an English word.\n*\n* @param {string} word\n* @param {number} [amount]\n* @returns {string}\n*/\nfunction plural(word, amount) {\n\tif (amount !== void 0 && amount === 1) return word;\n\tconst plurals = {\n\t\t\"(quiz)$\": \"$1zes\",\n\t\t\"^(ox)$\": \"$1en\",\n\t\t\"([m|l])ouse$\": \"$1ice\",\n\t\t\"(matr|vert|ind)ix|ex$\": \"$1ices\",\n\t\t\"(x|ch|ss|sh)$\": \"$1es\",\n\t\t\"([^aeiouy]|qu)y$\": \"$1ies\",\n\t\t\"(hive)$\": \"$1s\",\n\t\t\"(?:([^f])fe|([lr])f)$\": \"$1$2ves\",\n\t\t\"(shea|lea|loa|thie)f$\": \"$1ves\",\n\t\tsis$: \"ses\",\n\t\t\"([ti])um$\": \"$1a\",\n\t\t\"(tomat|potat|ech|her|vet)o$\": \"$1oes\",\n\t\t\"(bu)s$\": \"$1ses\",\n\t\t\"(alias)$\": \"$1es\",\n\t\t\"(octop)us$\": \"$1i\",\n\t\t\"(ax|test)is$\": \"$1es\",\n\t\t\"(us)$\": \"$1es\",\n\t\t\"([^s]+)$\": \"$1s\"\n\t};\n\tconst irregular = {\n\t\tmove: \"moves\",\n\t\tfoot: \"feet\",\n\t\tgoose: \"geese\",\n\t\tsex: \"sexes\",\n\t\tchild: \"children\",\n\t\tman: \"men\",\n\t\ttooth: \"teeth\",\n\t\tperson: \"people\"\n\t};\n\tif ([\n\t\t\"sheep\",\n\t\t\"fish\",\n\t\t\"deer\",\n\t\t\"moose\",\n\t\t\"series\",\n\t\t\"species\",\n\t\t\"money\",\n\t\t\"rice\",\n\t\t\"information\",\n\t\t\"equipment\",\n\t\t\"bison\",\n\t\t\"cod\",\n\t\t\"offspring\",\n\t\t\"pike\",\n\t\t\"salmon\",\n\t\t\"shrimp\",\n\t\t\"swine\",\n\t\t\"trout\",\n\t\t\"aircraft\",\n\t\t\"hovercraft\",\n\t\t\"spacecraft\",\n\t\t\"sugar\",\n\t\t\"tuna\",\n\t\t\"you\",\n\t\t\"wood\"\n\t].indexOf(word.toLowerCase()) >= 0) return word;\n\tfor (const w in irregular) {\n\t\tconst pattern = new RegExp(`${w}$`, \"i\");\n\t\tconst replace = irregular[w];\n\t\tif (pattern.test(word)) return word.replace(pattern, replace);\n\t}\n\tfor (const reg in plurals) {\n\t\tconst pattern = new RegExp(reg, \"i\");\n\t\tif (pattern.test(word)) return word.replace(pattern, plurals[reg]);\n\t}\n\treturn word;\n}\n/**\n* Returns the singular of an English word.\n*\n* @param {string} word\n* @param {number} [amount]\n* @returns {string}\n*/\nfunction singular(word, amount) {\n\tif (amount !== void 0 && amount !== 1) return word;\n\tconst singulars = {\n\t\t\"(quiz)zes$\": \"$1\",\n\t\t\"(matr)ices$\": \"$1ix\",\n\t\t\"(vert|ind)ices$\": \"$1ex\",\n\t\t\"^(ox)en$\": \"$1\",\n\t\t\"(alias)es$\": \"$1\",\n\t\t\"(octop|vir)i$\": \"$1us\",\n\t\t\"(cris|ax|test)es$\": \"$1is\",\n\t\t\"(shoe)s$\": \"$1\",\n\t\t\"(o)es$\": \"$1\",\n\t\t\"(bus)es$\": \"$1\",\n\t\t\"([m|l])ice$\": \"$1ouse\",\n\t\t\"(x|ch|ss|sh)es$\": \"$1\",\n\t\t\"(m)ovies$\": \"$1ovie\",\n\t\t\"(s)eries$\": \"$1eries\",\n\t\t\"([^aeiouy]|qu)ies$\": \"$1y\",\n\t\t\"([lr])ves$\": \"$1f\",\n\t\t\"(tive)s$\": \"$1\",\n\t\t\"(hive)s$\": \"$1\",\n\t\t\"(li|wi|kni)ves$\": \"$1fe\",\n\t\t\"(shea|loa|lea|thie)ves$\": \"$1f\",\n\t\t\"(^analy)ses$\": \"$1sis\",\n\t\t\"((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$\": \"$1$2sis\",\n\t\t\"([ti])a$\": \"$1um\",\n\t\t\"(n)ews$\": \"$1ews\",\n\t\t\"(h|bl)ouses$\": \"$1ouse\",\n\t\t\"(corpse)s$\": \"$1\",\n\t\t\"(us)es$\": \"$1\",\n\t\ts$: \"\"\n\t};\n\tconst irregular = {\n\t\tmove: \"moves\",\n\t\tfoot: \"feet\",\n\t\tgoose: \"geese\",\n\t\tsex: \"sexes\",\n\t\tchild: \"children\",\n\t\tman: \"men\",\n\t\ttooth: \"teeth\",\n\t\tperson: \"people\"\n\t};\n\tif ([\n\t\t\"sheep\",\n\t\t\"fish\",\n\t\t\"deer\",\n\t\t\"moose\",\n\t\t\"series\",\n\t\t\"species\",\n\t\t\"money\",\n\t\t\"rice\",\n\t\t\"information\",\n\t\t\"equipment\",\n\t\t\"bison\",\n\t\t\"cod\",\n\t\t\"offspring\",\n\t\t\"pike\",\n\t\t\"salmon\",\n\t\t\"shrimp\",\n\t\t\"swine\",\n\t\t\"trout\",\n\t\t\"aircraft\",\n\t\t\"hovercraft\",\n\t\t\"spacecraft\",\n\t\t\"sugar\",\n\t\t\"tuna\",\n\t\t\"you\",\n\t\t\"wood\"\n\t].indexOf(word.toLowerCase()) >= 0) return word;\n\tfor (const w in irregular) {\n\t\tconst pattern = new RegExp(`${irregular[w]}$`, \"i\");\n\t\tif (pattern.test(word)) return word.replace(pattern, w);\n\t}\n\tfor (const reg in singulars) {\n\t\tconst pattern = new RegExp(reg, \"i\");\n\t\tif (pattern.test(word)) return word.replace(pattern, singulars[reg]);\n\t}\n\treturn word;\n}\n//#endregion\nexport { plural, singular };\n\n//# sourceMappingURL=plurals.es.js.map","import { toSnakeCase } from \"./strings.es.js\";\nimport { singular } from \"./plurals.es.js\";\n//#region src/names.ts\n/**\n* Generates a foreign key column name from a given string, typically a collection slug or name.\n* It singularizes the name, converts it to snake_case and appends '_id'.\n*\n* Singularization runs *before* snake-casing so that acronyms survive: `toSnakeCase`\n* splits on every capital, which turned \"URLs\" into \"ur_ls\" and then \"ur_l_id\".\n*\n* @param name The base name to convert to a foreign key.\n* @returns A foreign key name in the format 'singular_name_id'.\n*\n* @example\n* // returns \"user_id\"\n* generateForeignKeyName(\"users\")\n*\n* @example\n* // returns \"category_id\"\n* generateForeignKeyName(\"categories\")\n*\n* @example\n* // returns \"product_id\"\n* generateForeignKeyName(\"Product\")\n*\n*/\nfunction generateForeignKeyName(name) {\n\treturn `${toSnakeCase(singularizeForKey(name))}_id`;\n}\n/**\n* `singular()` handles real English plurals, but its final catch-all rule strips\n* any trailing \"s\", which mangles words that only look plural. Guard the two\n* cases that produce a column name nobody would recognise:\n*\n* - a double \"s\" ending is never a plural marker (\"address\", \"class\", \"process\"),\n* so stripping it yields \"addres\";\n* - a name that singularizes to nothing (the literal \"s\") would yield \"_id\".\n*/\nfunction singularizeForKey(name) {\n\tif (/ss$/i.test(name)) return name;\n\tconst result = singular(name);\n\treturn result.length > 0 ? result : name;\n}\n/**\n* What `generateForeignKeyName` returned before it learned to singularize:\n* snake-case the name, then chop one trailing \"s\".\n*\n* This is here to be *detected*, never to be generated. A database provisioned\n* under the old rule carries `categorie_id`, `addresse_id`, `children_id` or\n* `ur_l_id` where the current rule expects `category_id`, `address_id`,\n* `child_id` and `url_id` — and the boot-time schema ensure is additive, so it\n* would create the new column empty beside the populated old one and leave the\n* relation reading nothing. No error, no missing table: the failure is silent,\n* which is the only reason this function still exists.\n*\n* `ensureCollectionSchema` calls it to recognise that shape and say so.\n* Returns the same string as `generateForeignKeyName` for every regular plural,\n* so a caller can compare the two and act only when they differ.\n*/\nfunction legacyForeignKeyName(name) {\n\tconst snake = toSnakeCase(name);\n\treturn `${snake.endsWith(\"s\") ? snake.slice(0, -1) : snake}_id`;\n}\n/**\n* Truncate an identifier to what Postgres will actually store.\n*\n* Postgres silently truncates identifiers at NAMEDATALEN-1 = 63 **bytes**, so a\n* name generated longer than that is not the name the database ends up holding.\n* Anything that later looks the object up by the name it generated then misses.\n*\n* Byte length, not string length: NAMEDATALEN is a byte bound, and a multi-byte\n* character straddling the boundary would be cut mid-sequence by `slice(0, 63)`.\n*\n* `TextEncoder` rather than `Buffer`, which is not a matter of taste: `Buffer`\n* is a Node global, and this package is imported by browser-facing ones. It\n* typechecked only where `@types/node` happened to be in scope, so\n* `packages/codegen` — whose tsconfig is `lib: [\"ESNext\", \"dom\"]` — could not\n* compile the file at all, and both of its suites failed to run. `TextEncoder`\n* and `TextDecoder` are standard in both runtimes and need no ambient types.\n*/\nfunction toPostgresIdentifier(name) {\n\treturn truncateToBytes(name, 63);\n}\n/**\n* {@link toPostgresIdentifier} with the bound lifted to a parameter.\n*\n* Exists for names that end in something load-bearing. Truncating at 63 keeps\n* the *head* of a name and discards the tail, which is right for a descriptive\n* identifier and wrong for a hashed one: the hash is the part that makes it\n* unique, and it is at the end. A caller that appends a fingerprint truncates\n* the readable head to `63 - <tail>` itself and then appends, so the bound is\n* still 63 and the hash always survives.\n*\n* `contracts/derived-names.txt` records what the alternative costs — a foreign\n* key frozen as `..._corres`, its `_fkey` suffix truncated away, so a second\n* foreign key on that table would derive a byte-identical name.\n*\n* One truncation rule, in one function, so the two cannot drift.\n*/\nfunction truncateToBytes(name, maxBytes) {\n\tconst bytes = new TextEncoder().encode(name);\n\tif (bytes.byteLength <= maxBytes) return name;\n\treturn new TextDecoder(\"utf-8\").decode(bytes.subarray(0, maxBytes)).replace(/�+$/, \"\");\n}\n/**\n* The API name a database column is served under.\n*\n* The wire name of a field is its property key, and Rebase's property keys are\n* camelCase — `displayName`, `createdAt`, `photoURL`. Columns are snake_case,\n* because an unquoted Postgres identifier folds to lower case and a camelCase\n* column is therefore reachable only as `\"authorId\"` forever: in hand-written\n* SQL, in psql, in an RLS policy body, in a dump, and in every third-party tool\n* that ever touches the database. So the two conventions are both right, and\n* this is the function that crosses between them.\n*\n* It exists because two sources of field names never crossed: a foreign key\n* derived from a relation (`author_id`) and a column read back by introspection\n* (`user_id`) both landed on the wire under their column name, while every\n* hand-authored collection next to them used camelCase. One API, two\n* conventions, and no rule a caller could infer from outside — those names are\n* also the `where` and `orderBy` keys, so it was not a matter of taste.\n*\n* Rules, in the order they matter:\n*\n* - **A name with no separator is returned unchanged.** `photoURL` stays\n* `photoURL` and `id` stays `id`. Lower-casing a single token is what makes\n* a \"camelCase\" helper destructive — `camelCase(\"photoURL\")` is `photourl` —\n* and this function is applied to names that are *already* keys.\n* - **Each following segment keeps its own casing** apart from an upper-cased\n* first letter, so `photo_URL` → `photoURL` rather than `photoUrl`.\n* - **The result may still not be a JavaScript identifier.** `2fa_enabled`\n* becomes `2faEnabled`, which is a perfectly good object key and still needs\n* quoting where one is written into generated source.\n*\n* Not the inverse of {@link toSnakeCase}: `toSnakeCase` tokenises on case\n* boundaries and would turn `photoURL` into `photo_url`. Round-tripping is not\n* a property either function promises, which is why a column name that a\n* property maps explicitly is always read off `columnName` rather than derived.\n*/\nfunction toWireKey(columnName) {\n\tif (!columnName) return columnName;\n\tconst segments = columnName.split(/[-_ ]+/).filter(Boolean);\n\tif (segments.length <= 1) return columnName;\n\treturn segments.map((segment, index) => index === 0 ? segment.charAt(0).toLowerCase() + segment.slice(1) : segment.charAt(0).toUpperCase() + segment.slice(1)).join(\"\");\n}\n/**\n* The first candidate key not already used, or a numbered fallback.\n*\n* Introspection turns a set of column names into a set of object keys, and the\n* mapping is not injective: `user_id` and `userId` are two columns and one\n* {@link toWireKey}, and two foreign keys can strip to the same relation name.\n* A duplicate key in a generated object literal is a TypeScript error, so the\n* whole collection stops compiling — and a duplicate key in a `Record` built at\n* runtime is worse, because it silently drops a column instead.\n*\n* The numbered tail is what makes this total: a function that returns a key it\n* cannot guarantee is free has only moved the duplicate one line down.\n*\n* Structurally typed on `has` so a `Map` of emitted blocks and a `Set` of taken\n* names both satisfy it. Lives here, in the package both introspection\n* producers and the admin's table import can reach, because they must resolve a\n* collision the same way or one database describes itself three ways.\n*/\nfunction firstFreeKey(candidates, taken) {\n\tfor (const candidate of candidates) if (!taken.has(candidate)) return candidate;\n\tconst base = candidates[candidates.length - 1];\n\tfor (let suffix = 2;; suffix++) {\n\t\tconst candidate = `${base}_${suffix}`;\n\t\tif (!taken.has(candidate)) return candidate;\n\t}\n}\n//#endregion\nexport { firstFreeKey, generateForeignKeyName, legacyForeignKeyName, toPostgresIdentifier, toWireKey, truncateToBytes };\n\n//# sourceMappingURL=names.es.js.map","import {\n CollectionConfig,\n Relation,\n ResolvedRelation\n} from \"@rebasepro/types\";\nimport { generateForeignKeyName, toSnakeCase } from \"@rebasepro/utils\";\n\nimport { getTableName } from \"./relations\";\n\n/**\n * Fill in a relation's defaults.\n *\n * This replaces `sanitizeRelation`, which had to work out *which kind of link\n * you meant* from whichever optional fields happened to be set — 194 lines of\n * it, including a pass that inspected the target collection's own relations to\n * decide whether a `many`/`inverse` pair was a one-to-many or the far side of a\n * many-to-many, wrapped in a `try/catch` that fell through to the wrong answer\n * when it could not tell. Two consumers running that logic at different moments\n * could reach different conclusions about the same relation.\n *\n * With the kind declared there is nothing to work out. What remains is\n * defaulting — a table name, a column name — which is deterministic, depends\n * only on the relation and its two endpoints, and cannot fail. That is why this\n * function returns rather than throws, and why it needs no cache to be\n * consistent.\n */\nexport function resolveRelation(\n relation: Relation,\n sourceCollection: CollectionConfig,\n propertyKey?: string\n): ResolvedRelation {\n const target = relation.target;\n if (typeof target !== \"function\") {\n throw new Error(\n `Relation${relation.relationName ? ` '${relation.relationName}'` : \"\"} on ` +\n `'${sourceCollection.slug}' has no \\`target\\`. Give it a thunk: \\`target: () => otherCollection\\`.`\n );\n }\n\n const targetCollection = callTarget(relation, sourceCollection, propertyKey, target);\n\n // The name is the address: the `include` key, the admin tab, and the\n // segment of a nested path. Declared name wins, then the declaring\n // property's key, then the target's slug.\n const relationName = relation.relationName ?? propertyKey ?? toSnakeCase(targetCollection.slug);\n\n const shared: Pick<ResolvedRelation, \"relationName\" | \"target\" | \"targetSlug\" | \"onUpdate\" | \"onDelete\" | \"overrides\"> = {\n relationName,\n // Normalised, not the thunk as written. Resolution reads the target once\n // and every later consumer calls it again — the driver building a join,\n // the DDL and policy generators, the admin's relation fields — so\n // handing back the raw thunk would give all of them the module namespace\n // `callTarget` just looked past, and the fix would hold only for the\n // fields resolution happens to read here. Still lazy: same call at the\n // same moment, one unwrap on the way out.\n target: () => unwrapModuleNamespace(target()) as CollectionConfig,\n targetSlug: targetCollection.slug,\n onUpdate: relation.onUpdate,\n onDelete: relation.onDelete,\n overrides: relation.overrides\n // No `validation`. Whether the link is required is a fact about the\n // *property*, and copying it onto the resolved relation gave the\n // question two answers that were free to disagree. Ask\n // `isRelationRequired(collection, relation)`, which reads the one.\n };\n\n const sourceName = toSnakeCase(sourceCollection.slug ?? sourceCollection.name);\n\n switch (relation.kind) {\n case \"belongsTo\":\n return {\n ...shared,\n kind: \"belongsTo\",\n cardinality: \"one\",\n writable: true,\n shared: false,\n localKey: relation.localKey ?? generateForeignKeyName(relationName),\n index: relation.index !== false\n };\n\n case \"hasOne\":\n return {\n ...shared,\n kind: \"hasOne\",\n cardinality: \"one\",\n writable: true,\n shared: false,\n foreignKeyOnTarget: relation.foreignKeyOnTarget ?? generateForeignKeyName(sourceName),\n sourceKey: relation.sourceKey\n };\n\n case \"hasMany\":\n return {\n ...shared,\n kind: \"hasMany\",\n cardinality: \"many\",\n writable: true,\n shared: false,\n foreignKeyOnTarget: relation.foreignKeyOnTarget ?? generateForeignKeyName(sourceName),\n // Not defaulted: the source's primary key needs the driver's\n // schema to resolve, which resolution does not have. `undefined`\n // means \"the primary key\" — see `ResolvedHasMany.sourceKey`.\n sourceKey: relation.sourceKey\n };\n\n case \"manyToMany\": {\n const sourceTable = getTableName(sourceCollection);\n const targetTable = getTableName(targetCollection);\n return {\n ...shared,\n kind: \"manyToMany\",\n cardinality: \"many\",\n writable: true,\n shared: true,\n through: {\n // Sorted so both sides of the same link derive the same\n // table without having to agree in advance.\n table: relation.through?.table ?? [sourceTable, targetTable].sort().join(\"_\"),\n sourceColumn: relation.through?.sourceColumn ?? generateForeignKeyName(sourceName),\n targetColumn: relation.through?.targetColumn ?? generateForeignKeyName(relationName),\n // `{}` rather than `undefined`, for the reason every other\n // field here is filled in: a consumer reads one shape and\n // does not have to decide what an absent payload means.\n properties: relation.through?.properties ?? {}\n }\n };\n }\n\n case \"via\":\n return {\n ...shared,\n kind: \"via\",\n cardinality: relation.cardinality,\n writable: false,\n // A join chain reaches rows that other parents reach too, and\n // Rebase does not know which hop, if any, is a link it owns.\n shared: true,\n joinPath: relation.joinPath\n };\n\n default: {\n // Exhaustive: a new kind is a compile error here, not a silent\n // fall-through to whatever shape happened to match first.\n const exhaustive: never = relation;\n throw new Error(`Unknown relation kind: ${JSON.stringify(exhaustive)}`);\n }\n }\n}\n\n/** How this relation is addressed in an error message, before it has a resolved name. */\nfunction describe(relation: Relation, sourceCollection: CollectionConfig, propertyKey?: string): string {\n const name = relation.relationName ?? propertyKey;\n return `Relation${name ? ` '${name}'` : \"\"} on '${sourceCollection.slug}'`;\n}\n\n/**\n * A module namespace, unwrapped to the collection it exports.\n *\n * A cycle transpiled to CommonJS does not hand the importing module the\n * *default export* — it hands it the module object, `{ __esModule: true,\n * default: … }`, captured before the exporting module finished evaluating. The\n * `default` slot fills in later, so by the time a lazy `target` thunk runs the\n * collection is sitting right there, one level down. Returning the namespace is\n * never a thing a thunk means to do, and there is exactly one reading of it.\n *\n * Only unwrapped when the inner value is itself a collection: a `default` that\n * is not one is a genuinely wrong thunk, and it should reach the error below\n * rather than be quietly swapped in.\n */\nfunction unwrapModuleNamespace(value: unknown): unknown {\n if (!value || typeof value !== \"object\") return value;\n if ((value as { slug?: unknown }).slug) return value;\n const inner = (value as { default?: unknown }).default;\n return inner && typeof inner === \"object\" && (inner as { slug?: unknown }).slug ? inner : value;\n}\n\n/**\n * Call the `target` thunk, and translate the ways an import cycle breaks it into\n * an error that names the cause — or, where the value is recoverable, into the\n * collection the thunk meant.\n *\n * The thunk exists to defer the reference until every module has finished\n * evaluating, and for a cycle that closes at import time it does. Two cycles\n * leave the binding permanently unusable:\n *\n * - **ESM/TDZ.** `const` and `class` bindings in a not-yet-evaluated module are\n * in the temporal dead zone, so reading one throws `ReferenceError: x is not\n * defined`. The stack points at the thunk — a one-line arrow function that is\n * obviously fine — and says nothing about the cycle that made it throw.\n * - **CJS interop, unresolved.** The half-initialised module object has no\n * `default` yet, the import resolves to `undefined`, and the thunk returns it\n * without complaint. That one used to surface here as \"did not resolve to a\n * collection\", which is true and unhelpful.\n *\n * Both mean the same thing, and the fix for both is the same: break the cycle,\n * or move the relation into the collection that does not close it.\n *\n * A third shape is *not* an error, and used to be reported as one. A loader that\n * transpiles ESM to CJS — jiti, which is what `rebase generate-sdk` and\n * `rebase build` load collections with — gives the module entered second in a\n * cycle a namespace object rather than the default export, and never replaces it\n * with a live binding. The thunk then returns `{ __esModule: true, default: … }`\n * holding the fully-initialised collection. Native ESM resolves the same thunk\n * to the collection directly, so this was a loader artefact reported as an\n * authoring mistake, and the advice it gave — make the target a lazy thunk — was\n * already satisfied by the code it was rejecting. Bidirectional relations make\n * these cycles unavoidable, and the lazy thunk is this framework's own answer to\n * them, so {@link unwrapModuleNamespace} takes the collection and moves on.\n */\nfunction callTarget(\n relation: Relation,\n sourceCollection: CollectionConfig,\n propertyKey: string | undefined,\n target: Relation[\"target\"]\n): ReturnType<Relation[\"target\"]> {\n let targetCollection: ReturnType<Relation[\"target\"]> | undefined;\n try {\n targetCollection = unwrapModuleNamespace(target()) as ReturnType<Relation[\"target\"]>;\n } catch (error) {\n // A ReferenceError from inside the thunk is a binding that was never\n // initialised — nothing else in a one-expression arrow can raise one.\n if (error instanceof ReferenceError) {\n throw new Error(\n `${describe(relation, sourceCollection, propertyKey)} targets a collection that is not ` +\n `initialized yet — almost always an import cycle between the two collection files. ` +\n `Break the cycle (move the shared piece into a third module, or import the target ` +\n `lazily) so the target's module finishes evaluating before the registry is built.`,\n { cause: error }\n );\n }\n throw error;\n }\n\n if (!targetCollection?.slug) {\n throw new Error(\n `${describe(relation, sourceCollection, propertyKey)} has a \\`target\\` that resolved to ` +\n `${targetCollection === undefined ? \"`undefined`\" : \"something that is not a collection\"}. ` +\n (targetCollection === undefined\n ? \"Under CommonJS interop an import cycle resolves the default import to `undefined`, \" +\n \"so check whether this collection and its target import each other. Otherwise the thunk \" +\n \"is returning the wrong value — it must return the collection itself, not a promise or a module.\"\n : typeof (targetCollection as { then?: unknown }).then === \"function\"\n ? \"The thunk returned a promise — `target: () => import(\\\"./other\\\")` is asynchronous. \" +\n \"Import the collection at the top of the file and return the binding: \" +\n \"`target: () => otherCollection`.\"\n : \"The thunk must return a collection config with a `slug`.\")\n );\n }\n\n return targetCollection;\n}\n","import { CollectionConfig, isRelationalCollectionConfig, Property, ResolvedRelation, RelationProperty } from \"@rebasepro/types\";\nimport { toSnakeCase, toWireKey } from \"@rebasepro/utils\";\n\nimport { resolveRelation } from \"./resolve-relation\";\n\n/**\n * Whether the target rows are shared with other parents — a many-to-many, or a\n * multi-hop `via` chain.\n *\n * Decides what a write \"through\" the relation may touch: a shared target\n * belongs to every parent that links it, so the parent owns the *link* and not\n * the row. The backend enforces that (an unlink rather than a delete) and the\n * admin renders it (remove-from-parent rather than delete).\n *\n * Now a field on the resolved relation rather than a re-derivation, so both\n * sides read the same answer instead of each computing one.\n */\nexport function isJunctionBackedRelation(relation: ResolvedRelation): boolean {\n return relation.shared;\n}\n\n/** WeakMap cache — same collection instance always yields the same relation map. */\nconst _resolvedRelationsCache = new WeakMap<CollectionConfig, Record<string, ResolvedRelation>>();\n\n/**\n * Every relation a collection declares, keyed by the name it is addressed by.\n *\n * A relation reaches the map from either of two places — the collection's\n * `relations` array, or a `relation` property that declares one inline — and is\n * keyed by its resolved `relationName`, which is what a nested path segment,\n * an `include` key and an admin tab all match against.\n *\n * Resolution no longer swallows failures. It used to wrap each relation in a\n * `try/catch` that dropped anything it could not work out, so a\n * mis-declared relation silently vanished instead of being reported; with the\n * kind declared, the only remaining failure is a `target` that does not resolve,\n * which is worth hearing about.\n */\nexport function resolveCollectionRelations(\n collection: CollectionConfig\n): Record<string, ResolvedRelation> {\n const cached = _resolvedRelationsCache.get(collection);\n if (cached) return cached;\n\n if (!isRelationalCollectionConfig(collection)) return {};\n\n const relations: Record<string, ResolvedRelation> = {};\n\n for (const relation of collection.relations ?? []) {\n const resolved = resolveRelation(relation, collection);\n relations[resolved.relationName] = resolved;\n }\n\n // A property declaring a relation inline is registered under the property\n // key as well: the fetch layer hydrates the result back onto that key, and\n // it is the name the admin addresses the field by.\n for (const [propertyKey, property] of Object.entries(collection.properties ?? {})) {\n if ((property as Property)?.type !== \"relation\") continue;\n const declared = (property as RelationProperty).relation;\n if (!declared || relations[propertyKey]) continue;\n\n relations[propertyKey] = resolveRelation(declared, collection, propertyKey);\n }\n\n _resolvedRelationsCache.set(collection, relations);\n return relations;\n}\n\n/**\n * The `type: \"relation\"` property that declares a link, or `undefined` for one\n * that only exists in the collection's `relations` array.\n *\n * Both declaration sites end up in {@link resolveCollectionRelations}, and only\n * one of them has a property to carry field-level facts — `name`, `admin`, and\n * the one this exists for, `validation.required`.\n */\nexport function relationDeclaringProperty(\n collection: CollectionConfig,\n relation: ResolvedRelation\n): RelationProperty | undefined {\n const resolved = resolveCollectionRelations(collection);\n for (const [key, raw] of Object.entries(collection.properties ?? {})) {\n const prop = raw as Property | undefined;\n if (prop?.type !== \"relation\") continue;\n // A relation declared inline is keyed by the property; one declared in\n // `relations` is keyed by its name, which the property addresses.\n if (resolved[key] === relation) return prop as RelationProperty;\n const addressed = (prop as RelationProperty).relation?.relationName;\n if (addressed && findRelation(resolved, addressed) === relation) return prop as RelationProperty;\n }\n return undefined;\n}\n\n/**\n * Must every row of this collection point at a target through this link?\n *\n * Read from the declaring property's `validation.required` — the same key every\n * other field uses, and the only place it lives.\n *\n * `RelationBase` carried its own `validation.required` until 0.18, which made\n * this two questions rather than one. They were answered by different readers:\n * the Postgres DDL generator asked the property (so the foreign-key column was\n * `NOT NULL`) and the SDK type generator asked the relation (so the generated\n * `Insert` type made the field optional). A `create()` that left the relation\n * out therefore typechecked and then failed at the database with a not-null\n * violation, and the two `required`s had to be written twice, identically, for\n * the pair to agree.\n *\n * A relation with no declaring property — an entry in `relations` nothing\n * points at — is not required. There is no field to fill in.\n */\nexport function isRelationRequired(collection: CollectionConfig, relation: ResolvedRelation): boolean {\n return Boolean(relationDeclaringProperty(collection, relation)?.validation?.required);\n}\n\n/**\n * The path of the collection a relation property points at, derived from the\n * property alone.\n *\n * A preview holds a property and a value and no collection, so it cannot call\n * `resolveRelationProperty`. It does not need to: both forms that carry a\n * target — the stamped `resolvedRelation` and the inline `relation` — name it\n * directly. Only the third form, a relation declared by name in the\n * collection's `relations` array, is out of reach, and that one has no target\n * to read without the collection anyway.\n *\n * This is what lets a preview render a relation column that arrived as a bare\n * foreign key: the id says *which* row, the declared target says *which\n * collection*, and `RelationPreview` fetches the rest. Without it a scalar id\n * is indistinguishable from a value of the wrong type.\n */\nexport function getRelationTargetPath(property: RelationProperty): string | undefined {\n const stamped = property.resolvedRelation?.targetSlug;\n if (stamped) return stamped;\n\n const target = property.relation?.target;\n if (typeof target !== \"function\") return undefined;\n try {\n return target()?.slug;\n } catch (_e) {\n // A thunk reaching into a module that has not finished initialising:\n // there is no target to name yet, and a preview is not worth throwing over.\n return undefined;\n }\n}\n\n/**\n * The table a collection reads and writes.\n *\n * `table` when it is set, otherwise `toSnakeCase(slug)` — which is what made it\n * safe to drop `table` from the required fields on the config type: the runtime\n * had always derived it, and the type was demanding a value it did not need.\n *\n * The `||` chain is load-bearing. `toSnakeCase(undefined)` returns `\"\"`, not\n * `undefined`, so the previous `??` chain short-circuited on the empty string\n * and the name fallback could never run — a safety net that read like one and\n * caught nothing. It was unreachable while `slug` was required; it stops being\n * unreachable the moment anything constructs a config without one.\n */\nexport function getTableName(collection: CollectionConfig): string {\n const declared = isRelationalCollectionConfig(collection) ? collection.table : undefined;\n return declared || toSnakeCase(collection.slug) || toSnakeCase(collection.name);\n}\n\n/**\n * A JavaScript identifier: what a generated `export const <name> =` needs.\n *\n * Deliberately the same shape the two schema generators already define\n * privately — this is the third place that needed it, and the first two guard\n * property keys and member accesses while nothing guarded the variable name\n * itself.\n */\nconst JS_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n\n/**\n * The variable name a generated table is bound to.\n *\n * Camel-cases underscores, and then guarantees the result is a legal\n * identifier. It did only the first, so a table name that is legal in Postgres\n * and not in JavaScript produced a `schema.generated.ts` that does not parse:\n *\n * `2024_archive` → `export const 2024Archive = pgTable(…)`\n * \"An identifier or keyword cannot immediately follow\n * a numeric literal\"\n * `reporting.events` → `export const reporting.events = pgTable(…)`\n * \"',' expected\"\n *\n * That file is imported by the server, so the failure is not one broken\n * collection — `rebase build` and `db push` fail at tsc for the whole\n * directory. And it is reachable from a documented flow: `rebase init` against\n * a database holding a table called `2024_archive` writes a collection file\n * that parses and a schema file that does not.\n *\n * **A no-op for every name that already worked**, which is what makes changing\n * a derived name safe here: the only inputs whose output changes are the ones\n * that produced a syntax error, and nothing can be running against those.\n * Separators become camel case rather than disappearing, so `reporting.events`\n * and `reporting_events` do not collide into one variable.\n */\nexport function getTableVarName(tableName: string): string {\n const camel = tableName.replace(/_([a-z])/g, (_, char: string) => char.toUpperCase());\n if (JS_IDENTIFIER.test(camel)) return camel;\n\n const sanitised = camel\n // Any other separator gets the same treatment `_` did, so two tables\n // differing only by separator keep differing.\n .replace(/[^A-Za-z0-9_$]+([A-Za-z0-9])?/g, (_, char?: string) =>\n (char ? char.toUpperCase() : \"\"))\n // A leading digit is legal in Postgres and not in JavaScript. Prefixed\n // rather than stripped, so `2024_archive` and `archive` stay distinct.\n .replace(/^([0-9])/, \"t$1\");\n\n return JS_IDENTIFIER.test(sanitised) ? sanitised : `t${sanitised}`;\n}\n\nexport function getEnumVarName(tableName: string, propName: string): string {\n const tableVar = getTableVarName(tableName);\n const propVar = propName.charAt(0).toUpperCase() + propName.slice(1);\n return `${tableVar}${propVar}`;\n}\n\nexport function getColumnName(fullColumn: string): string {\n return fullColumn.includes(\".\") ? fullColumn.split(\".\").pop()! : fullColumn;\n}\n\n/**\n * The field key a database column is served and addressed under.\n *\n * A column has two names and they are not the same name. `author_id` is what\n * Postgres stores; `authorId` is the key on the JSON row, the key in the\n * generated Drizzle table, and the key a caller writes in `where` and\n * `orderBy`. Every place that starts from a column and has to reach a row, a\n * Drizzle table or a payload goes through here, so there is one answer rather\n * than one per call site — the two that disagreed put `displayName` and\n * `author_id` on the same API.\n *\n * A declared property is the authority when there is one, because its key *is*\n * the wire name and `columnName` is the only thing that ever renamed the\n * column:\n *\n * 1. an explicit `columnName` equal to this column;\n * 2. a property whose key is literally the column (an author who wrote\n * `author_id:` meant `author_id` on the wire, and gets it);\n * 3. a property whose key snake-cases to the column, which is the default\n * mapping — `authorId` → `author_id`.\n *\n * With no property in the way — a foreign key derived from a relation, which\n * usually has none — the name is derived: {@link toWireKey}.\n *\n * Note the fallback is *not* the column verbatim. That was the old behaviour\n * and it is precisely the defect: a derived foreign key reached the wire under\n * its column name while every hand-authored field beside it was camelCase.\n */\nexport function fieldKeyForColumn(collection: CollectionConfig | undefined, column: string): string {\n const properties = collection?.properties;\n if (properties) {\n for (const [key, prop] of Object.entries(properties)) {\n const columnName = (prop as { columnName?: unknown } | undefined)?.columnName;\n if (typeof columnName === \"string\" && columnName === column) return key;\n }\n for (const key of Object.keys(properties)) {\n if (key === column) return key;\n if (toSnakeCase(key) === column) return key;\n }\n }\n return toWireKey(column);\n}\n\n/**\n * Look up a relation by key with forgiving normalization.\n *\n * `resolveCollectionRelations` stores each relation under a single canonical\n * key (no aliases). This helper tries the given key as-is, then falls back to\n * slug form (underscores → hyphens) and snake_case form (hyphens → underscores)\n * so that callers that receive a key from external input (URL path segments,\n * user-provided config, etc.) can still find the right entry.\n */\nexport function findRelation(\n resolvedRelations: Record<string, ResolvedRelation>,\n key: string\n): ResolvedRelation | undefined {\n // Exact match first\n if (resolvedRelations[key]) return resolvedRelations[key];\n\n // Try slug form (e.g. \"company_id\" → \"company-id\")\n const slugKey = key.replace(/_/g, \"-\");\n if (slugKey !== key && resolvedRelations[slugKey]) return resolvedRelations[slugKey];\n\n // Try snake_case form (e.g. \"company-id\" → \"company_id\")\n const snakeKey = key.replace(/-/g, \"_\");\n if (snakeKey !== key && resolvedRelations[snakeKey]) return resolvedRelations[snakeKey];\n\n return undefined;\n}\n","import type { CollectionConfig, FieldAccess, Property } from \"@rebasepro/types\";\nimport { fieldKeyForColumn, findRelation, resolveCollectionRelations } from \"../util/relations\";\n\n/**\n * Field-level access control: one mechanism, read by every enforcement point.\n *\n * A collection's `securityRules` decide which *rows* a caller reaches;\n * `property.access` decides which *fields* of a reached row they see and may\n * set. The two are independent — a field rule never widens row access, and a\n * row a caller cannot read has no fields to talk about.\n *\n * `excludeFromApi` is sugar for `access: { read: [], write: [] }` and is\n * normalised into it by {@link effectiveAccess}, which is the only place either\n * spelling is read. It used to be its own code path in five files — the read\n * strip, the write refusal, the SDK generator, the OpenAPI schema builder and\n * the filter-parameter builder — and the second rule would have made ten.\n * There is one predicate now, and the flag is a shorthand for it.\n *\n * @module\n */\n\n/**\n * The caller a field rule is judged against: whatever the call context carries\n * as the user's application roles.\n *\n * `undefined` is the trusted server plane — an in-process `rebase.data` call\n * with no request behind it, the auth adapter writing a password hash, a\n * migration. Every API boundary has a viewer: an unauthenticated REST request is\n * scoped as `{ uid: ANONYMOUS_USER_ID, roles: [\"anon\"] }` before it reaches a\n * driver, so \"no viewer\" cannot be reached from outside.\n */\nexport interface FieldViewer {\n roles?: readonly string[];\n}\n\n/**\n * The role that satisfies any non-empty list.\n *\n * The same arm every baseline policy carries: `security_rules` injects\n * `rolesOverlap(['admin'])` into the default read and write policies, and\n * `rebase.dataAsAdmin` is scoped with `{ uid: \"service\", roles: [\"admin\"] }`.\n * Without this an author could declare `access: { read: [\"hr\"] }` and lock the\n * administrator out of a column of their own database — and lock the Studio out\n * of rendering it.\n */\nexport const ADMIN_ROLE = \"admin\";\n\n/**\n * What a property's access rules actually are, with `excludeFromApi` expanded.\n *\n * Returns `undefined` when the property constrains nothing, so callers can skip\n * the whole check for the overwhelmingly common case.\n */\nexport function effectiveAccess(property: Property | undefined): FieldAccess | undefined {\n if (!property) return undefined;\n if (property.excludeFromApi) return EXCLUDED_ACCESS;\n const access = property.access;\n if (!access) return undefined;\n if (access.read === undefined && access.write === undefined) return undefined;\n return access;\n}\n\n/** The rule `excludeFromApi: true` expands to. Frozen: it is shared by every caller. */\nconst EXCLUDED_ACCESS: FieldAccess = Object.freeze({ read: Object.freeze([]), write: Object.freeze([]) });\n\n/**\n * Does a caller holding `roles` satisfy `allowed`?\n *\n * Three cases, and the middle one is the one worth stating out loud:\n *\n * - `allowed` omitted — the field carries no rule of its own, so the row's\n * policies have already answered. True.\n * - `allowed` empty — nobody, at any privilege, through any API. Not the admin,\n * not the service key, not the trusted plane reading on a caller's behalf.\n * This is what `excludeFromApi` has always meant on the read side, and\n * collapsing the two spellings means the empty list has to keep meaning it.\n * - `allowed` non-empty — one of the named roles, or `admin`, or no viewer at\n * all (the trusted server plane, which is not an API caller).\n */\nfunction satisfies(allowed: readonly string[] | undefined, viewer: FieldViewer | undefined): boolean {\n if (allowed === undefined) return true;\n if (allowed.length === 0) return false;\n if (!viewer) return true;\n const roles = viewer.roles;\n if (!roles || roles.length === 0) return false;\n return roles.includes(ADMIN_ROLE) || allowed.some(role => roles.includes(role));\n}\n\n/** May this caller receive this field's value? */\nexport function canReadField(property: Property | undefined, viewer: FieldViewer | undefined): boolean {\n const access = effectiveAccess(property);\n return access ? satisfies(access.read, viewer) : true;\n}\n\n/** May this caller set this field's value? */\nexport function canWriteField(property: Property | undefined, viewer: FieldViewer | undefined): boolean {\n const access = effectiveAccess(property);\n return access ? satisfies(access.write, viewer) : true;\n}\n\n/**\n * The names on this collection a caller may not touch, in the two spellings a\n * caller can write them in.\n *\n * `declared` is the property keys, which is what has to leave a *known-fields*\n * set. `refused` is those plus the physical column names behind them: a caller\n * who knows the table can send `password_hash` as readily as `passwordHash`, and\n * a rule that only knew the wire name would be one rename away from useless.\n *\n * A restricted to-one relation takes its foreign key with it. `bandId: 7` names\n * the band exactly as `band: { id: 7 }` does, and the row carries the column\n * beside the relation — so a rule on the relation that left the key alone\n * withheld the value from the row and still let `?bandId=7` ask for it.\n *\n * `kind` picks which half of the rule is read; nothing else differs.\n */\nexport function restrictedFieldNames(\n collection: CollectionConfig,\n viewer: FieldViewer | undefined,\n kind: \"read\" | \"write\"\n): { declared: string[]; refused: Set<string> } {\n const allowed = kind === \"read\" ? canReadField : canWriteField;\n return namesWhere(collection, property => !allowed(property, viewer));\n}\n\n/**\n * The names on this collection that the API does not have, in every spelling\n * a caller could write them in — the `excludeFromApi` properties, their column\n * names, and the foreign key of an excluded to-one relation.\n *\n * Not the same set as {@link restrictedFieldNames}. A field restricted by role\n * is public by name: OpenAPI and `list_collections` describe it, and refusing a\n * query on it with \"not readable\" tells the caller nothing they could not read\n * there. An `excludeFromApi` field is described nowhere, so a query naming one\n * has to be answered as a query naming a column that does not exist — anything\n * else confirms that it does.\n */\nexport function apiHiddenFieldNames(collection: CollectionConfig): Set<string> {\n return namesWhere(collection, property => Boolean(property?.excludeFromApi)).refused;\n}\n\nfunction namesWhere(\n collection: CollectionConfig,\n restricted: (property: Property) => boolean\n): { declared: string[]; refused: Set<string> } {\n const declared: string[] = [];\n const refused = new Set<string>();\n const properties = collection.properties ?? {};\n // Resolved only when a restricted property is a relation, which is rare:\n // the read strip runs this on every row.\n let relations: ReturnType<typeof resolveCollectionRelations> | undefined;\n\n const restrict = (name: string) => {\n if (name in properties && !declared.includes(name)) declared.push(name);\n refused.add(name);\n };\n\n for (const [name, property] of Object.entries(properties)) {\n if (!restricted(property as Property)) continue;\n restrict(name);\n const columnName = (property as Property).columnName;\n if (columnName) refused.add(columnName);\n if ((property as Property).type === \"relation\") {\n relations ??= resolveCollectionRelations(collection);\n const relation = findRelation(relations, name);\n if (relation?.kind === \"belongsTo\") {\n restrict(fieldKeyForColumn(collection, relation.localKey));\n refused.add(relation.localKey);\n }\n }\n }\n return { declared, refused };\n}\n\n/**\n * True when nothing on this collection restricts a field, for either direction.\n *\n * Every read of every row runs through the strip, so the collection that has no\n * rules — which is almost all of them — has to cost one property walk and no\n * allocation.\n */\nexport function hasFieldAccessRules(collection: CollectionConfig): boolean {\n for (const property of Object.values(collection.properties ?? {})) {\n if (effectiveAccess(property as Property)) return true;\n }\n return false;\n}\n","import { ApiError } from \"../errors\";\n\n/**\n * The two query parameters soft delete adds to the REST surface.\n *\n * Kept in their own module so the call sites in `query-parser.ts` and the\n * delete routes are a single line each: the parsing rules belong to soft\n * delete, not to the parser, and a rule spread across the two files that read\n * it is a rule that drifts.\n */\n\n/** `?deleted=` — what to do about rows a soft delete has stamped. */\nexport const DELETED_QUERY_PARAM = \"deleted\";\n/** `?hard=` — ask for a real `DELETE` on a soft-delete collection. */\nexport const HARD_DELETE_QUERY_PARAM = \"hard\";\n\n/**\n * `?deleted=include|only` → the driver's `withDeleted`.\n *\n * Spelled `deleted` on the wire and `withDeleted` in the driver, deliberately:\n * the URL reads as a question about the rows (`?deleted=only` — \"only the\n * deleted ones\"), and the driver option reads as an instruction about the query.\n *\n * A value neither word is a 400 rather than a silent fallback to the default.\n * `?deleted=true` quietly hiding every deleted row is the worst of both: it\n * looks like it worked and answers the opposite question. Absent is the\n * default, which is \"hide them\".\n */\nexport function parseWithDeleted(raw: unknown): boolean | \"only\" | undefined {\n if (raw === undefined || raw === null || raw === \"\") return undefined;\n const value = String(raw).trim().toLowerCase();\n if (value === \"include\") return true;\n if (value === \"only\") return \"only\";\n throw ApiError.badRequest(\n `Invalid \\`?${DELETED_QUERY_PARAM}=${String(raw)}\\`. It takes 'include' (live rows and deleted ones) ` +\n \"or 'only' (deleted rows alone). Omit it to see only the live rows.\",\n \"INVALID_DELETED_PARAM\"\n );\n}\n\n/**\n * `?hard=true` → a real `DELETE` on a collection that soft-deletes.\n *\n * Needs no permission beyond the delete it replaces: it is the same verb, and a\n * second access-control surface for one operation is a second thing to get\n * wrong. What it changes is whether the row can be restored.\n *\n * Only the exact words `true` and `1` mean yes. Anything else is a 400, not a\n * \"no\" — a typo that silently soft-deletes when the caller asked to purge is a\n * caller who believes the data is gone.\n */\nexport function parseHardDelete(raw: unknown): boolean {\n if (raw === undefined || raw === null || raw === \"\") return false;\n const value = String(raw).trim().toLowerCase();\n if (value === \"true\" || value === \"1\") return true;\n if (value === \"false\" || value === \"0\") return false;\n throw ApiError.badRequest(\n `Invalid \\`?${HARD_DELETE_QUERY_PARAM}=${String(raw)}\\`. It takes 'true' or 'false'.`,\n \"INVALID_HARD_PARAM\"\n );\n}\n"],"mappings":"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"}
@@ -4,8 +4,8 @@ globalThis.process ??= __rebaseProcess;
4
4
  __rebaseCreateRequire(import.meta.url);
5
5
  import "./src-D5Y3N-S4.js";
6
6
  import { t as isSQLAdmin } from "./backend-DTAOsLQc.js";
7
- import { r as logger } from "./logger-Bt32sHFw.js";
8
- import { t as ApiError } from "./errors-_ytoGi8A.js";
7
+ import { a as logger } from "./logger-CW8pmTS6.js";
8
+ import { t as ApiError } from "./errors-OMWkKSH-.js";
9
9
  import { Hono } from "hono";
10
10
  //#region ../utils/dist/csv.es.js
11
11
  /**
@@ -237,4 +237,4 @@ function createSqlExportRoutes(options) {
237
237
  //#endregion
238
238
  export { createSqlExportRoutes };
239
239
 
240
- //# sourceMappingURL=sql-export-routes-BgTJ7K3S.js.map
240
+ //# sourceMappingURL=sql-export-routes-DmB_uZ4l.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sql-export-routes-BgTJ7K3S.js","names":[],"sources":["../../utils/dist/csv.es.js","../../common/src/util/sql-redaction.ts","../src/api/sql-export-routes.ts"],"sourcesContent":["//#region src/csv.ts\n/**\n* One CSV field (RFC 4180): quoted when it holds a comma, a quote or a line\n* break, with each quote doubled. SQL NULL — `null` or `undefined` — is an\n* empty field, as `COPY … CSV` writes it; an object (a plan, a parsed `jsonb`)\n* is its JSON.\n*/\nfunction csvField(value) {\n\tif (value === null || value === void 0) return \"\";\n\tconst text = typeof value === \"object\" ? JSON.stringify(value) : String(value);\n\treturn /[\",\\r\\n]/.test(text) ? `\"${text.replace(/\"/g, \"\\\"\\\"\")}\"` : text;\n}\n/** One CSV record: each value through {@link csvField}, separated by commas, with no line break. */\nfunction csvRecord(values) {\n\treturn values.map(csvField).join(\",\");\n}\n//#endregion\nexport { csvField, csvRecord };\n\n//# sourceMappingURL=csv.es.js.map","/**\n * SQL a person ran, as the audit log may keep it: every string literal masked.\n *\n * The SQL console's audit line wrote the statement verbatim, so `ALTER ROLE app\n * PASSWORD 'hunter2'`, `CREATE USER MAPPING … OPTIONS (password 'hunter2')` and\n * `dblink_connect('host=db password=hunter2')` put the password in the\n * production logs, where the logger's redaction — by key name — never looks.\n *\n * A literal is a value the statement carries inline, which is exactly what the\n * line already declines to write when it is bound as a parameter: the\n * statement's shape is the audit signal, the values are whatever the operator\n * was touching. So every literal goes, not only the ones beside a word that\n * looks like `password` — a secret has no reliable neighbour. Identifiers,\n * keywords, numbers and comments stay.\n *\n * Read with Postgres's own lexical rules, not a regular expression over the\n * text: a quote inside a comment, a doubled quote, an `E'…\\'…'` escape or a\n * `$tag$` body must not shift where a literal is thought to end, or the mask\n * covers the statement and leaves the secret.\n */\nexport function redactSqlLiterals(sql: string): string {\n let out = \"\";\n let i = 0;\n while (i < sql.length) {\n const char = sql[i];\n const next = sql[i + 1];\n\n // -- line comment\n if (char === \"-\" && next === \"-\") {\n const end = sql.indexOf(\"\\n\", i);\n const stop = end === -1 ? sql.length : end;\n out += sql.slice(i, stop);\n i = stop;\n continue;\n }\n\n // /* block comment */ — nested, as Postgres reads them\n if (char === \"/\" && next === \"*\") {\n let depth = 1;\n let j = i + 2;\n while (j < sql.length && depth > 0) {\n if (sql[j] === \"/\" && sql[j + 1] === \"*\") { depth++; j += 2; } else if (sql[j] === \"*\" && sql[j + 1] === \"/\") { depth--; j += 2; } else j++;\n }\n out += sql.slice(i, j);\n i = j;\n continue;\n }\n\n // \"quoted identifier\"\n if (char === \"\\\"\") {\n const end = closingQuote(sql, i + 1, \"\\\"\", false);\n out += sql.slice(i, end);\n i = end;\n continue;\n }\n\n // 'string', E'string' with backslash escapes, and the B'', X'', N'',\n // U&'' prefixes — the prefix is kept, the content masked.\n if (char === \"'\") {\n const prefix = /(?:^|[^A-Za-z0-9_$])([EeBbXxNn]|[Uu]&)$/.exec(out)?.[1] ?? \"\";\n out += \"'***'\";\n i = closingQuote(sql, i + 1, \"'\", prefix.toUpperCase() === \"E\");\n continue;\n }\n\n // $tag$ … $tag$ — a dollar-quoted string. A function or DO body is SQL\n // in its own right, and its literals are masked as these are; a\n // password written as `PASSWORD $$hunter2$$` is masked whole.\n if (char === \"$\") {\n const tag = /^\\$([A-Za-z_][A-Za-z0-9_]*)?\\$/.exec(sql.slice(i));\n const precededByIdentifier = /[A-Za-z0-9_]$/.test(out);\n if (tag && !precededByIdentifier) {\n const delimiter = tag[0];\n const bodyStart = i + delimiter.length;\n const bodyEnd = sql.indexOf(delimiter, bodyStart);\n const stop = bodyEnd === -1 ? sql.length : bodyEnd;\n const body = sql.slice(bodyStart, stop);\n const isPassword = /\\bpassword\\s*$/i.test(out);\n out += delimiter + (isPassword ? \"***\" : redactSqlLiterals(body)) + (bodyEnd === -1 ? \"\" : delimiter);\n i = bodyEnd === -1 ? sql.length : bodyEnd + delimiter.length;\n continue;\n }\n }\n\n out += char;\n i++;\n }\n return out;\n}\n\n/**\n * The index just past the quote that closes a literal or identifier opened\n * before `from`. A doubled quote is one quote inside it; with `backslashEscapes`\n * (an `E''` string) so is a backslash-escaped one. Unterminated runs to the end.\n */\nfunction closingQuote(sql: string, from: number, quote: string, backslashEscapes: boolean): number {\n let j = from;\n while (j < sql.length) {\n if (backslashEscapes && sql[j] === \"\\\\\") { j += 2; continue; }\n if (sql[j] === quote) {\n if (sql[j + 1] === quote) { j += 2; continue; }\n return j + 1;\n }\n j++;\n }\n return sql.length;\n}\n","import { Hono } from \"hono\";\nimport { isSQLAdmin, type DatabaseAdmin, type SqlCursor } from \"@rebasepro/types\";\nimport { redactSqlLiterals } from \"@rebasepro/common\";\nimport { csvRecord } from \"@rebasepro/utils\";\nimport { ApiError } from \"./errors\";\nimport type { HonoEnv } from \"./types\";\nimport { logger } from \"../utils/logger\";\n\n/**\n * The SQL console's \"Download full result as CSV\".\n *\n * The console shows at most the first 10,000 rows of a result (see\n * `SQL_CONSOLE_RESULT_LIMITS`), and its own export writes the rows on screen.\n * This route is the way to the rest: it runs ONE query through\n * `SQLAdmin.openSqlCursor` — a server-side cursor in a read-only transaction,\n * as the role the console picked — and streams it as RFC 4180 CSV, one batch\n * at a time. A batch is fetched only when the response has taken the last\n * one, so the server holds one batch however long the result is.\n *\n * Mounted under `/api/admin/sql` behind the admin gate with `database:write` —\n * the scope the socket's `EXECUTE_SQL` asks of the same console, so whoever\n * can run the query can download it and nobody else.\n */\n\n/** RFC 4180's record separator. */\nconst CRLF = \"\\r\\n\";\n\nexport interface SqlExportRoutesOptions {\n /** The default data source's admin capabilities, read per request. */\n getAdmin: () => DatabaseAdmin | undefined;\n}\n\ninterface ExportRequest {\n sql: string;\n database?: string;\n role?: string;\n}\n\nfunction readExportRequest(body: unknown): ExportRequest {\n if (typeof body !== \"object\" || body === null || !(\"sql\" in body) || typeof body.sql !== \"string\" || body.sql.trim() === \"\") {\n throw ApiError.badRequest(\"Send the query to download as `sql`, a non-empty string.\", \"INVALID_INPUT\");\n }\n const optional = (key: \"database\" | \"role\"): string | undefined => {\n const value: unknown = key in body ? Reflect.get(body, key) : undefined;\n if (value === undefined || value === null || value === \"\") return undefined;\n if (typeof value !== \"string\") throw ApiError.badRequest(`\\`${key}\\` must be a string.`, \"INVALID_INPUT\");\n return value;\n };\n return { sql: body.sql, database: optional(\"database\"), role: optional(\"role\") };\n}\n\n/**\n * The cursor's rows as CSV, a header record first. Read one batch per pull:\n * the stream asks for the next only when the response has taken this one.\n *\n * A failure after the first byte cannot change the status any more, so the\n * stream is errored instead — the download fails rather than ending early\n * with a file that looks whole.\n */\nfunction csvStream(cursor: SqlCursor, onFailure: (error: unknown) => void): ReadableStream<Uint8Array> {\n const encoder = new TextEncoder();\n let headerSent = false;\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n if (!headerSent) {\n headerSent = true;\n controller.enqueue(encoder.encode(csvRecord(cursor.columns) + CRLF));\n return;\n }\n const rows = await cursor.read();\n if (!rows) {\n await cursor.close();\n controller.close();\n return;\n }\n controller.enqueue(encoder.encode(rows.map(row => csvRecord(row) + CRLF).join(\"\")));\n } catch (error: unknown) {\n onFailure(error);\n await cursor.close();\n controller.error(error);\n }\n },\n async cancel() {\n // The reader went away: end the read and give the session back.\n await cursor.close();\n }\n }, { highWaterMark: 1 });\n}\n\nexport function createSqlExportRoutes(options: SqlExportRoutesOptions): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n\n router.post(\"/csv\", async (c) => {\n const request = readExportRequest(await c.req.json());\n const admin = options.getAdmin();\n if (!isSQLAdmin(admin) || typeof admin.openSqlCursor !== \"function\") {\n throw new ApiError(501, \"SQL_EXPORT_UNSUPPORTED\",\n \"This data source cannot stream a query's result. Add a LIMIT and export the rows on screen.\");\n }\n\n const caller = c.get(\"user\");\n const user = typeof caller === \"object\" && caller !== null ? caller : undefined;\n const audit = {\n // Literals masked, as on the socket: a statement's shape is the\n // audit signal, its values are whatever the operator was reading.\n sql: redactSqlLiterals(request.sql).substring(0, 500),\n database: request.database,\n role: request.role,\n uid: user?.uid ?? \"unknown\",\n roles: user?.roles ?? [],\n ...(c.get(\"apiKey\") ? { apiKey: c.get(\"apiKey\")?.id } : {}),\n requestId: c.get(\"requestId\")\n };\n\n // Opened before the response starts: a query the database refuses — a\n // write, a second statement, a syntax error — is a 400 with its\n // reason, not an empty file.\n const cursor = await admin.openSqlCursor(request.sql, { database: request.database, role: request.role });\n logger.info(\"[SQL Audit] SQL console CSV download\", audit);\n\n const body = csvStream(cursor, (error) => logger.warn(\"[SQL export] The download failed part-way and was cut off\", {\n requestId: audit.requestId,\n error: error instanceof Error ? error.message : String(error)\n }));\n return new Response(body, {\n status: 200,\n headers: {\n \"Content-Type\": \"text/csv; charset=utf-8\",\n \"Content-Disposition\": \"attachment; filename=\\\"query_results.csv\\\"\",\n \"Cache-Control\": \"no-store\"\n }\n });\n });\n\n return router;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sql-export-routes-DmB_uZ4l.js","names":[],"sources":["../../utils/dist/csv.es.js","../../common/src/util/sql-redaction.ts","../src/api/sql-export-routes.ts"],"sourcesContent":["//#region src/csv.ts\n/**\n* One CSV field (RFC 4180): quoted when it holds a comma, a quote or a line\n* break, with each quote doubled. SQL NULL — `null` or `undefined` — is an\n* empty field, as `COPY … CSV` writes it; an object (a plan, a parsed `jsonb`)\n* is its JSON.\n*/\nfunction csvField(value) {\n\tif (value === null || value === void 0) return \"\";\n\tconst text = typeof value === \"object\" ? JSON.stringify(value) : String(value);\n\treturn /[\",\\r\\n]/.test(text) ? `\"${text.replace(/\"/g, \"\\\"\\\"\")}\"` : text;\n}\n/** One CSV record: each value through {@link csvField}, separated by commas, with no line break. */\nfunction csvRecord(values) {\n\treturn values.map(csvField).join(\",\");\n}\n//#endregion\nexport { csvField, csvRecord };\n\n//# sourceMappingURL=csv.es.js.map","/**\n * SQL a person ran, as the audit log may keep it: every string literal masked.\n *\n * The SQL console's audit line wrote the statement verbatim, so `ALTER ROLE app\n * PASSWORD 'hunter2'`, `CREATE USER MAPPING … OPTIONS (password 'hunter2')` and\n * `dblink_connect('host=db password=hunter2')` put the password in the\n * production logs, where the logger's redaction — by key name — never looks.\n *\n * A literal is a value the statement carries inline, which is exactly what the\n * line already declines to write when it is bound as a parameter: the\n * statement's shape is the audit signal, the values are whatever the operator\n * was touching. So every literal goes, not only the ones beside a word that\n * looks like `password` — a secret has no reliable neighbour. Identifiers,\n * keywords, numbers and comments stay.\n *\n * Read with Postgres's own lexical rules, not a regular expression over the\n * text: a quote inside a comment, a doubled quote, an `E'…\\'…'` escape or a\n * `$tag$` body must not shift where a literal is thought to end, or the mask\n * covers the statement and leaves the secret.\n */\nexport function redactSqlLiterals(sql: string): string {\n let out = \"\";\n let i = 0;\n while (i < sql.length) {\n const char = sql[i];\n const next = sql[i + 1];\n\n // -- line comment\n if (char === \"-\" && next === \"-\") {\n const end = sql.indexOf(\"\\n\", i);\n const stop = end === -1 ? sql.length : end;\n out += sql.slice(i, stop);\n i = stop;\n continue;\n }\n\n // /* block comment */ — nested, as Postgres reads them\n if (char === \"/\" && next === \"*\") {\n let depth = 1;\n let j = i + 2;\n while (j < sql.length && depth > 0) {\n if (sql[j] === \"/\" && sql[j + 1] === \"*\") { depth++; j += 2; } else if (sql[j] === \"*\" && sql[j + 1] === \"/\") { depth--; j += 2; } else j++;\n }\n out += sql.slice(i, j);\n i = j;\n continue;\n }\n\n // \"quoted identifier\"\n if (char === \"\\\"\") {\n const end = closingQuote(sql, i + 1, \"\\\"\", false);\n out += sql.slice(i, end);\n i = end;\n continue;\n }\n\n // 'string', E'string' with backslash escapes, and the B'', X'', N'',\n // U&'' prefixes — the prefix is kept, the content masked.\n if (char === \"'\") {\n const prefix = /(?:^|[^A-Za-z0-9_$])([EeBbXxNn]|[Uu]&)$/.exec(out)?.[1] ?? \"\";\n out += \"'***'\";\n i = closingQuote(sql, i + 1, \"'\", prefix.toUpperCase() === \"E\");\n continue;\n }\n\n // $tag$ … $tag$ — a dollar-quoted string. A function or DO body is SQL\n // in its own right, and its literals are masked as these are; a\n // password written as `PASSWORD $$hunter2$$` is masked whole.\n if (char === \"$\") {\n const tag = /^\\$([A-Za-z_][A-Za-z0-9_]*)?\\$/.exec(sql.slice(i));\n const precededByIdentifier = /[A-Za-z0-9_]$/.test(out);\n if (tag && !precededByIdentifier) {\n const delimiter = tag[0];\n const bodyStart = i + delimiter.length;\n const bodyEnd = sql.indexOf(delimiter, bodyStart);\n const stop = bodyEnd === -1 ? sql.length : bodyEnd;\n const body = sql.slice(bodyStart, stop);\n const isPassword = /\\bpassword\\s*$/i.test(out);\n out += delimiter + (isPassword ? \"***\" : redactSqlLiterals(body)) + (bodyEnd === -1 ? \"\" : delimiter);\n i = bodyEnd === -1 ? sql.length : bodyEnd + delimiter.length;\n continue;\n }\n }\n\n out += char;\n i++;\n }\n return out;\n}\n\n/**\n * The index just past the quote that closes a literal or identifier opened\n * before `from`. A doubled quote is one quote inside it; with `backslashEscapes`\n * (an `E''` string) so is a backslash-escaped one. Unterminated runs to the end.\n */\nfunction closingQuote(sql: string, from: number, quote: string, backslashEscapes: boolean): number {\n let j = from;\n while (j < sql.length) {\n if (backslashEscapes && sql[j] === \"\\\\\") { j += 2; continue; }\n if (sql[j] === quote) {\n if (sql[j + 1] === quote) { j += 2; continue; }\n return j + 1;\n }\n j++;\n }\n return sql.length;\n}\n","import { Hono } from \"hono\";\nimport { isSQLAdmin, type DatabaseAdmin, type SqlCursor } from \"@rebasepro/types\";\nimport { redactSqlLiterals } from \"@rebasepro/common\";\nimport { csvRecord } from \"@rebasepro/utils\";\nimport { ApiError } from \"./errors\";\nimport type { HonoEnv } from \"./types\";\nimport { logger } from \"../utils/logger\";\n\n/**\n * The SQL console's \"Download full result as CSV\".\n *\n * The console shows at most the first 10,000 rows of a result (see\n * `SQL_CONSOLE_RESULT_LIMITS`), and its own export writes the rows on screen.\n * This route is the way to the rest: it runs ONE query through\n * `SQLAdmin.openSqlCursor` — a server-side cursor in a read-only transaction,\n * as the role the console picked — and streams it as RFC 4180 CSV, one batch\n * at a time. A batch is fetched only when the response has taken the last\n * one, so the server holds one batch however long the result is.\n *\n * Mounted under `/api/admin/sql` behind the admin gate with `database:write` —\n * the scope the socket's `EXECUTE_SQL` asks of the same console, so whoever\n * can run the query can download it and nobody else.\n */\n\n/** RFC 4180's record separator. */\nconst CRLF = \"\\r\\n\";\n\nexport interface SqlExportRoutesOptions {\n /** The default data source's admin capabilities, read per request. */\n getAdmin: () => DatabaseAdmin | undefined;\n}\n\ninterface ExportRequest {\n sql: string;\n database?: string;\n role?: string;\n}\n\nfunction readExportRequest(body: unknown): ExportRequest {\n if (typeof body !== \"object\" || body === null || !(\"sql\" in body) || typeof body.sql !== \"string\" || body.sql.trim() === \"\") {\n throw ApiError.badRequest(\"Send the query to download as `sql`, a non-empty string.\", \"INVALID_INPUT\");\n }\n const optional = (key: \"database\" | \"role\"): string | undefined => {\n const value: unknown = key in body ? Reflect.get(body, key) : undefined;\n if (value === undefined || value === null || value === \"\") return undefined;\n if (typeof value !== \"string\") throw ApiError.badRequest(`\\`${key}\\` must be a string.`, \"INVALID_INPUT\");\n return value;\n };\n return { sql: body.sql, database: optional(\"database\"), role: optional(\"role\") };\n}\n\n/**\n * The cursor's rows as CSV, a header record first. Read one batch per pull:\n * the stream asks for the next only when the response has taken this one.\n *\n * A failure after the first byte cannot change the status any more, so the\n * stream is errored instead — the download fails rather than ending early\n * with a file that looks whole.\n */\nfunction csvStream(cursor: SqlCursor, onFailure: (error: unknown) => void): ReadableStream<Uint8Array> {\n const encoder = new TextEncoder();\n let headerSent = false;\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n if (!headerSent) {\n headerSent = true;\n controller.enqueue(encoder.encode(csvRecord(cursor.columns) + CRLF));\n return;\n }\n const rows = await cursor.read();\n if (!rows) {\n await cursor.close();\n controller.close();\n return;\n }\n controller.enqueue(encoder.encode(rows.map(row => csvRecord(row) + CRLF).join(\"\")));\n } catch (error: unknown) {\n onFailure(error);\n await cursor.close();\n controller.error(error);\n }\n },\n async cancel() {\n // The reader went away: end the read and give the session back.\n await cursor.close();\n }\n }, { highWaterMark: 1 });\n}\n\nexport function createSqlExportRoutes(options: SqlExportRoutesOptions): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n\n router.post(\"/csv\", async (c) => {\n const request = readExportRequest(await c.req.json());\n const admin = options.getAdmin();\n if (!isSQLAdmin(admin) || typeof admin.openSqlCursor !== \"function\") {\n throw new ApiError(501, \"SQL_EXPORT_UNSUPPORTED\",\n \"This data source cannot stream a query's result. Add a LIMIT and export the rows on screen.\");\n }\n\n const caller = c.get(\"user\");\n const user = typeof caller === \"object\" && caller !== null ? caller : undefined;\n const audit = {\n // Literals masked, as on the socket: a statement's shape is the\n // audit signal, its values are whatever the operator was reading.\n sql: redactSqlLiterals(request.sql).substring(0, 500),\n database: request.database,\n role: request.role,\n uid: user?.uid ?? \"unknown\",\n roles: user?.roles ?? [],\n ...(c.get(\"apiKey\") ? { apiKey: c.get(\"apiKey\")?.id } : {}),\n requestId: c.get(\"requestId\")\n };\n\n // Opened before the response starts: a query the database refuses — a\n // write, a second statement, a syntax error — is a 400 with its\n // reason, not an empty file.\n const cursor = await admin.openSqlCursor(request.sql, { database: request.database, role: request.role });\n logger.info(\"[SQL Audit] SQL console CSV download\", audit);\n\n const body = csvStream(cursor, (error) => logger.warn(\"[SQL export] The download failed part-way and was cut off\", {\n requestId: audit.requestId,\n error: error instanceof Error ? error.message : String(error)\n }));\n return new Response(body, {\n status: 200,\n headers: {\n \"Content-Type\": \"text/csv; charset=utf-8\",\n \"Content-Disposition\": \"attachment; filename=\\\"query_results.csv\\\"\",\n \"Cache-Control\": \"no-store\"\n }\n });\n });\n\n return router;\n}\n"],"mappings":"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"}
@@ -2,7 +2,7 @@ import { createRequire as __rebaseCreateRequire } from "module";
2
2
  import __rebaseProcess from "process";
3
3
  globalThis.process ??= __rebaseProcess;
4
4
  __rebaseCreateRequire(import.meta.url);
5
- import { t as ApiError } from "./errors-_ytoGi8A.js";
5
+ import { t as ApiError } from "./errors-OMWkKSH-.js";
6
6
  import fs from "node:fs";
7
7
  //#region src/storage/types.ts
8
8
  /**
@@ -169,4 +169,4 @@ function validateUploadedFile(file, maxSize, allowedMimeTypes) {
169
169
  //#endregion
170
170
  export { readStreamFully as a, DOCUMENT_MIME_TYPES as c, fileWebStream as i, IMAGE_MIME_TYPES as l, bufferWebStream as n, webStreamFrom as o, bytesView as r, DEFAULT_MAX_FILE_SIZE as s, validateUploadedFile as t };
171
171
 
172
- //# sourceMappingURL=upload-validation-I9cCUBIW.js.map
172
+ //# sourceMappingURL=upload-validation-Bgo626uY.js.map