@rebasepro/server 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/dist/{GCSStorageController-C7ARrDAp.js → GCSStorageController-40FyFeSx.js} +8 -4
  2. package/dist/GCSStorageController-40FyFeSx.js.map +1 -0
  3. package/dist/{S3StorageController-qevJ0pVE.js → S3StorageController-Bl5shp94.js} +8 -4
  4. package/dist/S3StorageController-Bl5shp94.js.map +1 -0
  5. package/dist/access-BFAUvsSs.js +1897 -0
  6. package/dist/access-BFAUvsSs.js.map +1 -0
  7. package/dist/api/branch-routes.d.ts +25 -0
  8. package/dist/api/closed-database.d.ts +25 -0
  9. package/dist/api/live-schema-routes.d.ts +10 -0
  10. package/dist/api/rest/api-generator.d.ts +9 -1
  11. package/dist/api/rest/idempotency.d.ts +15 -1
  12. package/dist/api/root-error-handler.d.ts +6 -5
  13. package/dist/api/types.d.ts +12 -0
  14. package/dist/{ast-schema-editor-mCeSC03x.js → ast-schema-editor-madTa9Hh.js} +2 -2
  15. package/dist/{ast-schema-editor-mCeSC03x.js.map → ast-schema-editor-madTa9Hh.js.map} +1 -1
  16. package/dist/auth/access.d.ts +10 -0
  17. package/dist/auth/adapter-middleware.d.ts +2 -1
  18. package/dist/auth/adapter-service-key.d.ts +24 -0
  19. package/dist/auth/api-keys/api-key-middleware.d.ts +21 -1
  20. package/dist/auth/api-keys/api-key-store.d.ts +8 -1
  21. package/dist/auth/api-keys/key-grant.d.ts +18 -1
  22. package/dist/auth/expired-token-sweep.d.ts +5 -0
  23. package/dist/auth/impersonation.d.ts +3 -1
  24. package/dist/auth/jwt.d.ts +8 -2
  25. package/dist/auth/oauth-signin-policy.d.ts +21 -35
  26. package/dist/auth/rls-scope.d.ts +17 -7
  27. package/dist/auth/rls-simulator-routes.d.ts +51 -0
  28. package/dist/auth/simulation-read-only.d.ts +28 -0
  29. package/dist/auth/simulation.d.ts +141 -0
  30. package/dist/auth/sql-rate-limit-store.d.ts +15 -1
  31. package/dist/{auth-CteGWxsL.js → auth-izI7AEzC.js} +391 -1893
  32. package/dist/auth-izI7AEzC.js.map +1 -0
  33. package/dist/auth_adapter-B7RUMk6e.js +89 -0
  34. package/dist/auth_adapter-B7RUMk6e.js.map +1 -0
  35. package/dist/backend/boot-announcements.d.ts +13 -0
  36. package/dist/backend/create-backend.d.ts +10 -0
  37. package/dist/backend/options.d.ts +9 -4
  38. package/dist/{backend-DTAOsLQc.js → backend-CaG_Jyso.js} +9 -2
  39. package/dist/backend-CaG_Jyso.js.map +1 -0
  40. package/dist/{backup-Dq0lyJ_D.js → backup-BVbHoKo8.js} +8 -8
  41. package/dist/{backup-Dq0lyJ_D.js.map → backup-BVbHoKo8.js.map} +1 -1
  42. package/dist/boot/boot.d.ts +2 -15
  43. package/dist/boot/ddl-bootstrap.d.ts +34 -0
  44. package/dist/boot/env.d.ts +6 -0
  45. package/dist/boot/fetch-bundle.d.ts +5 -1
  46. package/dist/boot/provision-only.d.ts +23 -0
  47. package/dist/boot/provision.d.ts +53 -2
  48. package/dist/boot/role.d.ts +2 -0
  49. package/dist/boot/schema-change-recorder.d.ts +45 -0
  50. package/dist/boot/security-headers.d.ts +22 -0
  51. package/dist/branch-routes-B2vvEx53.js +84 -0
  52. package/dist/branch-routes-B2vvEx53.js.map +1 -0
  53. package/dist/{contract-routes-DiFwiLg2.js → contract-routes-CU2A3JnF.js} +3 -3
  54. package/dist/{contract-routes-DiFwiLg2.js.map → contract-routes-CU2A3JnF.js.map} +1 -1
  55. package/dist/cron/cron-store.d.ts +8 -1
  56. package/dist/{cron-loader-NHqRfKF0.js → cron-loader-mMhqbVsl.js} +3 -3
  57. package/dist/{cron-loader-NHqRfKF0.js.map → cron-loader-mMhqbVsl.js.map} +1 -1
  58. package/dist/{cron-routes-Dhf82HzT.js → cron-routes-Cs33j_Kr.js} +5 -5
  59. package/dist/{cron-routes-Dhf82HzT.js.map → cron-routes-Cs33j_Kr.js.map} +1 -1
  60. package/dist/{cron-scheduler-D1DT0nw5.js → cron-scheduler-Be7CNghL.js} +3 -3
  61. package/dist/{cron-scheduler-D1DT0nw5.js.map → cron-scheduler-Be7CNghL.js.map} +1 -1
  62. package/dist/{cron-store-DEpVstUU.js → cron-store-DLn11M8H.js} +11 -10
  63. package/dist/cron-store-DLn11M8H.js.map +1 -0
  64. package/dist/{strings.es-BeZWAodn.js → data_driver-D0mg1Y_x.js} +2 -90
  65. package/dist/data_driver-D0mg1Y_x.js.map +1 -0
  66. package/dist/{ddl-bootstrap-BNqi2NZe.js → ddl-bootstrap-CjRrn12l.js} +92 -3
  67. package/dist/ddl-bootstrap-CjRrn12l.js.map +1 -0
  68. package/dist/deploy/pod-contract.d.ts +1 -1
  69. package/dist/email/dev-sink.d.ts +6 -0
  70. package/dist/email/index.d.ts +1 -1
  71. package/dist/{errors-_ytoGi8A.js → errors-OMWkKSH-.js} +66 -5
  72. package/dist/errors-OMWkKSH-.js.map +1 -0
  73. package/dist/{soft-delete-params-BAgpmBvp.js → field-access-DYsvjI_P.js} +27 -57
  74. package/dist/field-access-DYsvjI_P.js.map +1 -0
  75. package/dist/{filter-dialect-O1HK6Nvr.js → filter-dialect-TrKFWLqT.js} +3 -3
  76. package/dist/{filter-dialect-O1HK6Nvr.js.map → filter-dialect-TrKFWLqT.js.map} +1 -1
  77. package/dist/{function-loader-DbCSoNJj.js → function-loader-B8YzXEsQ.js} +22 -15
  78. package/dist/function-loader-B8YzXEsQ.js.map +1 -0
  79. package/dist/{function-routes-DNV4RUF0.js → function-routes-RxrCHNuM.js} +2 -2
  80. package/dist/{function-routes-DNV4RUF0.js.map → function-routes-RxrCHNuM.js.map} +1 -1
  81. package/dist/functions/hono-like.d.ts +12 -0
  82. package/dist/functions/index.js +37 -2
  83. package/dist/functions/index.js.map +1 -1
  84. package/dist/functions/request-timeout.d.ts +3 -3
  85. package/dist/history/history-routes.d.ts +6 -0
  86. package/dist/history/retention-sweep.d.ts +5 -0
  87. package/dist/{history-recorder-RZ1Ow4YY.js → history-recorder-Bsau7h4X.js} +7 -7
  88. package/dist/history-recorder-Bsau7h4X.js.map +1 -0
  89. package/dist/{history-store-DPofGVDp.js → history-store-BQz5YCkW.js} +26 -2
  90. package/dist/history-store-BQz5YCkW.js.map +1 -0
  91. package/dist/index.d.ts +3 -1
  92. package/dist/index.es.js +3003 -681
  93. package/dist/index.es.js.map +1 -1
  94. package/dist/init/process-safety.d.ts +4 -3
  95. package/dist/init/shutdown.d.ts +8 -0
  96. package/dist/init.d.ts +6 -1
  97. package/dist/jobs/job-store.d.ts +8 -1
  98. package/dist/{jobs-8G-_agld.js → jobs-Co6Ni5HP.js} +7 -7
  99. package/dist/jobs-Co6Ni5HP.js.map +1 -0
  100. package/dist/{jwt-YhciR93H.js → jwt-rDS0884_.js} +5 -4
  101. package/dist/{jwt-YhciR93H.js.map → jwt-rDS0884_.js.map} +1 -1
  102. package/dist/{keys-COzQ2aE8.js → keys-BXcdRNhJ.js} +2 -2
  103. package/dist/{keys-COzQ2aE8.js.map → keys-BXcdRNhJ.js.map} +1 -1
  104. package/dist/{logger-Bt32sHFw.js → logger-CW8pmTS6.js} +35 -3
  105. package/dist/logger-CW8pmTS6.js.map +1 -0
  106. package/dist/{logs-routes-CelTiS7C.js → logs-routes-CBt7mMIL.js} +3 -3
  107. package/dist/{logs-routes-CelTiS7C.js.map → logs-routes-CBt7mMIL.js.map} +1 -1
  108. package/dist/mcp/consent-page.d.ts +38 -1
  109. package/dist/mcp/mcp-routes.d.ts +9 -0
  110. package/dist/mcp/mcp-tools.d.ts +7 -1
  111. package/dist/mcp/oauth-store.d.ts +12 -5
  112. package/dist/metrics/history-recorder.d.ts +4 -1
  113. package/dist/metrics/history-store.d.ts +8 -0
  114. package/dist/{offline-BrqCRgih-BtDB1ecy.js → offline-AvuEcg7u-DFHL98OT.js} +127 -40
  115. package/dist/offline-AvuEcg7u-DFHL98OT.js.map +1 -0
  116. package/dist/{offline-store-BhYXViha-BW5j1We_.js → offline-store-BhYXViha-YTmjBpo2.js} +5 -5
  117. package/dist/{offline-store-BhYXViha-BW5j1We_.js.map → offline-store-BhYXViha-YTmjBpo2.js.map} +1 -1
  118. package/dist/{openapi-generator-BamY6-ga.js → openapi-generator-CyxEtYEX.js} +61 -22
  119. package/dist/openapi-generator-CyxEtYEX.js.map +1 -0
  120. package/dist/{proxy-BEKbsil6.js → proxy-BYir8tWK.js} +5 -7
  121. package/dist/proxy-BYir8tWK.js.map +1 -0
  122. package/dist/{query-parser-CGohjyWI.js → query-parser-CDXWf2hH.js} +7 -6
  123. package/dist/{query-parser-CGohjyWI.js.map → query-parser-CDXWf2hH.js.map} +1 -1
  124. package/dist/{request-timeout-CRny5KD0.js → request-timeout-DVjo-c_8.js} +6 -6
  125. package/dist/request-timeout-DVjo-c_8.js.map +1 -0
  126. package/dist/rls-simulator-routes-bEY7-5-u.js +190 -0
  127. package/dist/rls-simulator-routes-bEY7-5-u.js.map +1 -0
  128. package/dist/schema-edit/deployed-schema-guard.d.ts +47 -0
  129. package/dist/{schema-editor-routes-CnNGCayU.js → schema-editor-routes-Dc3DU_jm.js} +4 -4
  130. package/dist/{schema-editor-routes-CnNGCayU.js.map → schema-editor-routes-Dc3DU_jm.js.map} +1 -1
  131. package/dist/soft-delete-params-BRONCOA2.js +59 -0
  132. package/dist/soft-delete-params-BRONCOA2.js.map +1 -0
  133. package/dist/{sql-export-routes-BgTJ7K3S.js → sql-export-routes-QycX8W8S.js} +40 -6
  134. package/dist/sql-export-routes-QycX8W8S.js.map +1 -0
  135. package/dist/{src-D5Y3N-S4.js → src-Bh9LRPb9.js} +21 -2
  136. package/dist/{src-D5Y3N-S4.js.map → src-Bh9LRPb9.js.map} +1 -1
  137. package/dist/storage/GCSStorageController.d.ts +2 -0
  138. package/dist/storage/LocalStorageController.d.ts +8 -3
  139. package/dist/storage/S3StorageController.d.ts +2 -0
  140. package/dist/storage/maintenance-keys.d.ts +17 -0
  141. package/dist/storage/maintenance-routes.d.ts +28 -5
  142. package/dist/storage/maintenance.d.ts +136 -4
  143. package/dist/storage/orphan-scans.d.ts +120 -0
  144. package/dist/storage/types.d.ts +11 -0
  145. package/dist/strings.es-B7QLnHh2.js +96 -0
  146. package/dist/strings.es-B7QLnHh2.js.map +1 -0
  147. package/dist/{upload-validation-I9cCUBIW.js → upload-validation-Bgo626uY.js} +2 -2
  148. package/dist/upload-validation-Bgo626uY.js.map +1 -0
  149. package/dist/utils/dev-dependency-command.d.ts +15 -0
  150. package/dist/utils/dev-port.d.ts +8 -0
  151. package/dist/utils/logger.d.ts +24 -0
  152. package/package.json +5 -5
  153. package/dist/GCSStorageController-C7ARrDAp.js.map +0 -1
  154. package/dist/S3StorageController-qevJ0pVE.js.map +0 -1
  155. package/dist/auth-CteGWxsL.js.map +0 -1
  156. package/dist/auth_adapter-CIItCTc8.js +0 -21
  157. package/dist/auth_adapter-CIItCTc8.js.map +0 -1
  158. package/dist/backend-DTAOsLQc.js.map +0 -1
  159. package/dist/cron-store-DEpVstUU.js.map +0 -1
  160. package/dist/ddl-bootstrap-BNqi2NZe.js.map +0 -1
  161. package/dist/errors-_ytoGi8A.js.map +0 -1
  162. package/dist/function-loader-DbCSoNJj.js.map +0 -1
  163. package/dist/history-recorder-RZ1Ow4YY.js.map +0 -1
  164. package/dist/history-store-DPofGVDp.js.map +0 -1
  165. package/dist/jobs-8G-_agld.js.map +0 -1
  166. package/dist/logger-Bt32sHFw.js.map +0 -1
  167. package/dist/offline-BrqCRgih-BtDB1ecy.js.map +0 -1
  168. package/dist/openapi-generator-BamY6-ga.js.map +0 -1
  169. package/dist/proxy-BEKbsil6.js.map +0 -1
  170. package/dist/request-timeout-CRny5KD0.js.map +0 -1
  171. package/dist/soft-delete-params-BAgpmBvp.js.map +0 -1
  172. package/dist/sql-export-routes-BgTJ7K3S.js.map +0 -1
  173. package/dist/strings.es-BeZWAodn.js.map +0 -1
  174. package/dist/upload-validation-I9cCUBIW.js.map +0 -1
@@ -1 +0,0 @@
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":";;;;;;;;;;;;;;;AAqFA,SAAS,SAAS,MAAM,QAAQ;CAC/B,IAAI,WAAW,KAAK,KAAK,WAAW,GAAG,OAAO;CAC9C,MAAM,YAAY;EACjB,cAAc;EACd,eAAe;EACf,mBAAmB;EACnB,YAAY;EACZ,cAAc;EACd,iBAAiB;EACjB,qBAAqB;EACrB,YAAY;EACZ,UAAU;EACV,YAAY;EACZ,eAAe;EACf,mBAAmB;EACnB,aAAa;EACb,aAAa;EACb,sBAAsB;EACtB,cAAc;EACd,YAAY;EACZ,YAAY;EACZ,mBAAmB;EACnB,2BAA2B;EAC3B,gBAAgB;EAChB,iEAAiE;EACjE,YAAY;EACZ,WAAW;EACX,gBAAgB;EAChB,cAAc;EACd,WAAW;EACX,IAAI;CACL;CACA,MAAM,YAAY;EACjB,MAAM;EACN,MAAM;EACN,OAAO;EACP,KAAK;EACL,OAAO;EACP,KAAK;EACL,OAAO;EACP,QAAQ;CACT;CACA,IAAI;EACH;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACD,CAAC,CAAC,QAAQ,KAAK,YAAY,CAAC,KAAK,GAAG,OAAO;CAC3C,KAAK,MAAM,KAAK,WAAW;EAC1B,MAAM,UAAU,IAAI,OAAO,GAAG,UAAU,GAAG,IAAI,GAAG;EAClD,IAAI,QAAQ,KAAK,IAAI,GAAG,OAAO,KAAK,QAAQ,SAAS,CAAC;CACvD;CACA,KAAK,MAAM,OAAO,WAAW;EAC5B,MAAM,UAAU,IAAI,OAAO,KAAK,GAAG;EACnC,IAAI,QAAQ,KAAK,IAAI,GAAG,OAAO,KAAK,QAAQ,SAAS,UAAU,IAAI;CACpE;CACA,OAAO;AACR;;;;;;;;;;;;;;;;;;;;;;;;;;ACzIA,SAAS,uBAAuB,MAAM;CACrC,OAAO,GAAG,YAAY,kBAAkB,IAAI,CAAC,EAAE;AAChD;;;;;;;;;;AAUA,SAAS,kBAAkB,MAAM;CAChC,IAAI,OAAO,KAAK,IAAI,GAAG,OAAO;CAC9B,MAAM,SAAS,SAAS,IAAI;CAC5B,OAAO,OAAO,SAAS,IAAI,SAAS;AACrC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiGA,SAAS,UAAU,YAAY;CAC9B,IAAI,CAAC,YAAY,OAAO;CACxB,MAAM,WAAW,WAAW,MAAM,QAAQ,CAAC,CAAC,OAAO,OAAO;CAC1D,IAAI,SAAS,UAAU,GAAG,OAAO;CACjC,OAAO,SAAS,KAAK,SAAS,UAAU,UAAU,IAAI,QAAQ,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,QAAQ,MAAM,CAAC,IAAI,QAAQ,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,QAAQ,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE;AACvK;;;;;;;;;;;;;;;;;;;;ACtHA,SAAgB,gBACZ,UACA,kBACA,aACgB;CAChB,MAAM,SAAS,SAAS;CACxB,IAAI,OAAO,WAAW,YAClB,MAAM,IAAI,MACN,WAAW,SAAS,eAAe,KAAK,SAAS,aAAa,KAAK,GAAG,OAClE,iBAAiB,KAAK,yEAC9B;CAGJ,MAAM,mBAAmB,WAAW,UAAU,kBAAkB,aAAa,MAAM;CAKnF,MAAM,eAAe,SAAS,gBAAgB,eAAe,YAAY,iBAAiB,IAAI;CAE9F,MAAM,SAAmH;EACrH;EAQA,cAAc,sBAAsB,OAAO,CAAC;EAC5C,YAAY,iBAAiB;EAC7B,UAAU,SAAS;EACnB,UAAU,SAAS;EACnB,WAAW,SAAS;CAKxB;CAEA,MAAM,aAAa,YAAY,iBAAiB,QAAQ,iBAAiB,IAAI;CAE7E,QAAQ,SAAS,MAAjB;EACI,KAAK,aACD,OAAO;GACH,GAAG;GACH,MAAM;GACN,aAAa;GACb,UAAU;GACV,QAAQ;GACR,UAAU,SAAS,YAAY,uBAAuB,YAAY;GAClE,OAAO,SAAS,UAAU;EAC9B;EAEJ,KAAK,UACD,OAAO;GACH,GAAG;GACH,MAAM;GACN,aAAa;GACb,UAAU;GACV,QAAQ;GACR,oBAAoB,SAAS,sBAAsB,uBAAuB,UAAU;GACpF,WAAW,SAAS;EACxB;EAEJ,KAAK,WACD,OAAO;GACH,GAAG;GACH,MAAM;GACN,aAAa;GACb,UAAU;GACV,QAAQ;GACR,oBAAoB,SAAS,sBAAsB,uBAAuB,UAAU;GAIpF,WAAW,SAAS;EACxB;EAEJ,KAAK,cAAc;GACf,MAAM,cAAc,aAAa,gBAAgB;GACjD,MAAM,cAAc,aAAa,gBAAgB;GACjD,OAAO;IACH,GAAG;IACH,MAAM;IACN,aAAa;IACb,UAAU;IACV,QAAQ;IACR,SAAS;KAGL,OAAO,SAAS,SAAS,SAAS,CAAC,aAAa,WAAW,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG;KAC5E,cAAc,SAAS,SAAS,gBAAgB,uBAAuB,UAAU;KACjF,cAAc,SAAS,SAAS,gBAAgB,uBAAuB,YAAY;KAInF,YAAY,SAAS,SAAS,cAAc,CAAC;IACjD;GACJ;EACJ;EAEA,KAAK,OACD,OAAO;GACH,GAAG;GACH,MAAM;GACN,aAAa,SAAS;GACtB,UAAU;GAGV,QAAQ;GACR,UAAU,SAAS;EACvB;EAEJ,SAII,MAAM,IAAI,MAAM,0BAA0B,KAAK,UAAU,QAAU,GAAG;CAE9E;AACJ;;AAGA,SAAS,SAAS,UAAoB,kBAAoC,aAA8B;CACpG,MAAM,OAAO,SAAS,gBAAgB;CACtC,OAAO,WAAW,OAAO,KAAK,KAAK,KAAK,GAAG,OAAO,iBAAiB,KAAK;AAC5E;;;;;;;;;;;;;;;AAgBA,SAAS,sBAAsB,OAAyB;CACpD,IAAI,CAAC,SAAS,OAAO,UAAU,UAAU,OAAO;CAChD,IAAK,MAA6B,MAAM,OAAO;CAC/C,MAAM,QAAS,MAAgC;CAC/C,OAAO,SAAS,OAAO,UAAU,YAAa,MAA6B,OAAO,QAAQ;AAC9F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,SAAS,WACL,UACA,kBACA,aACA,QAC8B;CAC9B,IAAI;CACJ,IAAI;EACA,mBAAmB,sBAAsB,OAAO,CAAC;CACrD,SAAS,OAAO;EAGZ,IAAI,iBAAiB,gBACjB,MAAM,IAAI,MACN,GAAG,SAAS,UAAU,kBAAkB,WAAW,EAAE,wRAIrD,EAAE,OAAO,MAAM,CACnB;EAEJ,MAAM;CACV;CAEA,IAAI,CAAC,kBAAkB,MACnB,MAAM,IAAI,MACN,GAAG,SAAS,UAAU,kBAAkB,WAAW,EAAE,qCAClD,qBAAqB,KAAA,IAAY,gBAAgB,qCAAqC,OACxF,qBAAqB,KAAA,IAChB,8QAGA,OAAQ,iBAAwC,SAAS,aACrD,8LAGA,2DACd;CAGJ,OAAO;AACX;;;;;;;;;;;;;;;ACzOA,SAAgB,yBAAyB,UAAqC;CAC1E,OAAO,SAAS;AACpB;;AAGA,IAAM,0CAA0B,IAAI,QAA4D;;;;;;;;;;;;;;;AAgBhG,SAAgB,2BACZ,YACgC;CAChC,MAAM,SAAS,wBAAwB,IAAI,UAAU;CACrD,IAAI,QAAQ,OAAO;CAEnB,IAAI,CAAC,6BAA6B,UAAU,GAAG,OAAO,CAAC;CAEvD,MAAM,YAA8C,CAAC;CAErD,KAAK,MAAM,YAAY,WAAW,aAAa,CAAC,GAAG;EAC/C,MAAM,WAAW,gBAAgB,UAAU,UAAU;EACrD,UAAU,SAAS,gBAAgB;CACvC;CAKA,KAAK,MAAM,CAAC,aAAa,aAAa,OAAO,QAAQ,WAAW,cAAc,CAAC,CAAC,GAAG;EAC/E,IAAK,UAAuB,SAAS,YAAY;EACjD,MAAM,WAAY,SAA8B;EAChD,IAAI,CAAC,YAAY,UAAU,cAAc;EAEzC,UAAU,eAAe,gBAAgB,UAAU,YAAY,WAAW;CAC9E;CAEA,wBAAwB,IAAI,YAAY,SAAS;CACjD,OAAO;AACX;;;;;;;;;AAUA,SAAgB,0BACZ,YACA,UAC4B;CAC5B,MAAM,WAAW,2BAA2B,UAAU;CACtD,KAAK,MAAM,CAAC,KAAK,QAAQ,OAAO,QAAQ,WAAW,cAAc,CAAC,CAAC,GAAG;EAClE,MAAM,OAAO;EACb,IAAI,MAAM,SAAS,YAAY;EAG/B,IAAI,SAAS,SAAS,UAAU,OAAO;EACvC,MAAM,YAAa,KAA0B,UAAU;EACvD,IAAI,aAAa,aAAa,UAAU,SAAS,MAAM,UAAU,OAAO;CAC5E;AAEJ;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,mBAAmB,YAA8B,UAAqC;CAClG,OAAO,QAAQ,0BAA0B,YAAY,QAAQ,CAAC,EAAE,YAAY,QAAQ;AACxF;;;;;;;;;;;;;;AA8CA,SAAgB,aAAa,YAAsC;CAE/D,QADiB,6BAA6B,UAAU,IAAI,WAAW,QAAQ,KAAA,MAC5D,YAAY,WAAW,IAAI,KAAK,YAAY,WAAW,IAAI;AAClF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2FA,SAAgB,kBAAkB,YAA0C,QAAwB;CAChG,MAAM,aAAa,YAAY;CAC/B,IAAI,YAAY;EACZ,KAAK,MAAM,CAAC,KAAK,SAAS,OAAO,QAAQ,UAAU,GAAG;GAClD,MAAM,aAAc,MAA+C;GACnE,IAAI,OAAO,eAAe,YAAY,eAAe,QAAQ,OAAO;EACxE;EACA,KAAK,MAAM,OAAO,OAAO,KAAK,UAAU,GAAG;GACvC,IAAI,QAAQ,QAAQ,OAAO;GAC3B,IAAI,YAAY,GAAG,MAAM,QAAQ,OAAO;EAC5C;CACJ;CACA,OAAO,UAAU,MAAM;AAC3B;;;;;;;;;;AAWA,SAAgB,aACZ,mBACA,KAC4B;CAE5B,IAAI,kBAAkB,MAAM,OAAO,kBAAkB;CAGrD,MAAM,UAAU,IAAI,QAAQ,MAAM,GAAG;CACrC,IAAI,YAAY,OAAO,kBAAkB,UAAU,OAAO,kBAAkB;CAG5E,MAAM,WAAW,IAAI,QAAQ,MAAM,GAAG;CACtC,IAAI,aAAa,OAAO,kBAAkB,WAAW,OAAO,kBAAkB;AAGlF;;;;;;;AChPA,SAAgB,gBAAgB,UAAyD;CACrF,IAAI,CAAC,UAAU,OAAO,KAAA;CACtB,IAAI,SAAS,gBAAgB,OAAO;CACpC,MAAM,SAAS,SAAS;CACxB,IAAI,CAAC,QAAQ,OAAO,KAAA;CACpB,IAAI,OAAO,SAAS,KAAA,KAAa,OAAO,UAAU,KAAA,GAAW,OAAO,KAAA;CACpE,OAAO;AACX;;AAGA,IAAM,kBAA+B,OAAO,OAAO;CAAE,MAAM,OAAO,OAAO,CAAC,CAAC;CAAG,OAAO,OAAO,OAAO,CAAC,CAAC;AAAE,CAAC;;;;;;;;;;;;;;;AAgBxG,SAAS,UAAU,SAAwC,QAA0C;CACjG,IAAI,YAAY,KAAA,GAAW,OAAO;CAClC,IAAI,QAAQ,WAAW,GAAG,OAAO;CACjC,IAAI,CAAC,QAAQ,OAAO;CACpB,MAAM,QAAQ,OAAO;CACrB,IAAI,CAAC,SAAS,MAAM,WAAW,GAAG,OAAO;CACzC,OAAO,MAAM,SAAA,OAAmB,KAAK,QAAQ,MAAK,SAAQ,MAAM,SAAS,IAAI,CAAC;AAClF;;AAGA,SAAgB,aAAa,UAAgC,QAA0C;CACnG,MAAM,SAAS,gBAAgB,QAAQ;CACvC,OAAO,SAAS,UAAU,OAAO,MAAM,MAAM,IAAI;AACrD;;AAGA,SAAgB,cAAc,UAAgC,QAA0C;CACpG,MAAM,SAAS,gBAAgB,QAAQ;CACvC,OAAO,SAAS,UAAU,OAAO,OAAO,MAAM,IAAI;AACtD;;;;;;;;;;;;;;;;;AAkBA,SAAgB,qBACZ,YACA,QACA,MAC4C;CAC5C,MAAM,UAAU,SAAS,SAAS,eAAe;CACjD,OAAO,WAAW,aAAY,aAAY,CAAC,QAAQ,UAAU,MAAM,CAAC;AACxE;;;;;;;;;;;;;AAcA,SAAgB,oBAAoB,YAA2C;CAC3E,OAAO,WAAW,aAAY,aAAY,QAAQ,UAAU,cAAc,CAAC,CAAC,CAAC;AACjF;AAEA,SAAS,WACL,YACA,YAC4C;CAC5C,MAAM,WAAqB,CAAC;CAC5B,MAAM,0BAAU,IAAI,IAAY;CAChC,MAAM,aAAa,WAAW,cAAc,CAAC;CAG7C,IAAI;CAEJ,MAAM,YAAY,SAAiB;EAC/B,IAAI,QAAQ,cAAc,CAAC,SAAS,SAAS,IAAI,GAAG,SAAS,KAAK,IAAI;EACtE,QAAQ,IAAI,IAAI;CACpB;CAEA,KAAK,MAAM,CAAC,MAAM,aAAa,OAAO,QAAQ,UAAU,GAAG;EACvD,IAAI,CAAC,WAAW,QAAoB,GAAG;EACvC,SAAS,IAAI;EACb,MAAM,aAAc,SAAsB;EAC1C,IAAI,YAAY,QAAQ,IAAI,UAAU;EACtC,IAAK,SAAsB,SAAS,YAAY;GAC5C,cAAc,2BAA2B,UAAU;GACnD,MAAM,WAAW,aAAa,WAAW,IAAI;GAC7C,IAAI,UAAU,SAAS,aAAa;IAChC,SAAS,kBAAkB,YAAY,SAAS,QAAQ,CAAC;IACzD,QAAQ,IAAI,SAAS,QAAQ;GACjC;EACJ;CACJ;CACA,OAAO;EAAE;EAAU;CAAQ;AAC/B;;;;;;;;;;;;AChKA,IAAa,sBAAsB;;AAEnC,IAAa,0BAA0B;;;;;;;;;;;;;AAcvC,SAAgB,iBAAiB,KAA4C;CACzE,IAAI,QAAQ,KAAA,KAAa,QAAQ,QAAQ,QAAQ,IAAI,OAAO,KAAA;CAC5D,MAAM,QAAQ,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,YAAY;CAC7C,IAAI,UAAU,WAAW,OAAO;CAChC,IAAI,UAAU,QAAQ,OAAO;CAC7B,MAAM,SAAS,WACX,cAAc,oBAAoB,GAAG,OAAO,GAAG,EAAE,yHAEjD,uBACJ;AACJ;;;;;;;;;;;;AAaA,SAAgB,gBAAgB,KAAuB;CACnD,IAAI,QAAQ,KAAA,KAAa,QAAQ,QAAQ,QAAQ,IAAI,OAAO;CAC5D,MAAM,QAAQ,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,YAAY;CAC7C,IAAI,UAAU,UAAU,UAAU,KAAK,OAAO;CAC9C,IAAI,UAAU,WAAW,UAAU,KAAK,OAAO;CAC/C,MAAM,SAAS,WACX,cAAc,wBAAwB,GAAG,OAAO,GAAG,EAAE,kCACrD,oBACJ;AACJ"}
@@ -1 +0,0 @@
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 +0,0 @@
1
- {"version":3,"file":"strings.es-BeZWAodn.js","names":[],"sources":["../../types/src/errors.ts","../../types/src/controllers/data_driver.ts","../../utils/dist/strings.es.js"],"sourcesContent":["/**\n * The error codes every route can produce, as `RebaseApiError.code`.\n *\n * These are the defaults on `ApiError`'s static constructors server-side, so\n * any endpoint can answer with one. They are **not** the complete set: routes\n * pass their own more specific codes too (`EMAIL_EXISTS`, `TOKEN_EXPIRED`,\n * `INVALID_BULK_BODY`, …), and auth alone defines a couple of dozen.\n *\n * Hence the union is deliberately open rather than closed. It exists to give\n * autocomplete and to catch a typo in the common cases — `code` was a bare\n * `string`, so `e.code === \"NOT_FOUND\"` and `e.code === \"NOTFOUND\"` were\n * equally valid and only one of them worked. Closing it would be a lie that\n * broke the moment a route added a code.\n *\n * @example\n * if (e instanceof RebaseApiError) {\n * switch (e.code) {\n * case \"NOT_FOUND\": return null; // completed\n * case \"FORBIDDEN\": return redirect();\n * default: throw e; // routes' own codes land here\n * }\n * }\n *\n * @group Errors\n */\nexport type RebaseErrorCode =\n | \"BAD_REQUEST\"\n | \"UNAUTHORIZED\"\n | \"FORBIDDEN\"\n | \"NOT_FOUND\"\n | \"CONFLICT\"\n | \"INTERNAL_ERROR\"\n | \"SERVICE_UNAVAILABLE\"\n | \"NETWORK_ERROR\"\n | \"OFFLINE\"\n | \"DB_PERMISSION_DENIED\"\n | \"SCHEMA_DRIFT\"\n /**\n * Realtime: the socket closed after a request was written and before the\n * server answered — the request may or may not have run, and it is never\n * re-sent. On a live subscription's `onError`: the connection has been\n * down longer than a blip; the subscription is kept and resumes, with a\n * fresh `onUpdate`, when the socket is back.\n */\n | \"CONNECTION_LOST\"\n /**\n * Realtime: no answer within the request timeout of the call — either it\n * waited for a socket the whole time and was never sent, or it was written\n * and never answered.\n */\n | \"REQUEST_TIMEOUT\"\n // `string & {}` keeps the union open while preserving completion on the\n // literals above — a bare `| string` would collapse them and offer nothing.\n | (string & {});\n\n/**\n * Structured initializer for {@link RebaseApiError}.\n *\n * @group Errors\n */\nexport interface RebaseErrorInit {\n /**\n * HTTP status code, when the error originated from an HTTP response.\n *\n * Three states, and they mean different things:\n *\n * - a real status — the server answered, and this is what it said;\n * - **`0`** — the request never reached a server: DNS, a refused\n * connection, CORS, an abort. `XMLHttpRequest` has always spelled that\n * `0`, and a fabricated 5xx would be indistinguishable from one the\n * server actually sent. The original failure is on `cause`;\n * - `undefined` — nothing was sent at all: a realtime/WebSocket failure,\n * or a client-side logic error raised before any request.\n */\n status?: number;\n /** Stable, machine-readable error code. See {@link RebaseErrorCode}. */\n code?: RebaseErrorCode;\n /** Structured error payload returned by the server, when present. */\n details?: unknown;\n /**\n * The server's correlation id for the request that failed, when it sent\n * one — the `requestId` in the error envelope, which also comes back on the\n * `X-Request-ID` header.\n *\n * The envelope has carried it for a while; the client dropped it on the\n * floor, so a bug report from an app could never quote the one string that\n * finds the server-side line.\n */\n requestId?: string;\n /**\n * Seconds to wait before retrying, from the response's `Retry-After`\n * header. Present on a 429 and on some 503s.\n *\n * Also dropped. The offline queue's own backoff therefore ignored a server\n * that had said exactly how long to wait — the one number that turns a\n * retry storm into a queue that drains.\n */\n retryAfterSeconds?: number;\n /** The underlying error this one wraps, if any. */\n cause?: unknown;\n}\n\n/**\n * The single error type thrown across the entire Rebase client surface —\n * HTTP data/control-plane calls, realtime/WebSocket operations, and\n * client-side logic errors (e.g. an unknown collection accessor). A `catch`\n * block only ever needs to check for this one class:\n *\n * ```ts\n * import { RebaseApiError } from \"@rebasepro/client\"; // re-exported\n *\n * try {\n * await client.data.products.update(id, { price: 9 });\n * } catch (e) {\n * if (e instanceof RebaseApiError) {\n * if (e.status === 404) { ... } // HTTP failures carry a status\n * console.error(e.code, e.details);\n * }\n * }\n * ```\n *\n * `status` is present for HTTP failures and `undefined` otherwise, so its\n * presence distinguishes transport-level errors from realtime/logic errors.\n *\n * @group Errors\n */\nexport class RebaseApiError extends Error {\n /** HTTP status code, or `undefined` for non-HTTP errors. */\n readonly status?: number;\n /** Stable machine-readable error code, when the server supplied one. See {@link RebaseErrorCode}. */\n readonly code?: RebaseErrorCode;\n /** Structured error payload from the server, when present. */\n readonly details?: unknown;\n /** See {@link RebaseErrorInit.requestId}. Quote it in a bug report. */\n readonly requestId?: string;\n /** See {@link RebaseErrorInit.retryAfterSeconds}. */\n readonly retryAfterSeconds?: number;\n\n constructor(message: string, init: RebaseErrorInit = {}) {\n super(message);\n this.name = \"RebaseApiError\";\n this.status = init.status;\n this.code = init.code;\n this.details = init.details;\n this.requestId = init.requestId;\n this.retryAfterSeconds = init.retryAfterSeconds;\n if (init.cause !== undefined) {\n // `cause` is standard on Error but not always in the lib target's type.\n (this as { cause?: unknown }).cause = init.cause;\n }\n }\n}\n\n/**\n * Client-side logic error — raised before any request is made (e.g. accessing\n * an unknown collection accessor when a typed dictionary is configured).\n *\n * A subclass of {@link RebaseApiError} (with no `status`), so a single\n * `catch (e) { if (e instanceof RebaseApiError) ... }` handles it too.\n *\n * @group Errors\n */\nexport class RebaseClientError extends RebaseApiError {\n /**\n * `init` is the same one {@link RebaseApiError} takes, and it is what makes\n * `code` reachable at all.\n *\n * The constructor used to accept a message and nothing else, so every\n * client-side failure — an undefined filter value, an unknown accessor,\n * `listen()` on a client built with `realtime: false`, a function name with\n * a `/` in it, `refreshSession()` while signed out — arrived with `code ===\n * undefined`. The documented `switch (e.code)` in this file's own example\n * fell to `default: throw e` for all of them, and the only client-side error\n * that *did* carry a code was `OFFLINE`, because that one path minted a\n * `RebaseApiError` instead.\n */\n constructor(message: string, init: RebaseErrorInit = {}) {\n super(message, init);\n this.name = \"RebaseClientError\";\n }\n}\n\n/**\n * Brand for a contract method a particular client cannot serve.\n *\n * `Symbol.for` rather than a fresh symbol: two copies of `@rebasepro/types` in\n * one tree — which happens, see `docs/dependency-duplication-traps.md` — must\n * agree about it, and a module-local symbol would not.\n */\nconst UNSUPPORTED_METHOD = Symbol.for(\"rebase.unsupportedMethod\");\n\n/**\n * Build the stub a client installs for a contract method it cannot serve.\n *\n * `listen`, `listenById` and `count` are part of `SDKCollectionClient`, not\n * optional extras — a caller should be able to write\n * `client.data.posts.count()` without asking first, and a transport that cannot\n * serve it should answer with a sentence naming the configuration that would,\n * rather than with `undefined is not a function` at the call site. Where the\n * transport genuinely cannot (a client built with `realtime: false`, a driver\n * with no `listenCollection`), it installs one of these instead of omitting the\n * method.\n *\n * @param message What to tell the caller, naming the fix.\n * @group Errors\n */\n/**\n * The tag {@link unsupportedMethod} puts on the stub it returns.\n *\n * Written as an optional member on a callable, which is what a tagged stub\n * actually is — so both the write and the read below are single casts and tsc\n * checks the symbol and the value type on each. Reading it as\n * `Record<symbol, boolean>` made every symbol key on the object a `boolean`\n * (which is not true of a function, and is why that needed `as unknown as` to\n * be written at all), and the write and the read were then free to disagree\n * about the tag.\n */\ntype UnsupportedTagged = ((...args: never[]) => unknown) & { [UNSUPPORTED_METHOD]?: boolean };\n\nexport function unsupportedMethod<F>(message: string): F {\n const stub = (): never => {\n // The two reasons a method is a stub — `realtime: false`, and a driver\n // with no `listenCollection` — are one thing to a caller: this client\n // cannot do realtime. One code covers both, and the message says which.\n throw new RebaseClientError(message, { code: \"REALTIME_DISABLED\" });\n };\n (stub as UnsupportedTagged)[UNSUPPORTED_METHOD] = true;\n return stub as F;\n}\n\n/**\n * Can this method actually do anything?\n *\n * `true` for a stub from {@link unsupportedMethod} **and** for a method that is\n * simply not there — a partial client, a hand-built test double, an\n * implementation written against an older shape of the interface. Both mean the\n * same thing to a caller, so both answer the same way, and an adapter that\n * checks this cannot be caught out by either.\n *\n * Ordinary code does not need it: calling the method and letting it throw is\n * the normal path. Adapters do — the admin panel chooses between subscribing\n * and a one-shot `find()` by asking whether the client can listen, and a UI\n * that subscribes into a throw is worse than one that polls. This is the\n * question `if (accessor.listen)` used to be asking, made explicit now that the\n * method is always there to call.\n *\n * @group Errors\n */\nexport function isUnsupported(method: unknown): boolean {\n if (typeof method !== \"function\") return true;\n return (method as UnsupportedTagged)[UNSUPPORTED_METHOD] === true;\n}\n","import { RebaseApiError } from \"../errors\";\nimport type { CollectionRegistryController } from \"./collection_registry\";\nimport type { EntityStatus, EntityValues } from \"../types/entities\";\nimport type { CollectionConfig, FilterValues } from \"../types/collections\";\nimport type { OrderByTuple } from \"../types/filter-operators\";\nimport type { RebaseCallContext } from \"../call_context\";\nimport type { IncludeSpec, LogicalCondition } from \"./data\";\nimport type { CollectionUpdateMeta } from \"../types/websockets\";\n\n\n/**\n * @internal\n */\nexport interface FetchOneProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n id: string | number;\n databaseId?: string;\n collection?: CollectionConfig<M>\n /**\n * See {@link FetchCollectionProps.withDeleted}. A soft-deleted row is a 404\n * here by default, so `findById` and `find` agree about which rows exist —\n * a row you cannot find in a listing and can still open by id is the kind\n * of inconsistency that makes a feature untrustworthy.\n */\n withDeleted?: boolean | \"only\";\n}\n\n/**\n * What a row listener subscribes to: the row, and the relations each frame\n * loads into it.\n *\n * @internal\n */\nexport type ListenOneTarget<M extends Record<string, unknown> = Record<string, unknown>> =\n FetchOneProps<M>\n & {\n /**\n * Relations to load into every frame, as {@link FetchCollectionProps.include}\n * loads them into each row of a list. Without it a to-many relation\n * is absent from each frame, and the admin's record form, opened by\n * URL, showed it empty — so a save from it unlinked every related row.\n */\n include?: IncludeSpec;\n }\n\n/**\n * @internal\n */\nexport type ListenOneProps<M extends Record<string, unknown> = Record<string, unknown>> =\n ListenOneTarget<M>\n & {\n onUpdate: (row: Record<string, unknown> | null) => void,\n onError?: (error: Error) => void,\n }\n\n/**\n * Configuration for vector similarity search queries.\n * Vector search applies an ORDER BY distance expression and optionally\n * filters results by a distance threshold.\n */\nexport interface VectorSearchParams {\n /** Property name containing the vector column */\n property: string;\n /** Query vector to compare against */\n vector: number[];\n /** Distance function (default: \"cosine\") */\n distance?: \"cosine\" | \"l2\" | \"inner_product\";\n /** Only return results within this distance threshold */\n threshold?: number;\n}\n\n// ── List pagination bounds ────────────────────────────────────────────────\n//\n// Client-driven list reads (REST `GET /<collection>` and the WebSocket\n// `subscribe_collection` message) accept a client-supplied `limit`. Without\n// bounds, an ABSENT limit streams the entire table into memory — a trivial\n// OOM/DoS — and `limit=100000000` (or `limit=0`, historically an unlimited\n// bypass) is honoured verbatim. `resolveClientListLimit` is the single shared\n// enforcement point so every untrusted ingress behaves identically. Trusted\n// server-side callers build fetch options directly and are intentionally NOT\n// bounded here (migrations, admin exports, and CDC refetches may need the full\n// set).\n//\n// A limit the platform will not serve is REFUSED, not quietly shrunk. Clamping\n// answers a request for 100 000 rows with 1 000 of them, and a short page is\n// indistinguishable from \"that is all the data there is\" — which is how a CSV\n// export shipped 50 rows of a 100 000-row collection under a filename that read\n// like the whole thing. `meta.total`/`meta.hasMore` make truncation *detectable*\n// on the REST list response, but only for a caller who thinks to compare what it\n// asked for against what it got, and the WebSocket `collection_update` frame\n// carries neither — so signalling cannot be the answer on every surface and\n// rejecting is. An ABSENT limit still defaults: naming no window is not the same\n// as asking for one that cannot be served.\n\n/** Rows returned for a plain / text-search list read when the client sends no `limit`. */\nexport const DEFAULT_LIST_LIMIT = 50;\n/** Rows returned for a vector-search list read when the client sends no `limit`. */\nexport const DEFAULT_VECTOR_LIST_LIMIT = 10;\n/** Largest `limit` a client may ask for on any surface. Above it, the read is refused. */\nexport const MAX_LIST_LIMIT = 1000;\n\n/** Overridable bounds for {@link resolveClientListLimit}. */\nexport interface ListLimitBounds {\n /** Default page size for plain and text-search reads. */\n defaultLimit?: number;\n /** Default page size for vector-search reads. */\n vectorDefaultLimit?: number;\n /** Largest limit a client may ask for. A larger one is rejected, not clamped. */\n maxLimit?: number;\n}\n\n/**\n * Thrown by {@link resolveClientListLimit} for a `limit` the platform will not\n * serve. Carries an HTTP status so an ingress that speaks HTTP can forward it\n * verbatim, and `maxLimit` so one can be built without re-deriving the ceiling.\n *\n * @group Errors\n */\nexport class ListLimitError extends RebaseApiError {\n /** The ceiling that was exceeded — what the caller should page by instead. */\n readonly maxLimit: number;\n\n constructor(message: string, maxLimit: number) {\n super(message, { status: 400, code: \"INVALID_LIMIT\" });\n this.name = \"ListLimitError\";\n this.maxLimit = maxLimit;\n // Keeps `instanceof` working when this is compiled down for an older\n // target, where extending a builtin otherwise loses the prototype.\n Object.setPrototypeOf(this, ListLimitError.prototype);\n }\n}\n\n/**\n * Resolve a client-supplied list `limit` into a safe, always-defined value.\n *\n * - An absent / blank limit falls back to the mode default:\n * `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.\n * - A limit that is present must be an integer in `[1, maxLimit]`. Anything\n * else — `0`, a negative, `1.5`, `abc`, `100000000` — throws\n * {@link ListLimitError} rather than being coerced into range, because every\n * coercion answers a question the caller did not ask with a page it cannot\n * tell apart from the whole collection.\n *\n * The return is never `undefined` — no ingress that routes its client limit\n * through this can produce an unbounded read.\n *\n * @throws {ListLimitError} when a present `limit` is not an integer in range.\n */\nexport function resolveClientListLimit(\n rawLimit: number | string | null | undefined,\n opts: ListLimitBounds & { vectorSearch?: boolean } = {}\n): number {\n const maxLimit = opts.maxLimit ?? MAX_LIST_LIMIT;\n if (rawLimit != null && String(rawLimit).trim() !== \"\") {\n // `Number`, not `parseInt`: `parseInt(\"50rows\")` is 50, which silently\n // reads a typo as a window the caller never wrote.\n const parsed = typeof rawLimit === \"number\" ? rawLimit : Number(String(rawLimit).trim());\n if (!Number.isInteger(parsed) || parsed < 1) {\n throw new ListLimitError(\n `Invalid \\`limit\\`: ${String(rawLimit)}. Expected a whole number between 1 and ${maxLimit}.`,\n maxLimit\n );\n }\n if (parsed > maxLimit) {\n throw new ListLimitError(\n `\\`limit\\` ${parsed} is above the maximum of ${maxLimit}. Ask for at most ${maxLimit} rows ` +\n \"per read and page through the rest with `offset` — answering with a smaller page would be \" +\n \"indistinguishable from there being no more rows.\",\n maxLimit\n );\n }\n return parsed;\n }\n return opts.vectorSearch\n ? (opts.vectorDefaultLimit ?? DEFAULT_VECTOR_LIST_LIMIT)\n : (opts.defaultLimit ?? DEFAULT_LIST_LIMIT);\n}\n\n/**\n * @internal\n */\nexport interface FetchCollectionProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n collection?: CollectionConfig<M>;\n filter?: FilterValues<Extract<keyof M, string>>,\n /**\n * An `or(...)`/`and(...)` group, applied alongside `filter`.\n *\n * The REST layer parsed `?or=` into this and then had nowhere to put it, so\n * the group was dropped and the read ran unfiltered — returning every row\n * the caller's policies allowed rather than the ones they asked for.\n */\n logical?: LogicalCondition;\n limit?: number;\n offset?: number;\n startAfter?: unknown;\n /**\n * The sort, in either of two spellings:\n *\n * - a field name, whose direction is the separate `order` below — the\n * original single-column contract, which every existing driver reads;\n * - a list of `[field, direction]` tuples applied in order of significance,\n * which carries a multi-column sort and ignores `order` entirely.\n *\n * `normalizeDriverOrderBy` in `@rebasepro/common` collapses the pair to the\n * list form. A driver that has not been taught the list form should read it\n * through that helper rather than assume a string: handed an array, `String()`\n * would produce a field name like `roles,asc` and the sort would 400 (or,\n * with unknown-field warnings on, silently vanish).\n */\n orderBy?: string | OrderByTuple[];\n searchString?: string;\n /** Ask each row which declared search field matched — populates `_matches`. */\n searchExplain?: boolean;\n /** Direction for the string form of `orderBy`. Ignored when `orderBy` is a list. */\n order?: \"desc\" | \"asc\";\n /** Vector similarity search configuration */\n vectorSearch?: VectorSearchParams;\n /**\n * What to do about rows a soft delete has stamped.\n *\n * Unset (the default) hides them, which is the whole point of the feature:\n * a deleted row is deleted as far as the application is concerned. `true`\n * includes them alongside the live ones — a trash view, an admin audit.\n * `\"only\"` returns nothing but them, which is the trash view proper and is\n * not expressible as a filter, because the field is not part of the\n * caller's vocabulary.\n *\n * Ignored by collections that do not declare {@link\n * PostgresCollectionConfig.softDelete}: there is no stamp to look at, and\n * silently returning nothing for `\"only\"` on such a collection would be a\n * worse answer than ignoring it.\n */\n withDeleted?: boolean | \"only\";\n /**\n * Relations to load — see {@link IncludeSpec}.\n *\n * Absent means *no* relations, the same as it does over REST. It used to be\n * absent from this contract entirely, and the driver's own fetch then loaded\n * every relation of every row unconditionally: `find()` returned a row with\n * a foreign key and `listen()` returned the same row with a nested object\n * where that key was, for the same query.\n */\n include?: IncludeSpec;\n /** Columns to read, as a projection. See `FindParams.fields`. */\n fields?: string[];\n /** `SELECT DISTINCT` over the projection. See `FindParams.distinct`. */\n distinct?: boolean;\n}\n\n/**\n * @internal\n */\nexport type ListenCollectionProps<M extends Record<string, unknown> = Record<string, unknown>> =\n FetchCollectionProps<M> &\n {\n /**\n * Page number (1-indexed), as `FindParams.page`.\n *\n * A subscription could name a `limit` and an `offset` but not a `page`,\n * so a live list on page three had to compute the offset itself — and\n * the two spellings then disagreed about what a page was.\n */\n page?: number;\n /**\n * Subscribe to the count alone: the server reads no rows, and each\n * frame carries `rows: []` and `meta.total`. A server that predates it\n * ignores the key and answers with the rows asked for (so ask for\n * `limit: 1`) and the same `meta`. See `listenCount`.\n */\n countOnly?: boolean;\n onUpdate: (rows: Record<string, unknown>[], meta?: CollectionUpdateMeta) => void;\n onError?: (error: Error) => void;\n };\n\n/**\n * @internal\n */\nexport interface SaveProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n values: Partial<EntityValues<M>>;\n id?: string | number; // can be empty for new entities\n previousValues?: Partial<EntityValues<M>>;\n collection?: CollectionConfig<M>;\n status: EntityStatus;\n /**\n * Write the row with INSERT ... ON CONFLICT DO UPDATE on the primary key\n * instead of choosing between insert and update up front.\n *\n * One statement, so it does not lose the race a read-then-write can, and it\n * succeeds whether or not the row is already there — what a re-runnable\n * import needs. Requires every primary key column to be present; without\n * them there is no conflict target and the row is inserted normally.\n */\n upsert?: boolean;\n\n /**\n * The columns the upsert matches a conflict on, instead of the primary key.\n *\n * The key is the only target that always exists, and it is the wrong one\n * for the write an upsert is usually reached for: \"this user, identified by\n * their email, exists with these values\". Keyed on the primary key that is\n * an insert, because the caller does not know the serial id — so the row is\n * duplicated on every run.\n *\n * Only column sets carrying a uniqueness guarantee are legal here; Postgres\n * refuses anything else with 42P10, from inside a transaction. The REST\n * layer checks the target against the collection's declarations first (see\n * `resolveConflictTarget`), so the answer is a 400 naming the available\n * targets rather than a 500 naming a constraint the caller never wrote.\n */\n onConflict?: readonly string[];\n\n /**\n * A check on the stored row an update addresses, run by the driver inside\n * the write's own transaction, before any hook runs and before anything is\n * written. See {@link RowPrecondition}. Honoured only by a driver that\n * declares {@link DataDriver.checksRowPreconditions}; ignored on a create.\n */\n precondition?: RowPrecondition;\n}\n\n/**\n * A check a caller makes on the row a write addresses, inside that write.\n *\n * The REST routes used to read the row in a transaction of their own before\n * writing it, to answer a missing row with their 404 and to compare\n * `If-Match`. Two transactions: the read loaded every declared relation of the\n * row, and two writes carrying the same tag could both pass the comparison,\n * because neither saw the other's write before making its own.\n *\n * @internal\n */\nexport interface RowPrecondition {\n /**\n * Lock the row before it is read, so no other write to it can land between\n * this check and this write. A conditional write (`If-Match`) sets it; a\n * plain existence check does not need it and does not pay for it.\n */\n lock?: boolean;\n /**\n * Judge the row as this caller reads it at the address written (their\n * policies, their `beforeQuery`, the trash hidden unless the write is a\n * restore or a purge) — `undefined` when there is none. Throw to refuse the\n * write; nothing has been written when this runs.\n */\n check(stored: Record<string, unknown> | undefined): void | Promise<void>;\n}\n\n/**\n * @internal\n */\nexport interface SaveManyProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n /**\n * The rows to write. A row carrying its primary key updates (or, with\n * `upsert`, inserts-or-updates) that row; one without inserts.\n */\n rows: Partial<EntityValues<M>>[];\n collection?: CollectionConfig<M>;\n /** Apply every row as INSERT ... ON CONFLICT DO UPDATE. See {@link SaveProps.upsert}. */\n upsert?: boolean;\n /** The conflict target for those upserts. See {@link SaveProps.onConflict}. */\n onConflict?: readonly string[];\n}\n\n/**\n * @internal\n */\nexport interface UpdateManyProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n /**\n * The rows to update, each named by its address.\n *\n * Distinct from {@link SaveManyProps.rows}, which carries keys *inside* the\n * values and is insert-shaped — `saveMany` passes `status: \"new\"` and no\n * `id`, so it cannot express \"update exactly this row\". This can, and it is\n * why bulk update is a separate driver method rather than a flag on that one.\n */\n updates: { id: string | number; values: Partial<EntityValues<M>> }[];\n collection?: CollectionConfig<M>;\n}\n\n/**\n * @internal\n */\nexport interface DeleteProps<M extends Record<string, unknown> = Record<string, unknown>> {\n /**\n * The row's address, and nothing about its contents.\n *\n * The driver reads the row itself, under the caller's own scope, and that\n * read is what `beforeDelete` and `afterDelete` receive and what history\n * records. This used to carry `values`, and the driver took them as the\n * row — so over the WebSocket, which forwarded the client's frame, the\n * caller wrote the audit record of their own deletion and could get past a\n * `beforeDelete` that refused on a column by sending `values: {}`.\n */\n row: { id: string | number; path: string };\n collection?: CollectionConfig<M>;\n /**\n * Issue a real `DELETE` on a collection that declares\n * {@link PostgresCollectionConfig.softDelete}.\n *\n * The row and every cascade behind it go. It needs the same permission an\n * ordinary delete does and nothing more: it is the same verb, and a second\n * access-control surface for one operation is a second thing to get wrong.\n * No effect on a collection without soft delete, where every delete is\n * already this one.\n */\n hard?: boolean;\n\n /**\n * A check on the stored row, inside the delete's own transaction, before\n * any hook runs. See {@link RowPrecondition}. Honoured only by a driver\n * that declares {@link DataDriver.checksRowPreconditions}.\n */\n precondition?: RowPrecondition;\n}\n\n/**\n * @internal\n */\nexport interface DeleteManyProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n ids: (string | number)[];\n collection?: CollectionConfig<M>;\n /** See {@link DeleteProps.hard}. */\n hard?: boolean;\n}\n\n/**\n * Addressing ONE link of a many-to-many, to set the columns it carries.\n *\n * `path` is the relation on a row — `posts/1/tags` — and `targetId` the row on\n * the far side, so the pair names exactly one junction row. Not a `SaveProps`,\n * because a save at that address means \"write the target row\", and a link's own\n * columns are not the target's: two posts sharing a tag see one tag and two\n * different links.\n *\n * @internal\n */\nexport interface UpdateRelationPivotProps {\n /** The nested relation path, e.g. `posts/1/tags`. */\n path: string;\n /** The far side's key. */\n targetId: string | number;\n /** The junction columns to set, keyed by the property key `through.properties` declares. */\n pivot: Record<string, unknown>;\n}\n\n/**\n * One operation of a {@link DataDriver.batchWrite}.\n *\n * `path` rather than a slug, because a batch entry addresses rows exactly as\n * the single-row props do and a nested path is a legal address there.\n *\n * @internal\n */\nexport interface BatchWriteOperation<M extends Record<string, unknown> = Record<string, unknown>> {\n op: \"create\" | \"update\" | \"upsert\" | \"delete\";\n path: string;\n /** Required for `update` and `delete`. May be a `$ref` marker; see `batchWrite`. */\n id?: unknown;\n values?: Partial<EntityValues<M>>;\n collection?: CollectionConfig<M>;\n /** See {@link SaveProps.onConflict}. `upsert` only. */\n onConflict?: readonly string[];\n /** Names this operation's result, for a later `$ref`. */\n ref?: string;\n}\n\n/**\n * @internal\n */\nexport interface BatchWriteProps<M extends Record<string, unknown> = Record<string, unknown>> {\n operations: BatchWriteOperation<M>[];\n}\n\nexport type FilterCombinationValidProps = {\n path: string;\n databaseId?: string;\n collection: CollectionConfig;\n filterValues: FilterValues<string>;\n sortBy?: [string, \"asc\" | \"desc\"];\n};\n\n/**\n * The integration SPI for plugging a data backend into Rebase.\n *\n * Implement this interface to connect a custom backend (or use a built-in\n * driver such as the Firestore one) and register it on\n * `<Rebase dataSources>`. Rebase wraps drivers via `buildRebaseData` and\n * routes collections to them by their `dataSource` key.\n *\n * For *consuming* data in application code, use `RebaseData` /\n * `context.data` instead — this interface is only for providing it.\n *\n * @group Datasource\n */\nexport interface DataDriver {\n\n /**\n * Key that identifies this driver\n */\n key?: string;\n\n /**\n * If the driver has been initialised\n */\n initialised?: boolean;\n\n /**\n * Fetch data from a collection\n * @param props\n * @return Promise of flat rows\n */\n fetchCollection<M extends Record<string, unknown> = Record<string, unknown>>(props: FetchCollectionProps<M>): Promise<Record<string, unknown>[]>;\n\n /**\n * Listen to a collection in a given path. If you don't implement this method\n * `fetchCollection` will be used instead, with no real time updates.\n * @param props\n * @return Function to cancel subscription\n */\n listenCollection?<M extends Record<string, unknown> = Record<string, unknown>>(props: ListenCollectionProps<M>): () => void;\n\n /**\n * Retrieve a single row given a path and a collection\n * @param props\n */\n fetchOne<M extends Record<string, unknown> = Record<string, unknown>>(props: FetchOneProps<M>): Promise<Record<string, unknown> | undefined>;\n\n /**\n * Get realtime updates on one row.\n * @param props\n * @return Function to cancel subscription\n */\n listenOne?<M extends Record<string, unknown> = Record<string, unknown>>(props: ListenOneProps<M>): () => void;\n\n /**\n * Save a row to the specified path\n * @param props\n */\n save<M extends Record<string, unknown> = Record<string, unknown>>(props: SaveProps<M>): Promise<Record<string, unknown>>;\n\n /**\n * Save many rows as one unit of work.\n *\n * Every row runs the same pipeline as {@link save} — callbacks, relations\n * and row-level security all still apply — but they share a single\n * transaction, so the batch either lands whole or not at all. That, and the\n * single round trip, is what makes importing tens of thousands of rows\n * viable without dropping to raw SQL.\n *\n * Optional: drivers that cannot do this leave it undefined and callers fall\n * back to `save` per row.\n */\n saveMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: SaveManyProps<M>): Promise<Record<string, unknown>[]>;\n\n /**\n * Update many rows in one transaction, each addressed by id.\n *\n * Optional for the same reason `saveMany` is: a driver that cannot make the\n * batch atomic should not pretend to. The REST layer reports\n * `BULK_UNSUPPORTED` rather than silently falling back to a loop of single\n * writes, which would be neither atomic nor one round trip — the two things\n * a caller reaches for a batch to get.\n */\n updateMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: UpdateManyProps<M>): Promise<Record<string, unknown>[]>;\n\n /**\n * Delete the row `props.row` addresses.\n *\n * **Resolving means the row is gone because this call removed it.** A\n * delete that matched nothing must reject with a not-found error\n * (`ApiError.notFound`, `statusCode: 404`) rather than resolving quietly.\n *\n * The rule is here rather than in each driver because the two\n * implementations answered differently and each had a test pinning its own\n * habit: Postgres threw, Mongo logged a warning and resolved. Three things\n * decide it in favour of rejecting.\n *\n * The REST layer already says 404 — `DELETE /api/data/<c>/<id>` reads the\n * row before removing it — so a quiet resolve made the driver API disagree\n * with the HTTP API about the same operation, and only in-process\n * `rebase.data` callers could see the difference.\n *\n * A caller cannot tell \"deleted\" from \"there was nothing there\" without it,\n * and those are different facts: one means the caller's model of the data\n * was right, the other that it was stale. Silence hands back the wrong one\n * and the caller carries on.\n *\n * And on a driver with row-level security, \"matched nothing\" is *also* how\n * a policy refusal arrives — Postgres filters `DELETE` through `USING`\n * rather than raising. A driver that resolves on zero rows therefore\n * reports a refused delete as a completed one, which is the defect\n * `explainZeroRowWrite` exists to prevent (see `write-denial.ts`).\n *\n * Conformance for both server drivers lives in\n * `packages/server/test/contract/delete-contract.ts`, run by each driver's\n * own suite against its own database. `packages/firebase`'s Firestore\n * driver does not honour it: `deleteDoc` resolves for a missing document\n * and reporting otherwise would cost a read on every delete. It runs in the\n * browser against Firestore's own semantics rather than behind\n * `rebase.data`, and that exception is stated here rather than left to be\n * discovered.\n */\n delete<M extends Record<string, unknown> = Record<string, unknown>>(props: DeleteProps<M>): Promise<void>;\n\n /**\n * Delete all entities from a collection.\n * @param path Collection path\n */\n deleteAll?(path: string): Promise<void>;\n\n /**\n * Set the columns ONE many-to-many link carries, leaving the membership\n * alone — `manyToMany`'s `through.properties`.\n *\n * `PATCH /api/data/<c>/<id>/<relation>/<targetId>` with a `_pivot` body\n * reaches this. The membership array cannot express it: sending one element\n * would unlink everything else, and re-sending the whole set to change one\n * value reintroduces the lost update the membership diff exists to avoid.\n *\n * Optional. A driver whose junctions carry nothing but the two keys leaves\n * it undefined, and the REST layer answers `RELATION_PIVOT_UNSUPPORTED`\n * rather than pretending the write landed.\n */\n updateRelationPivot?(props: UpdateRelationPivotProps): Promise<void>;\n\n /**\n * Delete many rows in one transaction, addressed by id.\n *\n * Ids rather than a filter, deliberately — see\n * {@link SDKCollectionClient.deleteMany}. Optional, as `saveMany` is.\n */\n deleteMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: DeleteManyProps<M>): Promise<void>;\n\n /**\n * Apply a mixed list of writes across collections as one unit of work.\n *\n * The capability `saveMany` and `deleteMany` cannot express between them: a\n * batch that touches two tables. Sent as two requests those can\n * half-succeed, and the recovery — read back, work out which half landed,\n * undo it — is code nobody writes.\n *\n * Every operation runs the pipeline its single-row equivalent runs, in\n * order, in one transaction, under the caller's own role. Operations may\n * carry `{ \"$ref\": \"<name>.<field>\" }` markers in `values` or `id`, which\n * the driver resolves against the rows earlier operations wrote — the\n * driver, because inside the transaction is the only place those rows\n * exist. `@rebasepro/server` exports `resolveBatchRefs` so the resolution\n * is one implementation rather than one per driver.\n *\n * Resolves to one entry per operation, aligned to the input: the written\n * row for a create, update or upsert, and `null` for a delete.\n *\n * Optional for the same reason `saveMany` is: a driver that cannot make it\n * atomic must not pretend to. The REST layer answers `BATCH_UNSUPPORTED`\n * rather than falling back to a loop, which would be the non-atomic\n * sequence the caller reached for this to avoid.\n */\n batchWrite?<M extends Record<string, unknown> = Record<string, unknown>>(\n props: BatchWriteProps<M>\n ): Promise<(Record<string, unknown> | null)[]>;\n\n /**\n * Check if the given property is unique in the given collection\n * @param path Collection path\n * @param name of the property\n * @param value\n * @param id\n * @param collection\n * @return `true` if there are no other fields besides the given entity\n */\n checkUniqueField(\n path: string,\n name: string,\n value: unknown,\n id?: string | number,\n collection?: CollectionConfig\n ): Promise<boolean>;\n\n /**\n * Count the number of entities in a collection\n */\n count?<M extends Record<string, unknown> = Record<string, unknown>>(props: FetchCollectionProps<M>): Promise<number>;\n\n /**\n * Run several reads as one: `work` is handed a driver whose reads all go\n * through a single read-only transaction that reads one snapshot, under\n * this driver's identity and row-level security, and its result is\n * returned.\n *\n * A list page is a read and a count, and each was a transaction of its\n * own — the second checkout, `BEGIN`, identity setup and `COMMIT` cost as\n * many round trips as the count itself. On one snapshot the two also\n * agree: `meta.total` describes the rows the page was taken from, and an\n * `ETag` the row the body was rendered from.\n *\n * Optional: a driver without it is called directly, read by read.\n */\n readTogether?<T>(work: (driver: DataDriver) => Promise<T>): Promise<T>;\n\n /**\n * Check if the given filter combination is valid\n * @param props\n */\n isFilterCombinationValid?(props: Omit<FilterCombinationValidProps, \"collection\"> & {\n databaseId?: string\n }): boolean;\n\n /**\n * Get the object to generate the current time in the driver\n */\n currentTime?: () => unknown;\n\n delegateToCMSModel?: (data: unknown) => unknown;\n\n cmsToDelegateModel?: (data: unknown) => unknown;\n\n initTextSearch?: (props: {\n context: RebaseCallContext,\n path: string,\n databaseId?: string,\n collection: CollectionConfig,\n parentCollectionSlugs?: string[];\n parentEntityIds?: string[];\n }) => Promise<boolean>;\n\n /**\n * Flag to indicate if the driver has requested the initialization of the text search index\n */\n needsInitTextSearch?: boolean;\n\n // ── REST fetch capabilities ─────────────────────────────────────────\n\n /**\n * Optional REST-optimised fetch service. When present, the REST API\n * generator uses these methods instead of the generic `fetchOne` /\n * `fetchCollection` pipeline, enabling include-aware eager-loading.\n */\n restFetchService?: RestFetchService;\n\n /**\n * This driver reads the row an update or a delete addresses inside that\n * write's own transaction, and runs {@link SaveProps.precondition} /\n * {@link DeleteProps.precondition} on it there. A caller may then leave its\n * own read of the row out. A driver without it ignores the precondition,\n * so a caller must read the row itself.\n */\n readonly checksRowPreconditions?: boolean;\n\n // ── Admin capabilities ─────────────────────────────────────────────\n //\n // Admin operations are now modelled as capability-specific interfaces\n // (SQLAdmin, DocumentAdmin, SchemaAdmin) in `@rebasepro/types/backend`.\n //\n // Drivers that support admin features should expose them here.\n // Consumers should use the `isSQLAdmin()`, `isSchemaAdmin()` etc.\n // type guards to safely narrow the type before calling methods.\n\n /**\n * Return the admin capabilities of this driver.\n * @see SQLAdmin\n * @see DocumentAdmin\n * @see SchemaAdmin\n */\n admin?: import(\"../types/backend\").DatabaseAdmin;\n\n}\n\n/**\n * REST-optimised fetch service exposed by drivers that support\n * eager-loading of relations via `include`.\n *\n * The methods return flattened rows — exactly the table's columns, under their\n * own names and with the types the database returned — and included relations\n * inlined as plain nested rows. This is the shape served to app developers\n * through the REST API / SDK client.\n *\n * No synthesized `id`: identity is a primary key, which may be named anything\n * and span several columns, so an address is derived by whoever needs one (see\n * `buildCompositeId`) rather than written into the row on top of the data.\n *\n * @group DataDriver\n */\nexport interface RestFetchService {\n /**\n * Fetch a collection of flattened entities with optional relation includes.\n */\n fetchCollectionForRest(\n collectionPath: string,\n options?: {\n filter?: FilterValues<string>;\n /** An `or(...)`/`and(...)` group, applied alongside `filter`. */\n logical?: LogicalCondition;\n /** See `FetchCollectionProps.orderBy`: a field name plus `order`, or a list of tuples. */\n orderBy?: string | OrderByTuple[];\n order?: \"desc\" | \"asc\";\n limit?: number;\n offset?: number;\n startAfter?: Record<string, unknown>;\n searchString?: string;\n /** Ask each row which declared search fields matched — populates `_matches`. */\n searchExplain?: boolean;\n databaseId?: string;\n vectorSearch?: VectorSearchParams;\n /** See {@link FetchCollectionProps.withDeleted}. */\n withDeleted?: boolean | \"only\";\n /**\n * Columns to read. A projection pushed into the SELECT, not a trim\n * of the response — `excludeFromApi` still applies on top, and the\n * primary key is always read whether or not it is named.\n */\n fields?: string[];\n /** `SELECT DISTINCT` over the projection. See `FindParams.distinct`. */\n distinct?: boolean;\n },\n include?: IncludeSpec\n ): Promise<Record<string, unknown>[]>;\n\n /**\n * The opaque cursor that continues a listing after `row`.\n *\n * On the driver rather than the route because deriving it needs the\n * collection's primary key — which may be named anything and span several\n * columns — and that is the driver's knowledge. The route holds the last\n * row and the sort keys and asks for the string.\n *\n * `undefined` where no cursor can describe the page: an ordering with no\n * stored value to compare against (relevance), or a row missing a value for\n * one of the sort keys. The listing then reports no `nextCursor` and the\n * caller pages by offset, which is what it did before cursors existed.\n *\n * Optional: a driver that cannot seek simply never issues one, and\n * `meta.nextCursor` is absent for every read it serves.\n */\n cursorFor?(\n collectionPath: string,\n row: Record<string, unknown>,\n orderBy?: OrderByTuple[]\n ): string | undefined;\n\n /**\n * `count`/`sum`/`avg`/`min`/`max` over the rows a filter selects,\n * optionally grouped.\n *\n * Optional, and the REST route answers 501 where a driver does not\n * implement it — an aggregate is not a thing to approximate, and an empty\n * result set would read as \"nothing matched\".\n *\n * Any implementation **must apply the same row-level authorization as a\n * read**. An aggregate is an efficient way to learn about rows you cannot\n * select, and `count(*)` over a table whose policies would return nothing\n * has to be zero.\n */\n aggregate?(\n collectionPath: string,\n options: {\n aggregates: { fn: \"count\" | \"sum\" | \"avg\" | \"min\" | \"max\"; field?: string; alias: string }[];\n groupBy?: string[];\n filter?: FilterValues<string>;\n logical?: LogicalCondition;\n searchString?: string;\n /** Groups to return. Applies to a grouped aggregate; one without `groupBy` is one row. */\n limit?: number;\n /** Groups to skip, for paging past `limit`. Grouped aggregates only. */\n offset?: number;\n /**\n * Sort for the groups, by a `groupBy` field or an aggregate's alias\n * (`count`, `sum_total`). Whatever it leaves tied is ordered by the\n * group keys, so the order is total and a page boundary falls in\n * the same place every time. Grouped aggregates only.\n */\n orderBy?: OrderByTuple[];\n /** See {@link FetchCollectionProps.withDeleted}. */\n withDeleted?: boolean | \"only\";\n }\n ): Promise<Record<string, unknown>[]>;\n\n /**\n * Fetch a single flattened entity with optional relation includes.\n */\n fetchOneForRest(\n collectionPath: string,\n id: string | number,\n include?: IncludeSpec,\n databaseId?: string,\n options?: {\n /** See `FetchCollectionProps.fields`. */\n fields?: string[];\n /** See {@link FetchOneProps.withDeleted}. */\n withDeleted?: boolean | \"only\";\n }\n ): Promise<Record<string, unknown> | null>;\n}\n","//#region src/strings.ts\nvar tokenizeRegex = /[A-Z]{2,}(?=[A-Z][a-z]|\\b)|[A-Z]?[a-z]+|[0-9]+(?:[a-z](?![a-z]))?|[A-Z]/g;\nvar toKebabCase = (str) => {\n\tif (!str || typeof str !== \"string\") return \"\";\n\tconst regExpMatchArray = str.match(tokenizeRegex);\n\tif (!regExpMatchArray) return \"\";\n\treturn regExpMatchArray.map((x) => x.toLowerCase()).join(\"-\");\n};\nvar snakeCaseRegex = tokenizeRegex;\nvar toSnakeCase = (str) => {\n\tif (!str || typeof str !== \"string\") return \"\";\n\tconst regExpMatchArray = str.match(snakeCaseRegex);\n\tif (!regExpMatchArray) return \"\";\n\treturn regExpMatchArray.map((x) => x.toLowerCase()).join(\"_\");\n};\nfunction camelCase(str) {\n\tif (!str) return \"\";\n\tif (str.length === 1) return str.toLowerCase();\n\tconst parts = str.split(/[-_ ]+/).filter(Boolean);\n\tif (parts.length === 0) return \"\";\n\treturn parts[0].toLowerCase() + parts.slice(1).map((part) => part.charAt(0).toUpperCase() + part.substring(1).toLowerCase()).join(\"\");\n}\n/**\n* A random base-36 string of exactly `strLength` characters.\n*\n* Not `Math.random().toString(36).slice(2, 2 + strLength)`: that has no\n* guaranteed length. Base-36 of a double drops trailing zeros, so the source\n* string is short about once in 36 calls and the slice quietly returns fewer\n* characters than asked for — `randomString(10)` returning 9. These values\n* prefix uploaded filenames to keep them apart, so a short one is a likelier\n* collision, and it fails at the rate that makes a test look flaky.\n*/\nfunction randomString(strLength = 5) {\n\tconst alphabet = \"0123456789abcdefghijklmnopqrstuvwxyz\";\n\tlet result = \"\";\n\tfor (let i = 0; i < strLength; i++) result += alphabet.charAt(Math.floor(Math.random() * 36));\n\treturn result;\n}\nfunction randomColor() {\n\treturn Math.floor(Math.random() * 16777215).toString(16);\n}\nfunction slugify(text, separator = \"_\", lowercase = true) {\n\tif (!text) return \"\";\n\tconst from = \"ãàáäâẽèéëêìíïîõòóöôùúüûñç·/_,:;-\";\n\tconst to = `aaaaaeeeeeiiiiooooouuuunc${separator}${separator}${separator}${separator}${separator}${separator}${separator}`;\n\tfor (let i = 0, l = 32; i < l; i++) text = text.replace(new RegExp(from.charAt(i), \"g\"), to.charAt(i));\n\ttext = text.toString().trim().replace(/^\\s+|\\s+$/g, \"\").replace(/\\s+/g, separator).replace(/&/g, separator).replace(/[^\\w\\\\-]+/g, \"\").replace(new RegExp(\"\\\\\" + separator + \"\\\\\" + separator + \"+\", \"g\"), separator);\n\treturn lowercase ? text.toLowerCase() : text;\n}\nfunction unslugify(slug) {\n\tif (!slug) return \"\";\n\tif (slug.includes(\"-\") || slug.includes(\"_\") || !slug.includes(\" \")) return slug.replace(/[-_]/g, \" \").replace(/\\w\\S*/g, function(txt) {\n\t\treturn txt.charAt(0).toUpperCase() + txt.substring(1);\n\t}).trim();\n\telse return slug.trim();\n}\n/**\n* Are these two identifiers one edit apart, ignoring case?\n*\n* Tight on purpose. A suggester that fires on two edits offers `columnWidth`\n* for `colWith` and `readOnly` for `required`, and a \"did you mean\" that is\n* usually wrong teaches people to stop reading them. One edit covers what\n* actually happens — a dropped letter (`multilne`), a doubled one, a transposed\n* pair (`validaton`), a wrong case — and nothing else.\n*\n* Lives here rather than beside either caller because both the CLI's manifest\n* reader and the server's collection validator answer the same question about\n* an unknown key, and two copies of a similarity rule is two rules the moment\n* one of them is tuned.\n*/\nfunction isNearMiss(a, b) {\n\tconst x = a.toLowerCase();\n\tconst y = b.toLowerCase();\n\tif (x === y) return true;\n\tif (Math.abs(x.length - y.length) > 1) return false;\n\tconst [shorter, longer] = x.length <= y.length ? [x, y] : [y, x];\n\tlet i = 0;\n\tlet j = 0;\n\tlet edits = 0;\n\twhile (i < shorter.length && j < longer.length) {\n\t\tif (shorter[i] === longer[j]) {\n\t\t\ti++;\n\t\t\tj++;\n\t\t\tcontinue;\n\t\t}\n\t\tif (++edits > 1) return false;\n\t\tif (shorter.length === longer.length) i++;\n\t\tj++;\n\t}\n\treturn edits + (longer.length - j) + (shorter.length - i) <= 1;\n}\n/**\n* The key in `known` that `key` was probably meant to be, if any.\n*\n* `undefined` when nothing is close — which is the common case for a key that\n* is simply from a newer version, and the reason the caller must be able to say\n* \"unknown\" without saying \"wrong\".\n*/\nfunction suggestNearMiss(known, key) {\n\treturn known.find((candidate) => isNearMiss(candidate, key));\n}\nfunction prettifyIdentifier(input) {\n\tif (!input) return \"\";\n\tlet text = input;\n\ttext = text.replace(/([a-z])([A-Z])|([A-Z])([A-Z][a-z])/g, \"$1$3 $2$4\");\n\ttext = text.replace(/[_-]+/g, \" \");\n\treturn text.trim().replace(/\\b\\w/g, (char) => char.toUpperCase());\n}\n/**\n* How many terms one search string is allowed to contribute.\n*\n* Every term multiplies the work: the substring fallback compiles to one\n* `ILIKE '%term%'` per term *per searchable column*, evaluated per row on a\n* sequential scan. Eight is far past what a person types into a search box and\n* still bounds a pasted paragraph to a fixed number of predicates. Terms past\n* the cap are dropped rather than rejected — dropping only widens the result\n* set, and a search box that errors on a long paste is worse than one that\n* matches on its first eight words.\n*/\nvar MAX_SEARCH_TERMS = 8;\n/**\n* The terms a user meant when they typed into a search box.\n*\n* A search box is not a substring box. Typing `sebastian melendez` on a\n* collection whose name is split across `first_name` and `last_name` has to\n* find the row, and matching the whole string against each field separately\n* cannot: no single column holds both words. Splitting on whitespace and\n* requiring *every* term to match *some* field is what does, and it is what\n* `websearch_to_tsquery` already does for collections that declared a `search`\n* block — so this is also what keeps the two search paths answering the same\n* question.\n*\n* It fixes the trailing space too: `sebastian ` matched nothing at all, because\n* the space was part of the pattern.\n*\n* A double-quoted run is one term, again matching the full-text path, so a\n* caller who really does want the phrase can ask for it. An unbalanced quote is\n* stripped rather than honoured — the alternative is that everything after a\n* stray `\"` silently becomes one long term that matches nothing.\n*\n* Returns an empty array for a string with no terms in it at all (whitespace,\n* or nothing but quotes); the caller decides what an empty search means, since\n* only it knows whether \"no constraint\" or \"no rows\" is the safe reading.\n*/\nfunction splitSearchTerms(searchString, maxTerms = MAX_SEARCH_TERMS) {\n\tconst terms = [];\n\tconst pattern = /\"([^\"]*)\"|(\\S+)/g;\n\tlet match;\n\twhile ((match = pattern.exec(searchString)) !== null) {\n\t\tconst term = (match[1] ?? match[2].replace(/\"/g, \"\")).trim();\n\t\tif (!term) continue;\n\t\tterms.push(term);\n\t\tif (terms.length >= maxTerms) break;\n\t}\n\treturn terms;\n}\n//#endregion\nexport { MAX_SEARCH_TERMS, camelCase, isNearMiss, prettifyIdentifier, randomColor, randomString, slugify, splitSearchTerms, suggestNearMiss, toKebabCase, toSnakeCase, unslugify };\n\n//# sourceMappingURL=strings.es.js.map"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"upload-validation-I9cCUBIW.js","names":[],"sources":["../src/storage/types.ts","../src/storage/streams.ts","../src/storage/upload-validation.ts"],"sourcesContent":["/**\n * Storage configuration and types for Rebase backend\n */\n\nimport { StorageSource, UploadFileProps, UploadFileResult, DownloadConfig, DownloadMetadata, StorageListResult, StorageReference } from \"@rebasepro/types\";\n\n/**\n * Local filesystem storage configuration\n */\nexport interface LocalStorageConfig {\n type: \"local\";\n /** Base directory for file storage (e.g., './uploads') */\n basePath: string;\n /** Maximum file size in bytes (default: 50MB) */\n maxFileSize?: number;\n /** Allowed MIME types (if not set, all types allowed) */\n allowedMimeTypes?: string[];\n /** Base URL for generating download URLs (default: auto-detected from request) */\n baseUrl?: string;\n /**\n * Set when this local backend stands in for an object store the source\n * declared and nothing bound — `bucket(\"media\", { engine: \"s3\" })` with no\n * `S3_BUCKET__MEDIA` in a development process. Names the engine it stands\n * in for, so boot can say so and `rebase status` can show it. Never set in\n * production, where an unbound object store stays unbound.\n */\n standsInFor?: string;\n /**\n * Set when nothing chose this backend: no `STORAGE_TYPE` and no declared\n * engine, so local disk is only the default a source falls back to.\n * Production drops a local backend either way. This only decides how that\n * is reported: an implicit default being dropped means the project uses no\n * storage, which is not a misconfiguration.\n */\n implicit?: boolean;\n}\n\n/**\n * S3-compatible storage configuration (works with AWS S3 and MinIO)\n */\nexport interface S3StorageConfig {\n type: \"s3\";\n /** S3 bucket name */\n bucket: string;\n /** AWS region (e.g., 'us-east-1') */\n region?: string;\n /** Custom endpoint URL (required for MinIO, Cloudflare R2, Hetzner Object Storage) */\n endpoint?: string;\n /** AWS access key ID */\n accessKeyId: string;\n /** AWS secret access key */\n secretAccessKey: string;\n /** Use path-style URLs (required for MinIO) */\n forcePathStyle?: boolean;\n /** Maximum file size in bytes (default: 50MB) */\n maxFileSize?: number;\n /** Allowed MIME types (if not set, all types allowed) */\n allowedMimeTypes?: string[];\n}\n\n/**\n * Google Cloud Storage configuration (works with GCS and Firebase Storage)\n */\nexport interface GCSStorageConfig {\n type: \"gcs\";\n /** GCS bucket name (e.g. \"my-project.appspot.com\" for Firebase Storage) */\n bucket: string;\n /** GCP project ID (optional, auto-detected from credentials) */\n projectId?: string;\n /** Path to service account JSON key file */\n keyFilename?: string;\n /** Service account credentials object (alternative to keyFilename) */\n credentials?: { client_email: string; private_key: string; project_id?: string };\n /** Maximum file size in bytes (default: 50MB) */\n maxFileSize?: number;\n /** Allowed MIME types */\n allowedMimeTypes?: string[];\n}\n\n/**\n * Storage configuration — local filesystem, S3-compatible, or Google Cloud Storage.\n *\n * **Built-in providers:**\n * - `local` — Zero-config filesystem storage. Great for dev and single-server deployments (Hetzner, bare metal).\n * - `s3` — Any S3-compatible provider. AWS S3, Cloudflare R2, MinIO, Hetzner Object Storage,\n * Backblaze B2, DigitalOcean Spaces, and even GCS (via its S3-compatible interoperability API).\n * - `gcs` — Native Google Cloud Storage / Firebase Storage via `@google-cloud/storage`.\n *\n * **Custom providers:**\n * For other cloud storage (Azure Blob, etc.), implement the `StorageController`\n * interface and pass the instance directly to the `storage` config.\n */\nexport type BackendStorageConfig = LocalStorageConfig | S3StorageConfig | GCSStorageConfig;\n\n/** What a storage request is trying to do to an object. */\nexport type {\n StorageOperation,\n StorageAuthorizeUser,\n StorageAuthorizeContext,\n StorageAuthorizeData,\n StorageAuthorize\n} from \"@rebasepro/types\";\n\n/**\n * How the server writes an object. See {@link StorageController.putObject}.\n */\nexport interface PutObjectOptions {\n /**\n * Hold this write to the controller's upload limits: `maxFileSize` and\n * `allowedMimeTypes`. Defaults to `true`.\n *\n * Those limits say what a user may upload. A write the server makes for\n * itself, such as a database backup, passes `false`. A dump is neither of\n * an allowed type nor, past a small database, under 50 MB. The upload\n * routes never pass it.\n */\n uploadLimits?: boolean;\n}\n\n/**\n * Storage controller interface for backend implementations\n */\nexport interface StorageController {\n /**\n * Upload an object.\n *\n * A write is held to the controller's upload limits — `maxFileSize` and\n * `allowedMimeTypes` — unless `options.uploadLimits` is `false`. Only the\n * server's own writes pass that; nothing a request sends reaches it.\n */\n putObject(props: UploadFileProps, options?: PutObjectOptions): Promise<UploadFileResult>;\n\n /**\n * Get a download URL (signed URL equivalent) for an object.\n *\n * For server-side code only: no route hands this URL to a client. A\n * client reads a private object through `/file/*` with the download\n * token `GET /metadata/*` mints. The built-in S3 and GCS controllers sign\n * it for an hour.\n */\n getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig>;\n\n /**\n * Describe an object — size, type, custom metadata — without producing a\n * URL for it, or `null` when there is no such object.\n *\n * `GET /metadata/*` wants only this: the client builds its own `/file/*`\n * URL and spends the download token the route mints. Asking\n * `getSignedUrl` for it made every metadata read sign a URL nobody used,\n * and on GCS signing is not free — without a key file the client asks the\n * IAM Credentials API to sign, which needs `iam.serviceAccounts.signBlob`\n * on the runtime account itself. Cloud Run does not grant that, so every\n * private object answered 500 there.\n *\n * A controller that does not implement this keeps the old behaviour: the\n * route falls back to `getSignedUrl` and reads its `metadata`.\n */\n getMetadata?(key: string, bucket?: string): Promise<DownloadMetadata | null>;\n\n /**\n * Get object as a File\n */\n getObject(key: string, bucket?: string): Promise<File | null>;\n\n /**\n * Describe an object for serving it — its size, stored type and version —\n * without reading its bytes, or `null` when there is no such object.\n *\n * Optional, together with {@link getObjectStream}. A controller that has\n * both is served streamed: `GET /file/*` answers a `304`, a `416`, a `HEAD`\n * and a cached image rendition from this alone, and otherwise reads only\n * the bytes it sends — the requested range, a chunk at a time. One without\n * them is served through {@link getObject}, which holds the whole object in\n * memory for every request, a `Range` request for one megabyte included.\n */\n statObject?(key: string, bucket?: string): Promise<StoredObjectInfo | null>;\n\n /**\n * Read an object, or one byte range of it, as a stream, or `null` when\n * there is no such object. See {@link statObject}.\n *\n * The stream holds a file or a connection until it is read to the end or\n * cancelled, so a caller that decides not to read it must cancel it.\n */\n getObjectStream?(key: string, options?: { bucket?: string; range?: StoredObjectRange }): Promise<StoredObjectStream | null>;\n\n /**\n * Delete an object\n */\n deleteObject(key: string, bucket?: string): Promise<void>;\n\n /**\n * List objects in a prefix\n */\n listObjects(prefix: string, options?: {\n bucket?: string;\n maxResults?: number;\n pageToken?: string;\n }): Promise<StorageListResult>;\n\n /**\n * Get the storage provider identifier.\n *\n * Built-in values are `'local'` and `'s3'`. Custom implementations\n * should return their own identifier (e.g. `'gcs'`, `'azure'`).\n */\n getType(): string;\n\n /**\n * The buckets this controller serves, if it knows.\n *\n * The `bucket` a request may name is not a free-text field: on S3 and GCS it\n * goes straight to the provider, so an unvalidated one addresses *any*\n * bucket the deployment's credentials can reach, and locally it names a\n * directory the framework would create on demand. Either way, a caller who\n * mistypes it gets \"file not found\" — the same answer as a missing object —\n * with nothing to say the bucket was the problem.\n *\n * So a controller that knows its buckets says so, and the routes refuse\n * anything else with `UNKNOWN_STORAGE_SOURCE` naming what is served. A\n * controller that does not implement this keeps the old behaviour and is\n * handed whatever the caller wrote — a custom implementation may address\n * buckets this framework has no way to enumerate.\n *\n * `\"default\"` is always among them: it is the logical name every route uses\n * when the caller names none, and each provider maps it to its own.\n */\n knownBuckets?(): string[];\n\n /**\n * The largest file this controller accepts, in bytes.\n *\n * Every door that takes a file reads it, so the number a client is told is\n * the number it is held to: the upload route checks it before anything is\n * stored, the resumable route checks it at creation — before the first\n * chunk rather than after the last — and advertises it as `Tus-Max-Size`,\n * and the server's upload body limit is the largest of them. A controller\n * that does not implement this is held to the deployment-wide limit alone.\n */\n maxFileSize?(): number;\n}\n\n/**\n * What serving an object needs to know before deciding whether to read it.\n * See {@link StorageController.statObject}.\n */\nexport interface StoredObjectInfo {\n /** The whole object's size in bytes. */\n size: number;\n /** The stored content type — the uploader's claim, not a sniffed one. */\n contentType: string;\n /**\n * The object's own modification time, in milliseconds since the epoch.\n * The routes build `ETag`, `Last-Modified` and the transform cache key from\n * it, so it must be the object's version, not the time of the read.\n */\n lastModified: number;\n}\n\n/** An inclusive byte range, as `Range: bytes=start-end` names one. */\nexport interface StoredObjectRange {\n start: number;\n end: number;\n}\n\n/** An object's bytes as a stream. See {@link StorageController.getObjectStream}. */\nexport interface StoredObjectStream extends StoredObjectInfo {\n /** The bytes: the range when {@link range} is set, the whole object otherwise. */\n stream: ReadableStream<Uint8Array>;\n /**\n * The range {@link stream} holds — the one asked for — or absent when it\n * holds the whole object. A controller may answer a range with the whole\n * object, as HTTP lets a server do; the caller then serves it whole.\n */\n range?: StoredObjectRange;\n}\n\n/**\n * Default maximum file size (50MB)\n */\nexport const DEFAULT_MAX_FILE_SIZE = 50 * 1024 * 1024;\n\n/**\n * Common image MIME types\n */\nexport const IMAGE_MIME_TYPES = [\n \"image/jpeg\",\n \"image/png\",\n \"image/gif\",\n \"image/webp\",\n \"image/svg+xml\",\n \"image/bmp\",\n \"image/tiff\"\n];\n\n/**\n * Common document MIME types\n */\nexport const DOCUMENT_MIME_TYPES = [\n \"application/pdf\",\n \"application/msword\",\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n \"application/vnd.ms-excel\",\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n \"application/vnd.ms-powerpoint\",\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n \"text/plain\",\n \"text/csv\"\n];\n","/**\n * Moving object bytes without holding them.\n *\n * A stored object can be as large as the deployment's `maxFileSize` allows — a\n * video, a database dump — and the paths that used to read one whole (a\n * `File`, an `arrayBuffer()`, a `Buffer.concat`) held it three or four times\n * over per request. These helpers are what the streamed paths share instead.\n */\nimport fs from \"node:fs\";\n\n/** Anything that hands over bytes a chunk at a time: a Node readable, an SDK body. */\ntype ByteSource = AsyncIterable<Uint8Array>;\n\n/**\n * A byte source as a web stream that reads a chunk only when the consumer asks\n * for one, and releases the source when the consumer stops early.\n *\n * Pull-based with a zero high-water mark, so nothing is read ahead of the\n * reader: a slow client holds one chunk, not a queue of them. A source given\n * as a function is opened on the first read, which keeps a stream nobody reads\n * — a `HEAD`, a 304 decided late — from holding a file descriptor or a\n * connection open. Cancelling (a client that disconnects mid-download)\n * returns the iterator, which destroys a Node stream and so closes its file or\n * socket.\n */\nexport function webStreamFrom(source: ByteSource | (() => ByteSource)): ReadableStream<Uint8Array> {\n let iterator: AsyncIterator<Uint8Array> | undefined;\n const open = (): AsyncIterator<Uint8Array> => {\n iterator ??= (typeof source === \"function\" ? source() : source)[Symbol.asyncIterator]();\n return iterator;\n };\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n const next = await open().next();\n if (next.done) controller.close();\n else controller.enqueue(next.value);\n } catch (err) {\n controller.error(err);\n }\n },\n async cancel() {\n // A source never opened has nothing to release — and opening it\n // now only to close it would be the cost this avoids.\n if (!iterator && typeof source === \"function\") return;\n try {\n await open().return?.();\n } catch {\n // Already failed or closed; either way it is released.\n }\n }\n }, { highWaterMark: 0 });\n}\n\n/**\n * A file on disk, or an inclusive byte range of it, as a web stream. The file\n * is opened on the first read; see {@link webStreamFrom}.\n */\nexport function fileWebStream(filePath: string, range?: { start: number; end: number }): ReadableStream<Uint8Array> {\n return webStreamFrom(() => fs.createReadStream(filePath, range ? { start: range.start, end: range.end } : {}));\n}\n\n/** A buffer already in memory as a web stream of views onto it, never copies. */\nexport function bufferWebStream(bytes: Uint8Array, chunkSize = 64 * 1024): ReadableStream<Uint8Array> {\n let offset = 0;\n return new ReadableStream<Uint8Array>({\n pull(controller) {\n if (offset >= bytes.byteLength) {\n controller.close();\n return;\n }\n const end = Math.min(offset + chunkSize, bytes.byteLength);\n controller.enqueue(bytes.subarray(offset, end));\n offset = end;\n }\n }, { highWaterMark: 0 });\n}\n\n/**\n * Read a stream into one buffer.\n *\n * For the one consumer that needs the whole object at once — an image decode.\n * With the size known the bytes are copied once, into a buffer of that size,\n * rather than collected and then concatenated (which holds them twice); a\n * stream that turns out longer or shorter than announced still comes back\n * whole and exact.\n */\nexport async function readStreamFully(stream: ReadableStream<Uint8Array>, expectedSize?: number): Promise<Buffer> {\n const reader = stream.getReader();\n const target = expectedSize !== undefined && expectedSize > 0 ? Buffer.allocUnsafe(expectedSize) : undefined;\n let offset = 0;\n let overflow: Uint8Array[] | undefined;\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n if (target && !overflow && offset + value.byteLength <= target.byteLength) {\n target.set(value, offset);\n } else {\n overflow ??= target ? [target.subarray(0, offset)] : [];\n overflow.push(value);\n }\n offset += value.byteLength;\n }\n } finally {\n reader.releaseLock();\n }\n if (overflow) return Buffer.concat(overflow, offset);\n if (!target) return Buffer.alloc(0);\n return offset === target.byteLength ? target : target.subarray(0, offset);\n}\n\n/**\n * The bytes of a buffer as a `Uint8Array` the response APIs accept, without\n * copying them. `new Uint8Array(buffer)` copies; a view does not. Only a buffer\n * over shared memory — which nothing here produces — is copied.\n */\nexport function bytesView(bytes: Uint8Array): Uint8Array<ArrayBuffer> {\n const backing = bytes.buffer;\n return backing instanceof ArrayBuffer\n ? new Uint8Array(backing, bytes.byteOffset, bytes.byteLength)\n : new Uint8Array(bytes);\n}\n","/**\n * What every storage controller checks about a file before it stores it: the\n * source's own size cap and, when configured, its allowed content types.\n *\n * One function for the three controllers, which carried a copy each. Both\n * refusals are `ApiError`s, so they reach the caller as what they are. A plain\n * `Error` was a 500 \"Internal Server Error\" on `/upload` and a 502 on TUS —\n * after the client had sent every byte.\n */\n\nimport { ApiError } from \"../api/errors\";\n\nexport function validateUploadedFile(\n file: { size: number; type: string },\n maxSize: number,\n allowedMimeTypes: string[] | undefined\n): void {\n if (file.size > maxSize) {\n throw new ApiError(\n 413,\n \"PAYLOAD_TOO_LARGE\",\n `File size ${file.size} exceeds this storage source's maximum of ${maxSize} bytes.`,\n { size: file.size, maxFileSize: maxSize }\n );\n }\n\n if (allowedMimeTypes && allowedMimeTypes.length > 0 && !allowedMimeTypes.includes(file.type)) {\n // The code a property's `acceptedFiles` refusal answers with: to the\n // caller, \"this source does not take this type\" is the same mistake.\n throw ApiError.badRequest(\n `File type ${file.type || \"(none)\"} is not allowed on this storage source. ` +\n `Allowed types: ${allowedMimeTypes.join(\", \")}.`,\n \"STORAGE_FILE_TYPE_REFUSED\",\n { allowedMimeTypes, contentType: file.type }\n );\n }\n}\n"],"mappings":"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"}