@rebasepro/server 0.23.0 → 0.24.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 (181) hide show
  1. package/README.md +1 -1
  2. package/bin/rebase-server.js +4 -2
  3. package/dist/{GCSStorageController-CjrA4PMo.js → GCSStorageController-BSiP1c-f.js} +22 -8
  4. package/dist/GCSStorageController-BSiP1c-f.js.map +1 -0
  5. package/dist/{S3StorageController-B6pKDNVj.js → S3StorageController-CAwFRgjV.js} +19 -7
  6. package/dist/S3StorageController-CAwFRgjV.js.map +1 -0
  7. package/dist/api/ast-schema-editor.d.ts +92 -1
  8. package/dist/api/errors.d.ts +9 -0
  9. package/dist/api/live-schema-routes.d.ts +38 -8
  10. package/dist/api/logs-routes.d.ts +39 -1
  11. package/dist/api/openapi-generator.d.ts +17 -0
  12. package/dist/api/rest/api-generator.d.ts +44 -10
  13. package/dist/api/rest/write-validation.d.ts +2 -2
  14. package/dist/api/types.d.ts +17 -1
  15. package/dist/{ast-schema-editor-Mvr50v_S.js → ast-schema-editor-CWqS_sLJ.js} +309 -11
  16. package/dist/ast-schema-editor-CWqS_sLJ.js.map +1 -0
  17. package/dist/auth/access.d.ts +105 -0
  18. package/dist/auth/adapter-middleware.d.ts +2 -1
  19. package/dist/auth/address-ownership.d.ts +16 -1
  20. package/dist/auth/admin-roles-route.d.ts +4 -2
  21. package/dist/auth/admin-roles.d.ts +17 -20
  22. package/dist/auth/admin-users-route.d.ts +1 -0
  23. package/dist/auth/api-keys/api-key-middleware.d.ts +56 -55
  24. package/dist/auth/api-keys/api-key-routes.d.ts +41 -11
  25. package/dist/auth/api-keys/api-key-store.d.ts +31 -8
  26. package/dist/auth/api-keys/api-key-types.d.ts +14 -16
  27. package/dist/auth/api-keys/http-operation.d.ts +19 -0
  28. package/dist/auth/api-keys/index.d.ts +11 -11
  29. package/dist/auth/api-keys/key-grant.d.ts +41 -0
  30. package/dist/auth/api-keys/legacy-permissions.d.ts +33 -0
  31. package/dist/auth/auth-hooks.d.ts +46 -7
  32. package/dist/auth/builtin-auth-adapter.d.ts +8 -0
  33. package/dist/auth/cookie-utils.d.ts +7 -0
  34. package/dist/auth/deliverable-address.d.ts +6 -0
  35. package/dist/auth/email-change-routes.d.ts +41 -0
  36. package/dist/auth/expired-token-sweep.d.ts +67 -0
  37. package/dist/auth/impersonation.d.ts +110 -0
  38. package/dist/auth/index.d.ts +4 -2
  39. package/dist/auth/interfaces.d.ts +110 -59
  40. package/dist/auth/jwt.d.ts +49 -3
  41. package/dist/auth/magic-link-routes.d.ts +2 -6
  42. package/dist/auth/mfa-routes.d.ts +2 -9
  43. package/dist/auth/middleware.d.ts +17 -5
  44. package/dist/auth/otp-routes.d.ts +2 -6
  45. package/dist/auth/passwordless-signup.d.ts +27 -0
  46. package/dist/auth/platform-token.d.ts +122 -0
  47. package/dist/auth/rate-limiter.d.ts +41 -0
  48. package/dist/auth/routes.d.ts +45 -0
  49. package/dist/auth/scope-routes.d.ts +22 -0
  50. package/dist/auth/session-routes.d.ts +11 -6
  51. package/dist/auth/token-revocation.d.ts +50 -1
  52. package/dist/auth/verify-credential.d.ts +28 -0
  53. package/dist/{auth-B-GIMpDG.js → auth-DMLngxn_.js} +2159 -569
  54. package/dist/auth-DMLngxn_.js.map +1 -0
  55. package/dist/backend-DTAOsLQc.js.map +1 -1
  56. package/dist/backup/backup-common.d.ts +10 -0
  57. package/dist/backup/backup-routes.d.ts +24 -4
  58. package/dist/backup/backup-schedule.d.ts +33 -0
  59. package/dist/backup/backup-storage.d.ts +14 -0
  60. package/dist/backup/index.d.ts +2 -0
  61. package/dist/backup-CN0s50D2.js +444 -0
  62. package/dist/backup-CN0s50D2.js.map +1 -0
  63. package/dist/boot/bundle.d.ts +19 -0
  64. package/dist/boot/env.d.ts +49 -4
  65. package/dist/boot/security-headers.d.ts +26 -0
  66. package/dist/boot/static-routing.d.ts +56 -0
  67. package/dist/collection_patch-BRu-BvDv.js +472 -0
  68. package/dist/collection_patch-BRu-BvDv.js.map +1 -0
  69. package/dist/{contract-routes-CbFjuBwa.js → contract-routes-fz8i4pxs.js} +17 -4
  70. package/dist/contract-routes-fz8i4pxs.js.map +1 -0
  71. package/dist/cron/cron-scheduler.d.ts +25 -20
  72. package/dist/cron/cron-store.d.ts +6 -2
  73. package/dist/{cron-loader-DfTj2Hbi.js → cron-loader-CwaANlOG.js} +4 -4
  74. package/dist/cron-loader-CwaANlOG.js.map +1 -0
  75. package/dist/{cron-routes-eE8nif_b.js → cron-routes-Bc-SB0Se.js} +10 -7
  76. package/dist/cron-routes-Bc-SB0Se.js.map +1 -0
  77. package/dist/{cron-scheduler-B0pLfAix.js → cron-scheduler-CYQgco86.js} +52 -34
  78. package/dist/cron-scheduler-CYQgco86.js.map +1 -0
  79. package/dist/{cron-store-TcoGz-xS.js → cron-store-D2Q9-Aco.js} +10 -15
  80. package/dist/cron-store-D2Q9-Aco.js.map +1 -0
  81. package/dist/{ddl-bootstrap-C6mo0Kmz.js → ddl-bootstrap-BaqMSa4Y.js} +2 -2
  82. package/dist/{ddl-bootstrap-C6mo0Kmz.js.map → ddl-bootstrap-BaqMSa4Y.js.map} +1 -1
  83. package/dist/email/index.d.ts +2 -2
  84. package/dist/email/templates.d.ts +22 -0
  85. package/dist/email/types.d.ts +26 -0
  86. package/dist/env.d.ts +1 -2
  87. package/dist/{errors-DWsX4yTd.js → errors-D6_y86c5.js} +102 -8
  88. package/dist/errors-D6_y86c5.js.map +1 -0
  89. package/dist/{function-loader-xnbDAPfa.js → function-loader-D7o5Epjj.js} +2 -2
  90. package/dist/{function-loader-xnbDAPfa.js.map → function-loader-D7o5Epjj.js.map} +1 -1
  91. package/dist/{function-routes-Chet4-lB.js → function-routes-CaNG4waN.js} +24 -12
  92. package/dist/function-routes-CaNG4waN.js.map +1 -0
  93. package/dist/functions/context.d.ts +17 -6
  94. package/dist/functions/guards.d.ts +22 -5
  95. package/dist/functions/index.d.ts +2 -2
  96. package/dist/functions/index.js +90 -36
  97. package/dist/functions/index.js.map +1 -1
  98. package/dist/{history-recorder-B4MpJfJK.js → history-recorder-Nr8zLvoU.js} +4 -4
  99. package/dist/{history-recorder-B4MpJfJK.js.map → history-recorder-Nr8zLvoU.js.map} +1 -1
  100. package/dist/{history-store-BhxWOuz9.js → history-store-rcAm_xFR.js} +2 -2
  101. package/dist/{history-store-BhxWOuz9.js.map → history-store-rcAm_xFR.js.map} +1 -1
  102. package/dist/index.d.ts +8 -2
  103. package/dist/index.es.js +3084 -753
  104. package/dist/index.es.js.map +1 -1
  105. package/dist/init/health.d.ts +17 -2
  106. package/dist/init/shutdown.d.ts +10 -0
  107. package/dist/init.d.ts +54 -0
  108. package/dist/{jobs-CazMYhyy.js → jobs-DqYNfquG.js} +5 -5
  109. package/dist/{jobs-CazMYhyy.js.map → jobs-DqYNfquG.js.map} +1 -1
  110. package/dist/{jwt-DnQHNFCl.js → jwt-R6bSPMjk.js} +39 -15
  111. package/dist/{jwt-DnQHNFCl.js.map → jwt-R6bSPMjk.js.map} +1 -1
  112. package/dist/{keys-CogCQpxG.js → keys-GAVZqbqx.js} +3 -17
  113. package/dist/{keys-CogCQpxG.js.map → keys-GAVZqbqx.js.map} +1 -1
  114. package/dist/{logger-DO2PZc4i.js → logger-D-S-hO5e.js} +26 -3
  115. package/dist/logger-D-S-hO5e.js.map +1 -0
  116. package/dist/{logs-routes-Bj4TYYUl.js → logs-routes-DAdv37GI.js} +48 -8
  117. package/dist/logs-routes-DAdv37GI.js.map +1 -0
  118. package/dist/mcp/consent-page.d.ts +1 -1
  119. package/dist/mcp/mcp-routes.d.ts +7 -0
  120. package/dist/mcp/mcp-tools.d.ts +15 -9
  121. package/dist/mcp/oauth-metadata.d.ts +21 -16
  122. package/dist/mcp/oauth-routes.d.ts +7 -1
  123. package/dist/{openapi-generator-O_O24MAT.js → openapi-generator-DAq_XVDu.js} +104 -13
  124. package/dist/openapi-generator-DAq_XVDu.js.map +1 -0
  125. package/dist/{proxy-Czngl3p9.js → proxy-qRlqeUmO.js} +2 -2
  126. package/dist/{proxy-Czngl3p9.js.map → proxy-qRlqeUmO.js.map} +1 -1
  127. package/dist/{query-parser-DGRVFNM3.js → query-parser-BgiKJKvc.js} +6 -56
  128. package/dist/query-parser-BgiKJKvc.js.map +1 -0
  129. package/dist/{request-timeout-C_4C2BeR.js → request-timeout-DgH7j8qO.js} +3 -3
  130. package/dist/{request-timeout-C_4C2BeR.js.map → request-timeout-DgH7j8qO.js.map} +1 -1
  131. package/dist/schema-edit/apply-schema-change.d.ts +63 -3
  132. package/dist/schema-edit/project-root.d.ts +3 -2
  133. package/dist/schema-edit/remote-source.d.ts +9 -4
  134. package/dist/{schema-editor-routes-C5-lh_jO.js → schema-editor-routes-oIyuWl3L.js} +12 -7
  135. package/dist/schema-editor-routes-oIyuWl3L.js.map +1 -0
  136. package/dist/serve-spa.d.ts +58 -0
  137. package/dist/services/routed-realtime-service.d.ts +11 -0
  138. package/dist/soft-delete-params-BWPilMPF.js +59 -0
  139. package/dist/soft-delete-params-BWPilMPF.js.map +1 -0
  140. package/dist/{src-vkcwKXbT.js → src-CatHFUym.js} +439 -20
  141. package/dist/src-CatHFUym.js.map +1 -0
  142. package/dist/{src-pmvW7BFx.js → src-I3aG1PcY.js} +252 -70
  143. package/dist/src-I3aG1PcY.js.map +1 -0
  144. package/dist/storage/GCSStorageController.d.ts +2 -0
  145. package/dist/storage/LocalStorageController.d.ts +2 -0
  146. package/dist/storage/S3StorageController.d.ts +2 -0
  147. package/dist/storage/index.d.ts +2 -2
  148. package/dist/storage/property-limits.d.ts +41 -6
  149. package/dist/storage/request-keys.d.ts +15 -0
  150. package/dist/storage/requested-object.d.ts +74 -0
  151. package/dist/storage/routes.d.ts +36 -18
  152. package/dist/storage/tus-handler.d.ts +30 -5
  153. package/dist/storage/types.d.ts +19 -0
  154. package/dist/types-BfKcm9do.js.map +1 -1
  155. package/dist/utils/logger.d.ts +12 -0
  156. package/package.json +5 -5
  157. package/dist/GCSStorageController-CjrA4PMo.js.map +0 -1
  158. package/dist/S3StorageController-B6pKDNVj.js.map +0 -1
  159. package/dist/admin-roles-vYdp_Pil.js +0 -36
  160. package/dist/admin-roles-vYdp_Pil.js.map +0 -1
  161. package/dist/admin_block-DxKLmdiv.js +0 -206
  162. package/dist/admin_block-DxKLmdiv.js.map +0 -1
  163. package/dist/ast-schema-editor-Mvr50v_S.js.map +0 -1
  164. package/dist/auth/api-keys/api-key-permission-guard.d.ts +0 -65
  165. package/dist/auth-B-GIMpDG.js.map +0 -1
  166. package/dist/backup-D7YR94N3.js +0 -253
  167. package/dist/backup-D7YR94N3.js.map +0 -1
  168. package/dist/contract-routes-CbFjuBwa.js.map +0 -1
  169. package/dist/cron-loader-DfTj2Hbi.js.map +0 -1
  170. package/dist/cron-routes-eE8nif_b.js.map +0 -1
  171. package/dist/cron-scheduler-B0pLfAix.js.map +0 -1
  172. package/dist/cron-store-TcoGz-xS.js.map +0 -1
  173. package/dist/errors-DWsX4yTd.js.map +0 -1
  174. package/dist/function-routes-Chet4-lB.js.map +0 -1
  175. package/dist/logger-DO2PZc4i.js.map +0 -1
  176. package/dist/logs-routes-Bj4TYYUl.js.map +0 -1
  177. package/dist/openapi-generator-O_O24MAT.js.map +0 -1
  178. package/dist/query-parser-DGRVFNM3.js.map +0 -1
  179. package/dist/schema-editor-routes-C5-lh_jO.js.map +0 -1
  180. package/dist/src-pmvW7BFx.js.map +0 -1
  181. package/dist/src-vkcwKXbT.js.map +0 -1
@@ -2,9 +2,44 @@ import { createRequire as __rebaseCreateRequire } from "module";
2
2
  import __rebaseProcess from "process";
3
3
  globalThis.process ??= __rebaseProcess;
4
4
  __rebaseCreateRequire(import.meta.url);
5
- import { B as fieldKeyForColumn, E as effectiveAccess, G as resolveCollectionRelations, R as getTenantConfig, U as isRelationRequired, V as findRelation, pt as isToMany } from "./src-pmvW7BFx.js";
6
- import "./src-vkcwKXbT.js";
5
+ import { n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
6
+ import { B as findRelation, H as isRelationRequired, L as getTenantConfig, T as effectiveAccess, W as resolveCollectionRelations, ft as isToMany, z as fieldKeyForColumn } from "./src-I3aG1PcY.js";
7
+ import "./src-CatHFUym.js";
8
+ import { n as HARD_DELETE_QUERY_PARAM, t as DELETED_QUERY_PARAM } from "./soft-delete-params-BWPilMPF.js";
9
+ /**
10
+ * The property a `softDelete` collection records a deletion in — its wire
11
+ * name, what a delete stamps and what a restore (an update to `null`) clears —
12
+ * or `undefined` for a collection without soft delete.
13
+ *
14
+ * One reading of the declaration for every layer that needs it: the driver
15
+ * that stamps and filters on it, and the REST route that has to recognise a
16
+ * restore before it looks the row up.
17
+ */
18
+ function softDeleteFieldOf(collection) {
19
+ const declared = collection && "softDelete" in collection ? collection.softDelete : void 0;
20
+ if (!declared) return void 0;
21
+ return typeof declared === "object" && declared.field || "deletedAt";
22
+ }
23
+ /**
24
+ * Whether an update's values restore a soft-deleted row: they set the
25
+ * soft-delete field back to `null`, the documented restore.
26
+ *
27
+ * A restore is an ordinary update of a row every default read hides, so a door
28
+ * that looks the row up before updating it has to include the trashed rows for
29
+ * this update — and only for this one: any other edit of a trashed row is a
30
+ * 404.
31
+ */
32
+ function restoresSoftDeletedRow(collection, values) {
33
+ const field = softDeleteFieldOf(collection);
34
+ return field !== void 0 && values?.[field] === null;
35
+ }
36
+ //#endregion
7
37
  //#region src/api/openapi-generator.ts
38
+ var openapi_generator_exports = /* @__PURE__ */ __exportAll({
39
+ buildCollectionInputSchema: () => buildCollectionInputSchema,
40
+ buildCollectionSchema: () => buildCollectionSchema,
41
+ generateOpenApiSpec: () => generateOpenApiSpec
42
+ });
8
43
  function generateOpenApiSpec(collections, options = {}) {
9
44
  const basePath = options.basePath ?? "/api";
10
45
  const requireAuth = options.requireAuth ?? true;
@@ -220,7 +255,50 @@ function generateOpenApiSpec(collections, options = {}) {
220
255
  const paths = spec.paths;
221
256
  const schemas = spec.components.schemas;
222
257
  const tags = spec.tags;
223
- const reservedParameterNames = new Set(listQueryParameters().map((p) => p.name));
258
+ const reservedParameterNames = /* @__PURE__ */ new Set([...listQueryParameters().map((p) => p.name), DELETED_QUERY_PARAM]);
259
+ /**
260
+ * `?deleted=` on the reads of a `softDelete` collection. A delete there
261
+ * moves the row to the trash, and every read hides trashed rows unless it
262
+ * asks for them; the list, the single row, the count and the aggregate all
263
+ * honour this. Empty for any other collection.
264
+ */
265
+ const deletedParameters = (collection) => {
266
+ const field = softDeleteFieldOf(collection);
267
+ if (!field) return [];
268
+ return [{
269
+ name: DELETED_QUERY_PARAM,
270
+ in: "query",
271
+ required: false,
272
+ schema: {
273
+ type: "string",
274
+ enum: ["include", "only"]
275
+ },
276
+ description: `Rows a delete moved to the trash (\`${field}\` set) are left out unless this asks for them: \`include\` reads them with the live rows, \`only\` reads them alone. Any other value is a 400 INVALID_DELETED_PARAM.`
277
+ }];
278
+ };
279
+ /**
280
+ * `?hard=` on the deletes of a `softDelete` collection: delete for good
281
+ * rather than move to the trash. Empty for any other collection.
282
+ */
283
+ const hardDeleteParameters = (collection) => {
284
+ if (!softDeleteFieldOf(collection)) return [];
285
+ return [{
286
+ name: HARD_DELETE_QUERY_PARAM,
287
+ in: "query",
288
+ required: false,
289
+ schema: {
290
+ type: "boolean",
291
+ default: false
292
+ },
293
+ description: "`true` deletes for good instead of moving to the trash — a row already in the trash included, which is how the trash is emptied. Only `true`/`1` and `false`/`0` are accepted; anything else is a 400 INVALID_HARD_PARAM."
294
+ }];
295
+ };
296
+ /** What a delete does to a `softDelete` collection's row, for the delete operations' descriptions. */
297
+ const softDeleteBehaviour = (collection) => {
298
+ const field = softDeleteFieldOf(collection);
299
+ if (!field) return void 0;
300
+ return `This collection soft-deletes: a delete moves the row to the trash by stamping \`${field}\`, and every read leaves it out unless it passes \`?deleted=include\` or \`?deleted=only\`. A PATCH setting \`${field}\` back to \`null\` restores it. Pass \`?hard=true\` to delete for good.`;
301
+ };
224
302
  const registeredSchemas = new Set((collections || []).map(schemaNameFor));
225
303
  /**
226
304
  * `Prefer: return=minimal`, on every route that would otherwise send a row
@@ -387,12 +465,17 @@ function generateOpenApiSpec(collections, options = {}) {
387
465
  schemas[`${schemaName}Input`] = buildCollectionInputSchema(collection);
388
466
  schemas[`${schemaName}Update`] = buildCollectionUpdateSchema(collection);
389
467
  const dataPath = `/data/${slug}`;
468
+ const softDeleteNote = softDeleteBehaviour(collection);
390
469
  paths[`${dataPath}/count`] = { get: {
391
470
  tags: [collection.name],
392
471
  summary: `Count ${collection.name}`,
393
472
  description: "The number of rows the same filters would return, without returning them. Takes the filter and search parameters of the list endpoint; `limit`, `offset` and `orderBy` are not part of the question and are ignored.",
394
473
  operationId: `count${schemaName}`,
395
- parameters: [...listQueryParameters().filter((p) => p.name === "searchString"), ...buildFilterParameters(collection, reservedParameterNames)],
474
+ parameters: [
475
+ ...listQueryParameters().filter((p) => p.name === "searchString"),
476
+ ...deletedParameters(collection),
477
+ ...buildFilterParameters(collection, reservedParameterNames)
478
+ ],
396
479
  responses: {
397
480
  200: {
398
481
  description: "The number of matching rows",
@@ -439,6 +522,7 @@ function generateOpenApiSpec(collections, options = {}) {
439
522
  schema: { type: "string" },
440
523
  example: "count:desc"
441
524
  },
525
+ ...deletedParameters(collection),
442
526
  ...buildFilterParameters(collection, reservedParameterNames)
443
527
  ],
444
528
  responses: {
@@ -479,7 +563,11 @@ function generateOpenApiSpec(collections, options = {}) {
479
563
  tags: [collection.name],
480
564
  summary: `List ${collection.name}`,
481
565
  operationId: `list${schemaName}`,
482
- parameters: [...listQueryParameters(), ...buildFilterParameters(collection, reservedParameterNames)],
566
+ parameters: [
567
+ ...listQueryParameters(),
568
+ ...deletedParameters(collection),
569
+ ...buildFilterParameters(collection, reservedParameterNames)
570
+ ],
483
571
  responses: {
484
572
  200: {
485
573
  description: "Paginated list of entities",
@@ -649,9 +737,9 @@ function generateOpenApiSpec(collections, options = {}) {
649
737
  paths[`/data/${slug}/bulk/delete`] = { post: {
650
738
  tags: [collection.name],
651
739
  summary: `Delete many ${collection.name} in one transaction`,
652
- description: "A POST, not `DELETE /bulk` with a body. Bodies on DELETE are permitted but widely dropped by proxies and CDNs, and several generators ignore `requestBody` on a DELETE operation — a generated client would send the request with no ids at all. Takes ids rather than a filter: a mistyped condition that empties a table cannot be reviewed at the call site the way an explicit list can. `beforeDelete`/`afterDelete` fire per row.",
740
+ description: "A POST, not `DELETE /bulk` with a body. Bodies on DELETE are permitted but widely dropped by proxies and CDNs, and several generators ignore `requestBody` on a DELETE operation — a generated client would send the request with no ids at all. Takes ids rather than a filter: a mistyped condition that empties a table cannot be reviewed at the call site the way an explicit list can. `beforeDelete`/`afterDelete` fire per row." + (softDeleteNote ? `\n\n${softDeleteNote}` : ""),
653
741
  operationId: `deleteMany${schemaName}`,
654
- parameters: [idempotencyHeader],
742
+ parameters: [idempotencyHeader, ...hardDeleteParameters(collection)],
655
743
  requestBody: {
656
744
  required: true,
657
745
  content: { "application/json": { schema: {
@@ -665,7 +753,7 @@ function generateOpenApiSpec(collections, options = {}) {
665
753
  },
666
754
  responses: {
667
755
  200: {
668
- description: "How many rows were deleted",
756
+ description: softDeleteNote ? "How many rows were moved to the trash, or deleted for good with `?hard=true`" : "How many rows were deleted",
669
757
  content: { "application/json": { schema: {
670
758
  type: "object",
671
759
  properties: { meta: {
@@ -708,7 +796,8 @@ function generateOpenApiSpec(collections, options = {}) {
708
796
  schema: { type: "string" },
709
797
  description: "Comma-separated columns to return, as a SELECT projection. The primary key always survives and `excludeFromApi` columns stay hidden.",
710
798
  example: "id,title"
711
- }
799
+ },
800
+ ...deletedParameters(collection)
712
801
  ],
713
802
  responses: {
714
803
  200: {
@@ -735,6 +824,7 @@ function generateOpenApiSpec(collections, options = {}) {
735
824
  delete: {
736
825
  tags: [collection.name],
737
826
  summary: `Delete ${collection.singularName || collection.name}`,
827
+ ...softDeleteNote ? { description: softDeleteNote } : {},
738
828
  operationId: `delete${schemaName}`,
739
829
  parameters: [
740
830
  {
@@ -751,10 +841,11 @@ function generateOpenApiSpec(collections, options = {}) {
751
841
  required: false,
752
842
  schema: { type: "string" },
753
843
  description: "Names this delete so a retry replays its answer. A delete replayed after the first attempt committed would otherwise answer 404 — which an offline queue reads as a permanent failure for a delete that in fact succeeded."
754
- }
844
+ },
845
+ ...hardDeleteParameters(collection)
755
846
  ],
756
847
  responses: {
757
- 204: { description: "Deleted successfully" },
848
+ 204: { description: softDeleteNote ? "Moved to the trash: the row still exists and can be restored. With `?hard=true`, deleted for good" : "Deleted successfully" },
758
849
  404: {
759
850
  description: "Entity not found",
760
851
  content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
@@ -1446,6 +1537,6 @@ function toPascalCase(str) {
1446
1537
  return str.replace(/[^a-zA-Z0-9]+/g, " ").split(" ").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
1447
1538
  }
1448
1539
  //#endregion
1449
- export { generateOpenApiSpec };
1540
+ export { softDeleteFieldOf as a, restoresSoftDeletedRow as i, buildCollectionSchema as n, openapi_generator_exports as r, buildCollectionInputSchema as t };
1450
1541
 
1451
- //# sourceMappingURL=openapi-generator-O_O24MAT.js.map
1542
+ //# sourceMappingURL=openapi-generator-DAq_XVDu.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openapi-generator-DAq_XVDu.js","names":[],"sources":["../../common/src/collections/soft-delete.ts","../src/api/openapi-generator.ts"],"sourcesContent":["import type { CollectionConfig } from \"@rebasepro/types\";\n\n/** The field `softDelete: true` means, when the object form names none. */\nexport const DEFAULT_SOFT_DELETE_FIELD = \"deletedAt\";\n\n/**\n * The property a `softDelete` collection records a deletion in — its wire\n * name, what a delete stamps and what a restore (an update to `null`) clears —\n * or `undefined` for a collection without soft delete.\n *\n * One reading of the declaration for every layer that needs it: the driver\n * that stamps and filters on it, and the REST route that has to recognise a\n * restore before it looks the row up.\n */\nexport function softDeleteFieldOf(collection: CollectionConfig | undefined): string | undefined {\n // Declared on the Postgres collection shape only; narrowed, not cast.\n const declared = collection && \"softDelete\" in collection ? collection.softDelete : undefined;\n if (!declared) return undefined;\n return (typeof declared === \"object\" && declared.field) || DEFAULT_SOFT_DELETE_FIELD;\n}\n\n/**\n * Whether an update's values restore a soft-deleted row: they set the\n * soft-delete field back to `null`, the documented restore.\n *\n * A restore is an ordinary update of a row every default read hides, so a door\n * that looks the row up before updating it has to include the trashed rows for\n * this update — and only for this one: any other edit of a trashed row is a\n * 404.\n */\nexport function restoresSoftDeletedRow(\n collection: CollectionConfig | undefined,\n values: Record<string, unknown> | undefined\n): boolean {\n const field = softDeleteFieldOf(collection);\n return field !== undefined && values?.[field] === null;\n}\n","import { CollectionConfig, Property, StringProperty, NumberProperty, ArrayProperty, MapProperty, isToMany, ResolvedRelation, VectorProperty, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT } from \"@rebasepro/types\";\nimport { effectiveAccess, fieldKeyForColumn, findRelation, getTenantConfig, isRelationRequired, resolveCollectionRelations, softDeleteFieldOf } from \"@rebasepro/common\";\nimport { DELETED_QUERY_PARAM, HARD_DELETE_QUERY_PARAM } from \"./rest/soft-delete-params\";\n\n/**\n * OpenAPI 3.0.3 specification generator.\n *\n * Produces a spec that exactly mirrors the REST API consumed by the\n * Rebase SDK client (`@rebasepro/client`).\n *\n * Routes are mounted at `{basePath}/data/{slug}` by `initializeRebaseBackend`.\n */\n\nexport interface OpenApiGeneratorOptions {\n /** Base path for the API (e.g. \"/api\"). Defaults to \"/api\". */\n basePath?: string;\n /** Whether auth is enabled on data routes. Defaults to true. */\n requireAuth?: boolean;\n /**\n * The list-pagination bounds the REST layer applies, so the spec states the\n * ones a request will actually meet.\n *\n * These were hardcoded as `default: 20, maximum: 100` — neither of which\n * the server has ever used. The spec drives the API Explorer and is what a\n * generated client is built from, so an understated ceiling is a request\n * the client refuses to make, and an overstated one is a 400 nobody\n * predicted.\n */\n listLimits?: { defaultLimit?: number; maxLimit?: number };\n}\n\nexport function generateOpenApiSpec(\n collections: CollectionConfig[],\n options: OpenApiGeneratorOptions = {}\n): Record<string, unknown> {\n const basePath = options.basePath ?? \"/api\";\n const requireAuth = options.requireAuth ?? true;\n const defaultLimit = options.listLimits?.defaultLimit ?? DEFAULT_LIST_LIMIT;\n const maxLimit = options.listLimits?.maxLimit ?? MAX_LIST_LIMIT;\n\n /**\n * The query parameters every list endpoint honours.\n *\n * Written once because it was written twice: the root listing named eight\n * and the subcollection listing named four, though both go through the same\n * `parseQueryOptions` and the same fetch. Four capabilities were therefore\n * unreachable from a generated client on nested routes, and `or`/`and` were\n * undocumented on both.\n */\n const listQueryParameters = () => [\n { name: \"limit\", in: \"query\", schema: { type: \"integer\", default: defaultLimit, minimum: 1, maximum: maxLimit },\n description: `Maximum number of records to return. Must be a whole number between 1 and ${maxLimit}; a larger one is rejected with 400 INVALID_LIMIT rather than trimmed, so a short page always means a short collection. Page past the ceiling with \\`offset\\`.` },\n { name: \"offset\", in: \"query\", schema: { type: \"integer\", default: 0 },\n description: \"Number of records to skip\" },\n { name: \"page\", in: \"query\", schema: { type: \"integer\", minimum: 1 },\n description: \"Page number (alternative to offset). Calculates offset as (page-1)*limit\" },\n {\n name: \"after\",\n in: \"query\",\n schema: { type: \"string\" },\n description:\n \"Keyset cursor: continue after the row the previous page ended on. Pass back \"\n + \"`meta.nextCursor` from that response, unchanged — it is opaque, and encodes both the \"\n + \"sort keys and the last row's values for them. Unlike `offset`, a row inserted or \"\n + \"deleted before the cursor cannot shift the window, so a walk neither repeats nor \"\n + \"skips rows. Cannot be combined with `offset`/`page` (400 CURSOR_WITH_OFFSET), and an \"\n + \"`orderBy` different from the one the cursor was issued under is refused \"\n + \"(400 CURSOR_ORDER_MISMATCH) rather than seeked in an order nobody asked for.\"\n },\n {\n name: \"orderBy\",\n in: \"query\",\n schema: { type: \"string\" },\n description:\n \"Sort field and direction. Accepts `field:asc`, `field:desc`, or `field:desc:last` — \"\n + \"the third segment places NULLs (`first`/`last`), defaulting to Postgres's own \"\n + \"convention (last ascending, first descending). Also accepts a JSON array \"\n + \"`[{\\\"field\\\":\\\"name\\\",\\\"direction\\\":\\\"asc\\\",\\\"nulls\\\":\\\"last\\\"}]` — several entries sort by \"\n + \"each in turn, the second breaking ties on the first.\",\n example: \"created_at:desc:last\"\n },\n {\n name: \"where\",\n in: \"query\",\n schema: { type: \"string\" },\n description: \"JSON object filter, mapping each field to a `[operator, value]` tuple. \"\n + \"Combines with the per-field `?field=op.value` parameters below; on the same field, the per-field parameter wins.\",\n example: \"{\\\"status\\\":[\\\"==\\\",\\\"active\\\"]}\"\n },\n {\n name: \"or\",\n in: \"query\",\n schema: { type: \"string\" },\n description: \"Disjunction of conditions, AND-ed with `where` and `searchString`.\",\n example: \"(status.eq.draft,status.eq.review)\"\n },\n {\n name: \"and\",\n in: \"query\",\n schema: { type: \"string\" },\n description: \"Conjunction of conditions, AND-ed with `where` and `searchString`. Ignored when `or` is also present.\",\n example: \"(views.gte.10,status.eq.draft)\"\n },\n {\n name: \"not\",\n in: \"query\",\n schema: { type: \"string\" },\n description:\n \"Negation, AND-ed with `where` and `searchString`. Negates the **conjunction** of its \"\n + \"conditions: `not(a)` is `NOT a`, `not(a,b)` is `NOT (a AND b)`. Groups nest, so \"\n + \"`not(or(a,b))` is the De Morgan case. Compiles to a real SQL `NOT (...)`, which — \"\n + \"three-valued logic — also excludes rows whose column is NULL. Ignored when `or` or \"\n + \"`and` is also present.\",\n example: \"(status.eq.draft,views.gte.10)\"\n },\n {\n name: \"include\",\n in: \"query\",\n schema: { type: \"string\" },\n description:\n \"Relations to load, in either of two spellings. **Comma-separated names or dotted \"\n + \"paths** — `author,comments.author`, up to 3 hops deep; `*` loads every relation one \"\n + \"hop deep. **JSON**, when a relation needs narrowing — \"\n + \"`{\\\"comments\\\":{\\\"limit\\\":5,\\\"where\\\":{\\\"published\\\":[\\\"==\\\",true]},\"\n + \"\\\"orderBy\\\":\\\"created_at:desc\\\",\\\"fields\\\":\\\"id,body\\\",\\\"include\\\":{\\\"author\\\":true}}}`. \"\n + \"A value starting with `{` is read as the JSON form. A name that is not a relation of \"\n + \"the collection is a 400 UNKNOWN_RELATION, not a silently missing field.\",\n example: \"author,comments.author\"\n },\n {\n name: \"fields\",\n in: \"query\",\n schema: { type: \"string\" },\n description:\n \"Comma-separated columns to return. A projection pushed into the SELECT, so a query \"\n + \"that needs two fields of a wide row reads two columns. The primary key is always \"\n + \"returned (a row that cannot be addressed cannot be updated, deleted, or paged past), \"\n + \"and `excludeFromApi` columns stay hidden whether or not they are named here. An \"\n + \"unknown column is a 400 UNKNOWN_FIELD.\",\n example: \"id,name,created_at\"\n },\n {\n name: \"distinct\",\n in: \"query\",\n schema: { type: \"boolean\" },\n description:\n \"`SELECT DISTINCT` over the returned columns. Only meaningful alongside `fields`: the \"\n + \"primary key is always in the projection, so without narrowing it every row is \"\n + \"already distinct. The response has no `meta.total` — the rows before deduplication \"\n + \"are a different set, so their count would not describe the page — and `meta.hasMore` \"\n + \"is true when the page came back full. Refused (400) alongside \"\n + \"`searchString` or a vector search, which attach a per-row score that makes every row \"\n + \"distinct by construction, and (400 DISTINCT_ORDER_BY_NOT_SELECTED) when `orderBy` \"\n + \"names a column `fields` does not return.\",\n example: \"true\"\n },\n {\n name: \"searchString\",\n in: \"query\",\n schema: { type: \"string\" },\n description:\n \"Text search. By default a case-insensitive substring match OR-ed across the \" +\n \"collection's top-level string properties. A collection declaring a `search` block \" +\n \"gets ranked full-text matching over the fields it names, and rows carry a `_score`.\"\n },\n // Vector search has been served here since vectors landed and was\n // documented nowhere, so the only way to find it was to read the query\n // parser. All four are needed together; `vector_search` and `vector`\n // are ignored unless both are present.\n {\n name: \"vector_search\",\n in: \"query\",\n schema: { type: \"string\" },\n description: \"Name of the `vector` property to run a nearest-neighbour search against. Requires `vector`.\",\n example: \"embedding\"\n },\n {\n name: \"vector\",\n in: \"query\",\n schema: { type: \"string\" },\n description: \"The query embedding, as a JSON array of numbers. Its length must match the property's declared `dimensions`.\",\n example: \"[0.12,-0.04,0.98]\"\n },\n {\n name: \"vector_distance\",\n in: \"query\",\n schema: { type: \"string\", enum: [\"cosine\", \"l2\", \"inner_product\"], default: \"cosine\" },\n description: \"Distance function used for ordering.\"\n },\n {\n name: \"vector_threshold\",\n in: \"query\",\n schema: { type: \"number\" },\n description: \"Drop rows farther than this distance. Rows are returned closest-first with a `_distance` field.\"\n }\n ];\n\n const spec: Record<string, unknown> = {\n openapi: \"3.0.3\",\n info: {\n title: \"Rebase API\",\n version: \"1.0.0\",\n description:\n \"Auto-generated REST API from Rebase collection definitions. \" +\n \"This is the same API consumed by the `@rebasepro/client` SDK.\"\n },\n servers: [\n {\n url: basePath,\n description: \"API Server\"\n }\n ],\n paths: {} as Record<string, unknown>,\n components: {\n schemas: {\n ErrorResponse: {\n type: \"object\",\n properties: {\n error: {\n type: \"object\",\n required: [\"message\", \"code\"],\n properties: {\n message: { type: \"string\" },\n code: { type: \"string\" },\n details: {}\n }\n }\n }\n },\n PaginationMeta: {\n type: \"object\",\n properties: {\n total: { type: \"integer\",\ndescription: \"Total number of matching records. Absent on a `distinct` read, which has no count of the rows it returns.\" },\n limit: { type: \"integer\",\ndescription: \"Page size used for this query\" },\n offset: { type: \"integer\",\ndescription: \"Number of records skipped\" },\n hasMore: { type: \"boolean\",\ndescription: \"Whether more records exist beyond this page\" },\n nextCursor: {\n type: \"string\",\n description:\n \"Opaque keyset cursor continuing this listing — pass it back as `?after=`. \"\n + \"Present when `hasMore` is true and the page returned at least one row; \"\n + \"absent on the last page and on an ordering no cursor can describe \"\n + \"(relevance, whose scores are computed per query and not stored). Do not \"\n + \"parse it: the encoding exists to be changed.\"\n }\n }\n }\n } as Record<string, unknown>,\n securitySchemes: {} as Record<string, unknown>\n },\n tags: [] as Array<{ name: string; description?: string }>\n };\n\n // ── Security Schemes ─────────────────────────────────────────────────\n if (requireAuth) {\n (spec.components as Record<string, unknown>).securitySchemes = {\n bearerAuth: {\n type: \"http\",\n scheme: \"bearer\",\n bearerFormat: \"JWT\",\n description:\n \"JWT access token obtained from `POST /auth/login` or `POST /auth/register`. \" +\n \"Can also be a static service key for server-to-server authentication.\"\n }\n // No `?token=` scheme. It was declared here — globally, so on every\n // operation — and no data route has ever accepted one: both\n // `createAuthMiddleware` and `createAdapterAuthMiddleware` read the\n // `Authorization` header and nothing else, deliberately, because\n // URLs leak into access logs, proxies, Referer headers and browser\n // history (`auth/middleware.ts`). Following it cost a caller twice:\n // unauthenticated, *and* a 400, since `token` is not in the query\n // parser's `reservedQueryKeys` and so compiles as a filter on a\n // column named `token`. `queryTokenAuth` is real but is mounted\n // only on storage file serving, for `<img src>`; if those routes\n // are ever documented, the scheme belongs on them, per-operation.\n };\n (spec as Record<string, unknown>).security = [\n { bearerAuth: [] }\n ];\n }\n\n const paths = spec.paths as Record<string, unknown>;\n const schemas = (spec.components as Record<string, unknown>).schemas as Record<string, unknown>;\n const tags = spec.tags as Array<{ name: string; description?: string }>;\n\n // The names a listing has already spent. A collection is free to have a\n // `limit` or a `fields` column, and the query parser reads those names as\n // pagination and field selection before any filter is compiled — so the\n // per-field filter could never fire, and documenting it a second time put\n // two parameters with the same (`name`, `in`) pair on one operation, which\n // is invalid OpenAPI: Swagger UI renders a duplicate and several generators\n // abort. Taken from the parameter list itself so the two cannot drift.\n //\n // `deleted` too: the parser reads it as the soft-delete parameter on every\n // collection (a value other than `include`/`only` is a 400), so a filter\n // on a column of that name never runs either.\n const reservedParameterNames = new Set([...listQueryParameters().map(p => p.name), DELETED_QUERY_PARAM]);\n\n /**\n * `?deleted=` on the reads of a `softDelete` collection. A delete there\n * moves the row to the trash, and every read hides trashed rows unless it\n * asks for them; the list, the single row, the count and the aggregate all\n * honour this. Empty for any other collection.\n */\n const deletedParameters = (collection: CollectionConfig) => {\n const field = softDeleteFieldOf(collection);\n if (!field) return [];\n return [{\n name: DELETED_QUERY_PARAM,\n in: \"query\",\n required: false,\n schema: { type: \"string\", enum: [\"include\", \"only\"] },\n description:\n `Rows a delete moved to the trash (\\`${field}\\` set) are left out unless this asks for them: ` +\n \"`include` reads them with the live rows, `only` reads them alone. Any other value is a \" +\n \"400 INVALID_DELETED_PARAM.\"\n }];\n };\n\n /**\n * `?hard=` on the deletes of a `softDelete` collection: delete for good\n * rather than move to the trash. Empty for any other collection.\n */\n const hardDeleteParameters = (collection: CollectionConfig) => {\n if (!softDeleteFieldOf(collection)) return [];\n return [{\n name: HARD_DELETE_QUERY_PARAM,\n in: \"query\",\n required: false,\n schema: { type: \"boolean\", default: false },\n description:\n \"`true` deletes for good instead of moving to the trash — a row already in the trash \" +\n \"included, which is how the trash is emptied. Only `true`/`1` and `false`/`0` are \" +\n \"accepted; anything else is a 400 INVALID_HARD_PARAM.\"\n }];\n };\n\n /** What a delete does to a `softDelete` collection's row, for the delete operations' descriptions. */\n const softDeleteBehaviour = (collection: CollectionConfig): string | undefined => {\n const field = softDeleteFieldOf(collection);\n if (!field) return undefined;\n return `This collection soft-deletes: a delete moves the row to the trash by stamping \\`${field}\\`, ` +\n \"and every read leaves it out unless it passes `?deleted=include` or `?deleted=only`. A PATCH \" +\n `setting \\`${field}\\` back to \\`null\\` restores it. Pass \\`?hard=true\\` to delete for good.`;\n };\n\n // Every component name this document will carry, known before the first\n // schema is built: a relation may point at a collection that appears later\n // in the list, or at one that is not documented here at all, and a `$ref`\n // at a component that does not exist is a document Swagger UI renders empty\n // and a strict generator refuses.\n const registeredSchemas = new Set((collections || []).map(schemaNameFor));\n\n /**\n * `Prefer: return=minimal`, on every route that would otherwise send a row\n * back. Documented rather than left implicit because a client generated\n * from this spec cannot send a header the spec does not mention.\n */\n const preferHeader = {\n name: \"Prefer\",\n in: \"header\",\n required: false,\n schema: { type: \"string\", enum: [\"return=minimal\"] },\n description:\n \"`return=minimal` asks the server not to send the written row back. Single writes then \" +\n \"answer `204 No Content`; bulk and batch writes answer `200` carrying the ids only. \" +\n \"The response repeats it in `Preference-Applied` when it was honoured.\"\n };\n\n const ifMatchHeader = {\n name: \"If-Match\",\n in: \"header\",\n required: false,\n schema: { type: \"string\" },\n description:\n \"The `ETag` this edit was made against, from the `GET` that read the row. The write is \" +\n \"refused with `412` if the row has changed since — which is the difference between \" +\n \"\\\"update the row I read\\\" and \\\"overwrite whatever is there now\\\". `*` means only that \" +\n \"the row must exist.\"\n };\n\n const preconditionFailed = {\n 412: {\n description:\n \"The row changed since the ETag in `If-Match` was issued. Nothing was written: \" +\n \"re-read the row, re-apply the change, and send the new ETag\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n }\n };\n\n const minimalResponse = {\n 204: {\n description: \"Written. `Prefer: return=minimal` was honoured, so there is no body\",\n headers: {\n \"Preference-Applied\": { schema: { type: \"string\" }, description: \"`return=minimal`\" }\n }\n }\n };\n\n // ── POST /data/_batch — writes across collections, one transaction ────\n //\n // Registered before the per-collection paths for the same reason the route\n // is: `_batch` is not a collection, and reading it as one would document a\n // table that does not exist.\n if ((collections || []).length > 0) {\n paths[\"/data/_batch\"] = {\n post: {\n tags: [\"Data\"],\n summary: \"Write across collections in one transaction\",\n description:\n \"All-or-nothing across collections — an order and its line items, a user and \" +\n \"their membership row. `/bulk` is one collection at a time, and sending the two \" +\n \"halves as separate requests is exactly the sequence that can half-succeed.\\n\\n\" +\n \"Operations run in order, each through the same pipeline its single-row route \" +\n \"uses: the same validation, callbacks and row-level security, as the same role. \" +\n \"An operation may name itself with `ref`, and a later one may stand \" +\n \"`{ \\\"$ref\\\": \\\"order.id\\\" }` wherever a value goes — in `values`, at any depth, \" +\n \"or as an `id`. Only backward references resolve.\\n\\n\" +\n \"Capped at the same number of entries as a bulk write, because one batch is one \" +\n \"transaction and holds its locks for the whole of it.\",\n operationId: \"batchWrite\",\n parameters: [\n {\n name: \"Idempotency-Key\",\n in: \"header\",\n required: false,\n schema: { type: \"string\" },\n description:\n \"Names this batch so a retry is recognised instead of repeated. Without it a \" +\n \"client that lost the response cannot tell a replay from a second batch, and \" +\n \"the whole batch is written twice.\"\n },\n preferHeader\n ],\n requestBody: {\n required: true,\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n required: [\"operations\"],\n properties: {\n operations: {\n type: \"array\",\n items: { $ref: \"#/components/schemas/BatchOperation\" }\n }\n }\n }\n }\n }\n },\n responses: {\n 200: {\n description:\n \"One entry per operation, in order: the written row for a create, update or \" +\n \"upsert, and `null` for a delete\",\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n properties: {\n data: { type: \"array\", items: { type: \"object\", nullable: true } },\n meta: { type: \"object\", properties: { operations: { type: \"integer\" } } }\n }\n }\n }\n }\n },\n 400: {\n description:\n \"Malformed body, an unknown collection or field, an illegal field operation \" +\n \"or conflict target, a forward `$ref`, or more operations than the limit. \" +\n \"Nothing was written\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n 404: {\n description: \"An `update` or `delete` names a row that does not exist; nothing was written\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n 409: {\n description: \"A request with the same Idempotency-Key is still in flight\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n 422: {\n description: \"The Idempotency-Key was already used for a different request\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n ...errorResponses(requireAuth)\n }\n }\n };\n\n schemas.FieldOperation = {\n type: \"object\",\n description:\n \"A change to the column's current value rather than the value to store. Exactly one \" +\n \"operator per field. Only on an update: an operation over a value that does not exist \" +\n \"yet is refused with a 400.\",\n properties: {\n $inc: { type: \"number\", description: \"Add to a `number` column; negative to subtract.\" },\n $push: { description: \"Append a value, or each of an array of values, to an `array` column.\" },\n $pull: { description: \"Remove every occurrence of a value from an `array` column.\" },\n $merge: { type: \"object\", description: \"Shallow-merge an object into a `map` column.\" }\n }\n };\n\n schemas.BatchOperation = {\n type: \"object\",\n required: [\"op\", \"collection\"],\n properties: {\n op: { type: \"string\", enum: [\"create\", \"update\", \"upsert\", \"delete\"] },\n collection: { type: \"string\", description: \"The collection slug this operation writes to.\" },\n id: {\n description:\n \"Required for `update` and `delete`. May instead be a reference marker — an \" +\n \"object whose single key is `$ref` and whose value is `<ref name>.<field>`, \" +\n \"e.g. `{ \\\"$ref\\\": \\\"order.id\\\" }` — naming a column of the row an earlier \" +\n \"operation wrote. (Described rather than declared as a schema: `$ref` is a \" +\n \"reserved word to every OpenAPI reader, and a property named `$ref` is read \" +\n \"as a reference and mangled.)\",\n oneOf: [{ type: \"string\" }, { type: \"integer\" }, { type: \"object\" }]\n },\n values: {\n type: \"object\",\n description:\n \"The row's fields. Values may be `$ref` markers; an `update` may also carry \" +\n \"field operations.\",\n additionalProperties: true\n },\n onConflict: {\n type: \"array\",\n items: { type: \"string\" },\n description:\n \"`upsert` only: the columns the conflict is matched on. Must carry a declared \" +\n \"uniqueness guarantee. Defaults to the primary key.\"\n },\n ref: {\n type: \"string\",\n description: \"Names this operation's result, so a later one can `$ref` its columns.\"\n }\n }\n };\n }\n\n // ── Collection routes ────────────────────────────────────────────────\n for (const collection of (collections || [])) {\n const schemaName = schemaNameFor(collection);\n const slug = collection.slug;\n\n tags.push({\n name: collection.name,\n description: collection.description || `CRUD operations for ${collection.name}`\n });\n\n // Build component schema for this collection\n schemas[schemaName] = buildCollectionSchema(collection, registeredSchemas);\n\n // Build an \"input\" schema (no read-only/auto fields like autoValue dates)\n schemas[`${schemaName}Input`] = buildCollectionInputSchema(collection);\n\n // The update body — same columns, no `required`. PATCH and PUT both\n // merge, so a field left out means \"unchanged\", not \"omitted by mistake\".\n schemas[`${schemaName}Update`] = buildCollectionUpdateSchema(collection);\n\n const dataPath = `/data/${slug}`;\n const softDeleteNote = softDeleteBehaviour(collection);\n\n // ── GET /data/{slug}/count — How many rows match ──────────────\n //\n // Served for every collection since the route existed and described\n // here for the first time. A spec is what a generated client can see,\n // so an endpoint missing from it is an endpoint that client does not\n // have — and this is the one a paginating UI needs to know how many\n // pages there are.\n //\n // Registered before the list path so its literal segment cannot be read\n // as an `{id}`, which is the same ordering the router uses.\n paths[`${dataPath}/count`] = {\n get: {\n tags: [collection.name],\n summary: `Count ${collection.name}`,\n description:\n \"The number of rows the same filters would return, without returning them. \" +\n \"Takes the filter and search parameters of the list endpoint; `limit`, `offset` \" +\n \"and `orderBy` are not part of the question and are ignored.\",\n operationId: `count${schemaName}`,\n parameters: [\n ...listQueryParameters().filter(p => p.name === \"searchString\"),\n ...deletedParameters(collection),\n ...buildFilterParameters(collection, reservedParameterNames)\n ],\n responses: {\n 200: {\n description: \"The number of matching rows\",\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n required: [\"count\"],\n properties: {\n count: { type: \"integer\", description: \"Rows matching the filters\" }\n }\n }\n }\n }\n },\n ...errorResponses(requireAuth)\n }\n }\n };\n\n // ── GET /data/{slug}/aggregate — count/sum/avg/min/max ────────\n //\n // Before the list path for the same reason `/count` is: a literal\n // segment that a generated client would otherwise be told is an `{id}`.\n paths[`${dataPath}/aggregate`] = {\n get: {\n tags: [collection.name],\n summary: `Aggregate ${collection.name}`,\n description:\n \"Aggregate values over the rows the same filters would return. Takes the filter and \" +\n \"search parameters of the list endpoint. Row-level security applies to the rows \" +\n \"being aggregated, so a caller who can read nothing counts nothing.\",\n operationId: `aggregate${schemaName}`,\n parameters: [\n {\n name: \"select\",\n in: \"query\",\n required: true,\n description:\n \"Comma-separated aggregates, e.g. `count()`, `sum(total)`, `avg(total),max(total)`. \" +\n \"Results are keyed `count`, `sum_total`, `avg_total` and so on.\",\n schema: { type: \"string\" },\n example: \"count(),sum(total)\"\n },\n {\n name: \"groupBy\",\n in: \"query\",\n required: false,\n description: \"Comma-separated fields to group by. Each grouped field is returned alongside the aggregates.\",\n schema: { type: \"string\" },\n example: \"status\"\n },\n // `limit`, `offset` and `page` bound and page the groups,\n // exactly as they do a listing's rows.\n ...listQueryParameters().filter(p =>\n p.name === \"searchString\" || p.name === \"limit\" || p.name === \"offset\" || p.name === \"page\"),\n {\n name: \"orderBy\",\n in: \"query\",\n required: false,\n description:\n \"Sort for the groups, by a `groupBy` field or an aggregate's result key (`count`, \" +\n \"`sum_total`): `key:asc`, `key:desc` or `key:desc:last`. Groups it leaves tied are \" +\n \"ordered by the `groupBy` fields, so every page boundary falls in the same place. \" +\n \"`offset`, `page` and `orderBy` need `groupBy` — without it the result is one row — \" +\n \"and are otherwise a 400 INVALID_AGGREGATE_WINDOW.\",\n schema: { type: \"string\" },\n example: \"count:desc\"\n },\n ...deletedParameters(collection),\n ...buildFilterParameters(collection, reservedParameterNames)\n ],\n responses: {\n 200: {\n description: \"One row per group, or a single row when `groupBy` is absent\",\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n required: [\"data\"],\n properties: {\n data: {\n type: \"array\",\n items: { type: \"object\", additionalProperties: true }\n },\n meta: {\n type: \"object\",\n description: \"The page of groups. Present when `groupBy` is.\",\n properties: {\n limit: { type: \"integer\" },\n offset: { type: \"integer\" },\n hasMore: { type: \"boolean\", description: \"Whether a group follows this page.\" }\n }\n }\n }\n }\n }\n }\n },\n 501: { description: \"This backend's data driver does not implement aggregates\" },\n ...errorResponses(requireAuth)\n }\n }\n };\n\n // ── GET /data/{slug} — List entities ──────────────────────────\n paths[dataPath] = {\n get: {\n tags: [collection.name],\n summary: `List ${collection.name}`,\n operationId: `list${schemaName}`,\n parameters: [\n ...listQueryParameters(),\n ...deletedParameters(collection),\n ...buildFilterParameters(collection, reservedParameterNames)\n ],\n responses: {\n 200: {\n description: \"Paginated list of entities\",\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n properties: {\n data: {\n type: \"array\",\n items: { $ref: `#/components/schemas/${schemaName}` }\n },\n meta: { $ref: \"#/components/schemas/PaginationMeta\" }\n }\n }\n }\n }\n },\n ...errorResponses(requireAuth)\n }\n },\n post: {\n tags: [collection.name],\n summary: `Create ${collection.singularName || collection.name}`,\n operationId: `create${schemaName}`,\n parameters: [\n {\n name: \"on_conflict\",\n in: \"query\",\n required: false,\n schema: { type: \"string\" },\n description:\n \"Comma-separated columns to upsert on, turning the create into \" +\n \"INSERT ... ON CONFLICT DO UPDATE. They must carry a declared uniqueness \" +\n \"guarantee — `validation.unique`, a `unique` index, or the primary key — \" +\n \"or the request is refused with a 400 naming the targets that do exist. \" +\n \"Left off, this is a plain insert and a duplicate key still raises.\",\n example: \"email\"\n },\n preferHeader\n ],\n requestBody: {\n required: true,\n content: {\n \"application/json\": {\n schema: { $ref: `#/components/schemas/${schemaName}Input` }\n }\n }\n },\n responses: {\n 201: {\n description: \"Created entity\",\n content: {\n \"application/json\": {\n schema: { $ref: `#/components/schemas/${schemaName}` }\n }\n }\n },\n ...minimalResponse,\n ...errorResponses(requireAuth)\n }\n }\n };\n\n // ── Bulk: one transaction, all-or-nothing ─────────────────────\n //\n // These went undocumented while they existed, which is the same defect\n // the update verb had: an endpoint the server serves and the spec does\n // not mention cannot be reached by a generated client at all.\n const idempotencyHeader = {\n name: \"Idempotency-Key\",\n in: \"header\",\n required: false,\n schema: { type: \"string\" },\n description:\n \"Names this write so a retry is recognised instead of repeated. Without it a \" +\n \"client that lost the response cannot distinguish a replay from a second \" +\n \"genuine batch, and the whole batch is written twice. A key names one request: \" +\n \"re-send the identical request to replay its answer, and use a new key for a \" +\n \"different one.\"\n };\n\n const bulkErrors = {\n 400: {\n description: \"Malformed body, an unknown field, or more rows than the per-batch limit\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n 409: {\n description:\n \"A request with the same Idempotency-Key is still in flight. Retry it: the \" +\n \"first attempt's result is replayed once it lands\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n 422: {\n description: \"The Idempotency-Key was already used for a different request\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n ...errorResponses(requireAuth)\n };\n\n paths[`/data/${slug}/bulk`] = {\n post: {\n tags: [collection.name],\n summary: `Create many ${collection.name} in one transaction`,\n description:\n \"All-or-nothing: if any row is rejected none of them land, and the error names \" +\n \"the offending index. Every row still runs callbacks, relations and row-level \" +\n \"security. Capped server-side because one batch holds its locks for its whole \" +\n \"duration.\",\n operationId: `createMany${schemaName}`,\n parameters: [idempotencyHeader, preferHeader],\n requestBody: {\n required: true,\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n required: [\"rows\"],\n properties: {\n rows: { type: \"array\", items: { $ref: `#/components/schemas/${schemaName}Input` } },\n upsert: {\n type: \"boolean\",\n description: \"Write each row as INSERT ... ON CONFLICT DO UPDATE.\"\n },\n onConflict: {\n type: \"array\",\n items: { type: \"string\" },\n description:\n \"The columns the conflict is matched on, instead of the primary key. \" +\n \"They must carry a declared uniqueness guarantee. Naming them without \" +\n \"`upsert: true` is a 400 rather than a silently ignored field.\"\n }\n }\n }\n }\n }\n },\n responses: {\n 200: {\n description: \"The written rows, in the order given\",\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n properties: {\n data: { type: \"array\", items: { $ref: `#/components/schemas/${schemaName}` } },\n meta: { type: \"object\", properties: { written: { type: \"integer\" } } }\n }\n }\n }\n }\n },\n ...bulkErrors\n }\n },\n patch: {\n tags: [collection.name],\n summary: `Update many ${collection.name} in one transaction`,\n description:\n \"Each entry names its row and the fields to change. `{ id, data }` rather than \" +\n \"flat rows carrying their own key, because on a table keyed on something other \" +\n \"than `id` a flat row cannot say whether a column is the address or a value to \" +\n \"write. An id matching no row fails the batch.\",\n operationId: `updateMany${schemaName}`,\n parameters: [idempotencyHeader, preferHeader],\n requestBody: {\n required: true,\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n required: [\"updates\"],\n properties: {\n updates: {\n type: \"array\",\n items: {\n type: \"object\",\n required: [\"id\", \"data\"],\n properties: {\n id: { type: \"string\", description: \"The row to update\" },\n data: { $ref: `#/components/schemas/${schemaName}Update` }\n }\n }\n }\n }\n }\n }\n }\n },\n responses: {\n 200: {\n description: \"The updated rows, in the order given\",\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n properties: {\n data: { type: \"array\", items: { $ref: `#/components/schemas/${schemaName}` } },\n meta: { type: \"object\", properties: { written: { type: \"integer\" } } }\n }\n }\n }\n }\n },\n 404: {\n description: \"One of the ids matches no row; nothing was written\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n ...bulkErrors\n }\n }\n };\n\n paths[`/data/${slug}/bulk/delete`] = {\n post: {\n tags: [collection.name],\n summary: `Delete many ${collection.name} in one transaction`,\n description:\n \"A POST, not `DELETE /bulk` with a body. Bodies on DELETE are permitted but \" +\n \"widely dropped by proxies and CDNs, and several generators ignore \" +\n \"`requestBody` on a DELETE operation — a generated client would send the \" +\n \"request with no ids at all. Takes ids rather than a filter: a mistyped \" +\n \"condition that empties a table cannot be reviewed at the call site the way \" +\n \"an explicit list can. `beforeDelete`/`afterDelete` fire per row.\" +\n (softDeleteNote ? `\\n\\n${softDeleteNote}` : \"\"),\n operationId: `deleteMany${schemaName}`,\n parameters: [idempotencyHeader, ...hardDeleteParameters(collection)],\n requestBody: {\n required: true,\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n required: [\"ids\"],\n properties: {\n ids: {\n type: \"array\",\n items: { oneOf: [{ type: \"string\" }, { type: \"integer\" }] }\n }\n }\n }\n }\n }\n },\n responses: {\n 200: {\n description: softDeleteNote\n ? \"How many rows were moved to the trash, or deleted for good with `?hard=true`\"\n : \"How many rows were deleted\",\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n properties: {\n meta: { type: \"object\", properties: { deleted: { type: \"integer\" } } }\n }\n }\n }\n }\n },\n 404: {\n description: \"One of the ids matches no row; nothing was deleted\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n ...bulkErrors\n }\n }\n };\n\n // ── GET/PUT/DELETE /data/{slug}/{id} ──────────────────────────\n const entityPath = `/data/${slug}/{id}`;\n paths[entityPath] = {\n get: {\n tags: [collection.name],\n summary: `Get ${collection.singularName || collection.name} by ID`,\n operationId: `get${schemaName}ById`,\n parameters: [\n { name: \"id\",\nin: \"path\",\nrequired: true,\nschema: { type: \"string\" },\ndescription: \"Entity ID\" },\n // The same two parameters the list route documents, and the\n // same code serves them — one row and a page of them go\n // through one pipeline, so anything true of `include` or\n // `fields` there is true here.\n {\n name: \"include\",\n in: \"query\",\n schema: { type: \"string\" },\n description:\n \"Relations to load: comma-separated names or dotted paths \"\n + \"(`author,comments.author`, up to 3 hops), `*` for all one hop deep, or the \"\n + \"JSON form for per-relation `limit`/`where`/`orderBy`/`fields`. An unknown \"\n + \"name is a 400 UNKNOWN_RELATION.\",\n example: \"author,comments.author\"\n },\n {\n name: \"fields\",\n in: \"query\",\n schema: { type: \"string\" },\n description:\n \"Comma-separated columns to return, as a SELECT projection. The primary key \"\n + \"always survives and `excludeFromApi` columns stay hidden.\",\n example: \"id,title\"\n },\n ...deletedParameters(collection)\n ],\n responses: {\n 200: {\n description: \"Entity found\",\n headers: {\n ETag: {\n schema: { type: \"string\" },\n description:\n \"This row's version. Send it back as `If-Match` on a later PATCH or \" +\n \"DELETE to have the write refused if the row has changed in between.\"\n }\n },\n content: {\n \"application/json\": {\n schema: { $ref: `#/components/schemas/${schemaName}` }\n }\n }\n },\n 404: { description: \"Entity not found\",\ncontent: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } } },\n ...errorResponses(requireAuth)\n }\n },\n patch: updateOperation(collection, schemaName, requireAuth, {\n ifMatchHeader,\n preferHeader,\n preconditionFailed,\n minimalResponse\n }),\n delete: {\n tags: [collection.name],\n summary: `Delete ${collection.singularName || collection.name}`,\n ...(softDeleteNote ? { description: softDeleteNote } : {}),\n operationId: `delete${schemaName}`,\n parameters: [\n { name: \"id\",\nin: \"path\",\nrequired: true,\nschema: { type: \"string\" },\ndescription: \"Entity ID\" },\n ifMatchHeader,\n {\n name: \"Idempotency-Key\",\n in: \"header\",\n required: false,\n schema: { type: \"string\" },\n description:\n \"Names this delete so a retry replays its answer. A delete replayed after \" +\n \"the first attempt committed would otherwise answer 404 — which an offline \" +\n \"queue reads as a permanent failure for a delete that in fact succeeded.\"\n },\n ...hardDeleteParameters(collection)\n ],\n responses: {\n 204: {\n description: softDeleteNote\n ? \"Moved to the trash: the row still exists and can be restored. With `?hard=true`, deleted for good\"\n : \"Deleted successfully\"\n },\n 404: { description: \"Entity not found\",\ncontent: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } } },\n 409: {\n description: \"A request with the same Idempotency-Key is still in flight\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n 422: {\n description: \"The Idempotency-Key was already used for a different request\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n ...preconditionFailed,\n ...errorResponses(requireAuth)\n }\n }\n };\n\n }\n\n // ── Subcollection routes ─────────────────────────────────────────────\n //\n // A second pass, after every collection's component schema exists. These\n // routes `$ref` the *target's* schema, and the first pass builds schemas in\n // array order — so doing this inline meant a subcollection whose target\n // appeared later in the list silently degraded to an untyped `object`.\n //\n // The names come from the *resolved* relations, not from the authored\n // `relations` array. `relationName` is optional at the authoring surface —\n // it defaults to the property key, or to the target's slug — so reading the\n // raw field skipped every relation that relied on the default, and missed\n // relations declared inline on a property entirely, since those are not in\n // the array. These are the same resolved names the nested-path router\n // matches, so the spec and the routes cannot drift apart.\n //\n // A to-one relation is left out. `posts/1/author` resolves, but it\n // addresses a single row, and documenting it as a paginated list would\n // describe a response shape the client never gets.\n for (const collection of (collections || [])) {\n const slug = collection.slug;\n const schemaName = schemaNameFor(collection);\n const relations = Object.values(resolveCollectionRelations(collection))\n .filter(isToMany);\n for (const relation of relations) {\n const relationName = relation.relationName;\n const targetCollection = relation.target();\n const targetSchema = schemaNameFor(targetCollection);\n\n const subPath = `/data/${slug}/{parentId}/${relationName}`;\n\n // Only add if the schema exists (target collection is also registered)\n paths[subPath] = {\n get: {\n tags: [collection.name],\n summary: `List ${relationName} for ${withIndefiniteArticle(collection.singularName || collection.name)}`,\n operationId: `list${schemaName}${toPascalCase(relationName)}`,\n parameters: [\n { name: \"parentId\",\nin: \"path\",\nrequired: true,\nschema: { type: \"string\" },\ndescription: `${collection.singularName || collection.name} ID` },\n // The nested list handler goes through the same\n // `parseQueryOptions` and the same fetch as the root\n // one, so it honours the same parameters. It documented\n // four of them.\n ...listQueryParameters()\n ],\n responses: {\n 200: {\n description: `List of related ${relationName}`,\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n properties: {\n data: {\n type: \"array\",\n items: schemas[targetSchema]\n ? { $ref: `#/components/schemas/${targetSchema}` }\n : { type: \"object\" }\n },\n meta: { $ref: \"#/components/schemas/PaginationMeta\" }\n }\n }\n }\n }\n },\n ...errorResponses(requireAuth)\n }\n }\n };\n }\n }\n\n return spec;\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────────\n\n/**\n * Is this property part of the shape the document may describe?\n *\n * One exclusion, and it is the document telling the truth about what the server\n * does: `excludeFromApi` is a server-side guarantee that the column \"is\n * stripped from every row the API serves, for every caller, including admins\n * and service keys\" — `stripExcluded` in the row pipeline enforces it. A schema\n * that lists such a column describes a field that is never present, and it\n * describes it to everyone: `/docs` is mounted on the app, not on the data\n * router, so it carries none of the auth middleware `{basePath}/data` does.\n * Every project scaffolded by `rebase init` published its `users` collection's\n * `passwordHash` and `emailVerificationToken` this way.\n *\n * A `relation` property used to be excluded here too, on the reasoning that the\n * property is virtual. It is — but the row is not empty where it stands: the\n * owning side carries a foreign key under a *wire* name (`authorId`), and a\n * read that includes the relation carries the target's row under the relation's\n * own name. Excluding both left `/api/docs` describing a strictly smaller\n * collection than `generated/sdk/database.types.ts` did for the same config.\n * Relations are now emitted by {@link emitRelationProperties}, under the same\n * keys and in the same order as the SDK's `Row`; the direct-property loops skip\n * them so the two passes cannot emit one key twice.\n *\n * Written as one predicate rather than a `continue` per loop because it kept\n * being fixed in one loop at a time: this is the same rule the SDK generator\n * applies to its `Row` type (`packages/codegen/src/generate-types.ts`).\n */\nfunction isDocumentedProperty(property: Property, direction: \"read\" | \"write\" = \"read\"): boolean {\n const access = effectiveAccess(property);\n return access?.[direction]?.length !== 0;\n}\n\n/**\n * The `x-rebase-access` annotation, or nothing for a field with no rule.\n *\n * A vendor extension rather than a schema keyword because OpenAPI has no way to\n * say \"this property is present for some callers\" — `readOnly` is about the\n * direction of a field, not about who. Generators ignore what they do not know,\n * so a client built from this spec still compiles; a *human* reading `/docs`, or\n * a gateway that wants to enforce the same rule at the edge, gets the role lists\n * verbatim. Emitted for the role case only: a field closed to everybody is\n * absent from the document entirely, which is a stronger statement.\n */\nfunction accessAnnotation(property: Property): Record<string, unknown> | undefined {\n const access = effectiveAccess(property);\n if (!access) return undefined;\n const annotation: Record<string, unknown> = {};\n if (access.read !== undefined) annotation.read = [...access.read];\n if (access.write !== undefined) annotation.write = [...access.write];\n return Object.keys(annotation).length > 0 ? annotation : undefined;\n}\n\n/** The sentence `x-rebase-access` deserves in prose, for a reader of `/docs`. */\nfunction accessDescription(property: Property): string | undefined {\n const access = effectiveAccess(property);\n if (!access) return undefined;\n const parts: string[] = [];\n const phrase = (roles: readonly string[]) =>\n roles.length === 0 ? \"nobody through the API\" : roles.map(r => `\\`${r}\\``).join(\", \") + \" (and `admin`)\";\n if (access.read !== undefined) parts.push(`readable by ${phrase(access.read)}`);\n if (access.write !== undefined) parts.push(`writable by ${phrase(access.write)}`);\n if (parts.length === 0) return undefined;\n return `Field access: ${parts.join(\"; \")}. A caller without the role does not receive the field at all — it is absent, not null.`;\n}\n\n/**\n * The keys `stripExcluded` deletes: the property name *and* its column name.\n *\n * Seeding the emitted set with both is what stops a foreign key derived from a\n * relation putting an `excludeFromApi` column back on the surface under its\n * other name. Same pair, same reason, as `excludedApiKeys` in the SDK\n * generator.\n */\nfunction excludedApiKeys(collection: CollectionConfig, direction: \"read\" | \"write\" = \"read\"): Set<string> {\n const excluded = new Set<string>();\n for (const [key, property] of Object.entries(collection.properties ?? {})) {\n if (isDocumentedProperty(property as Property, direction)) continue;\n excluded.add(key);\n const columnName = (property as { columnName?: unknown }).columnName;\n if (typeof columnName === \"string\") excluded.add(columnName);\n }\n return excluded;\n}\n\n/**\n * The collection's primary key, as the key it is addressed by on the wire and\n * the property that declares it.\n *\n * `id` was a literal in three places — seeded before the read loop, assigned\n * after the input loop, inherited by the update schema from the input one — and\n * the two spellings disagreed: a declared `id: { type: \"number\" }` overwrote\n * the read seed and was overwritten by the input assignment, so the same field\n * was `integer` in `Post` and `string` in `PostInput`. One helper now answers\n * the question for all three.\n */\nfunction idPropertyEntry(collection: CollectionConfig | undefined): [string, Property] | undefined {\n for (const [key, property] of Object.entries(collection?.properties ?? {})) {\n if ((property as Record<string, unknown>)?.isId) return [key, property as Property];\n }\n return undefined;\n}\n\n/**\n * The schema of a primary key or of a foreign key pointing at one: the declared\n * property's own type, stripped of the field-level facts (description,\n * validation bounds) that belong to the column and not to a reference to it.\n *\n * Falls back to `string` for a collection that declares no primary key, which\n * is what every schema here assumed unconditionally before.\n */\nfunction idSchemaFor(collection: CollectionConfig | undefined): Record<string, unknown> {\n const declared = idPropertyEntry(collection);\n if (!declared) return { type: \"string\" };\n const converted = convertPropertyToSchema(declared[1]);\n const schema: Record<string, unknown> = { type: converted.type ?? \"string\" };\n if (converted.format) schema.format = converted.format;\n return schema;\n}\n\n/**\n * Relations resolve or they do not; a document is still owed for a collection\n * whose target thunk throws (a circular import, usually). The SDK generator\n * warns and carries on with no relation fields, and so does this — the\n * alternative is `/api/docs` 500ing for the whole project.\n */\nfunction resolveRelationsForDocument(collection: CollectionConfig): Record<string, ResolvedRelation> {\n try {\n return resolveCollectionRelations(collection);\n } catch {\n return {};\n }\n}\n\n/** Unwrap a target handed back as a module namespace — `() => import(\"./authors\")`. */\nfunction relationTarget(relation: ResolvedRelation): CollectionConfig | undefined {\n try {\n let target = relation.target() as CollectionConfig & { default?: CollectionConfig; __esModule?: boolean };\n if (target && (target.default || target.__esModule)) {\n target = (target.default ?? target) as typeof target;\n }\n return target;\n } catch {\n return undefined;\n }\n}\n\n/**\n * The schema an *included* relation arrives as: the target's own row.\n *\n * `$ref` when the target is one of the collections this document describes, and\n * an open object when it is not — a dangling pointer makes Swagger UI render an\n * empty model and makes a strict generator abort, which is worse than a vague\n * one.\n */\nfunction includedRelationSchema(\n relation: ResolvedRelation,\n registeredSchemas: ReadonlySet<string>\n): Record<string, unknown> {\n const target = relationTarget(relation);\n const targetSchema = target ? schemaNameFor(target) : undefined;\n const item: Record<string, unknown> = targetSchema && registeredSchemas.has(targetSchema)\n ? { $ref: `#/components/schemas/${targetSchema}` }\n : { type: \"object\" };\n return relation.cardinality === \"many\" ? { type: \"array\", items: item } : item;\n}\n\n/**\n * Emit a collection's relations onto a read schema: the foreign keys first,\n * then the relations themselves.\n *\n * The order and the keys are the SDK `Row`'s, deliberately — the parity test\n * next door compares the two key sets, and the only way that stays true is for\n * both to be derived the same way rather than kept in step by hand.\n *\n * A `belongsTo` reaches the wire under the *field* name of its local column\n * (`author_id` → `authorId`), which is what `fieldKeyForColumn` answers; a\n * relation addressed by the same name as its own foreign key is served as\n * either the scalar or the nested row depending on `include`, so it is\n * documented as both.\n */\nfunction emitRelationProperties(\n collection: CollectionConfig,\n properties: Record<string, unknown>,\n required: string[],\n emitted: Set<string>,\n registeredSchemas: ReadonlySet<string>\n): void {\n const resolved = resolveRelationsForDocument(collection);\n\n for (const [relationKey, relation] of Object.entries(resolved)) {\n if (relation.kind !== \"belongsTo\" || !relation.localKey) continue;\n const fieldKey = fieldKeyForColumn(collection, relation.localKey);\n if (emitted.has(fieldKey)) continue;\n\n const foreignKey = idSchemaFor(relationTarget(relation));\n const shadowedByInclude = relationKey === fieldKey;\n properties[fieldKey] = shadowedByInclude\n ? { oneOf: [foreignKey, includedRelationSchema(relation, registeredSchemas)] }\n : { ...foreignKey, description: `Foreign key into \\`${relation.targetSlug}\\`` };\n emitted.add(fieldKey);\n\n if (isRelationRequired(collection, relation) && !shadowedByInclude) required.push(fieldKey);\n }\n\n for (const [key, relation] of Object.entries(resolved)) {\n if (emitted.has(key)) continue;\n properties[key] = includedRelationSchema(relation, registeredSchemas);\n emitted.add(key);\n }\n\n // A `relation` property whose relation did not resolve. Still a field of the\n // row, just not a precisely describable one.\n for (const [key, property] of Object.entries(collection.properties ?? {})) {\n if ((property as Property)?.type !== \"relation\") continue;\n if (emitted.has(key)) continue;\n properties[key] = { type: \"object\" };\n emitted.add(key);\n }\n}\n\n/**\n * Build the component schema for a collection (output / read shape).\n *\n * Every declared property except the ones {@link isDocumentedProperty} rules\n * out, plus the foreign keys and relations {@link emitRelationProperties} adds.\n */\nexport function buildCollectionSchema(\n collection: CollectionConfig,\n registeredSchemas: ReadonlySet<string>\n): Record<string, unknown> {\n const idKey = idPropertyEntry(collection)?.[0] ?? \"id\";\n const properties: Record<string, unknown> = {\n [idKey]: { ...idSchemaFor(collection), description: \"Unique identifier\" }\n };\n const required: string[] = [idKey];\n const excluded = excludedApiKeys(collection);\n const emitted = new Set<string>(excluded);\n emitted.add(idKey);\n\n for (const [key, property] of Object.entries(collection.properties)) {\n if (property.type === \"relation\") continue;\n if (!isDocumentedProperty(property)) continue;\n\n properties[key] = convertPropertyToSchema(property);\n emitted.add(key);\n\n if (property.validation?.required && key !== idKey) {\n required.push(key);\n }\n }\n\n emitRelationProperties(collection, properties, required, emitted, registeredSchemas);\n annotateTenantField(collection, properties, \"read\");\n\n return {\n type: \"object\",\n required: required.length > 0 ? required : undefined,\n properties\n };\n}\n\n/**\n * Mark the tenant field, on whichever schema is being built.\n *\n * A vendor extension for the same reason `x-rebase-access` is one: OpenAPI has\n * no keyword for \"the server fills this in from who you are, and refuses a\n * value that is not yours\". `readOnly` is the closest and it is wrong — the\n * field *is* writable, by a caller sending their own tenant, and a bypass role\n * may send any. Generators ignore what they do not know, so a client built from\n * this document still compiles; a human reading `/docs`, or a gateway wanting\n * to enforce the same boundary at the edge, learns the field is special and\n * why.\n *\n * Applied after the property loops rather than inside `convertPropertyToSchema`\n * because tenancy is a fact about the *collection*, and that function is handed\n * a property with no idea which collection it came from.\n */\nfunction annotateTenantField(\n collection: CollectionConfig,\n properties: Record<string, unknown>,\n direction: \"read\" | \"write\"\n): void {\n const tenant = getTenantConfig(collection);\n if (!tenant) return;\n const schema = properties[tenant.field];\n if (!schema || typeof schema !== \"object\") return;\n\n const existing = (schema as { description?: unknown }).description;\n const sentence = direction === \"write\"\n ? \"The tenant this row belongs to. Omit it and the server stamps the tenant you are calling as; \" +\n \"send another tenant's and the write is refused with `TENANT_MISMATCH`. It cannot be changed \" +\n \"on an update (`TENANT_IMMUTABLE`).\"\n : \"The tenant this row belongs to. Rows of other tenants are not returned at all.\";\n\n properties[tenant.field] = {\n ...(schema as Record<string, unknown>),\n description: typeof existing === \"string\" && existing ? `${existing} — ${sentence}` : sentence,\n \"x-rebase-tenant\": true\n };\n}\n\n/**\n * The PATCH/PUT operation for `/data/{slug}/{id}`.\n *\n * Split out because both verbs serve it and they must not drift: the update\n * body is a **partial**, and describing it with the create schema was the bug\n * this replaces. `<Name>Input` marks every `validation.required` property as\n * required — correct for POST, wrong for an update, where omitting a field\n * means \"leave it alone\" rather than \"I forgot it\". A client generated from\n * that spec demanded fields the server does not, and a spec-validating gateway\n * would have rejected partial updates the server accepts.\n */\nfunction updateOperation(\n collection: CollectionConfig,\n schemaName: string,\n requireAuth: boolean,\n shared: {\n ifMatchHeader: Record<string, unknown>;\n preferHeader: Record<string, unknown>;\n preconditionFailed: Record<string, unknown>;\n minimalResponse: Record<string, unknown>;\n }\n): Record<string, unknown> {\n return {\n tags: [collection.name],\n summary: `Update ${collection.singularName || collection.name}`,\n description:\n \"Partial update: only the properties present in the body are written; the rest are left \" +\n \"unchanged.\\n\\n\" +\n \"A property's value may instead be a field operation — `{ \\\"views\\\": { \\\"$inc\\\": 1 } }`, \" +\n \"`{ \\\"tags\\\": { \\\"$push\\\": \\\"new\\\" } }`, `{ \\\"tags\\\": { \\\"$pull\\\": \\\"old\\\" } }`, \" +\n \"`{ \\\"meta\\\": { \\\"$merge\\\": { \\\"seen\\\": true } } }` — which is applied inside the \" +\n \"statement holding the row lock. That is the difference between a counter that is correct \" +\n \"under concurrency and one that silently loses increments, because expressing the same \" +\n \"change as a value means reading it first. `$inc` needs a `number` property, `$push`/`$pull` \" +\n \"an `array`, `$merge` a `map`; anything else is a 400. Pushed elements and merged keys answer \" +\n \"to the property's own rules, as a value would. See the `FieldOperation` schema.\",\n operationId: `update${schemaName}`,\n parameters: [\n { name: \"id\", in: \"path\", required: true, schema: { type: \"string\" }, description: \"Entity ID\" },\n shared.ifMatchHeader,\n shared.preferHeader,\n {\n name: \"Idempotency-Key\",\n in: \"header\",\n required: false,\n schema: { type: \"string\" },\n description:\n \"Names this update so a retry replays its answer instead of applying the edit \" +\n \"again. A PATCH is not naturally idempotent — a field operation emphatically is \" +\n \"not — so a retry after a lost response applies it twice.\"\n }\n ],\n requestBody: {\n required: true,\n content: {\n \"application/json\": {\n schema: { $ref: `#/components/schemas/${schemaName}Update` }\n }\n }\n },\n responses: {\n 200: {\n description: \"Updated entity\",\n content: {\n \"application/json\": {\n schema: { $ref: `#/components/schemas/${schemaName}` }\n }\n }\n },\n 404: {\n description: \"Entity not found\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n 409: {\n description: \"A request with the same Idempotency-Key is still in flight\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n 422: {\n description: \"The Idempotency-Key was already used for a different request\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n ...shared.preconditionFailed,\n ...shared.minimalResponse,\n ...errorResponses(requireAuth)\n }\n };\n}\n\n/**\n * The update body: the create schema with `required` dropped.\n *\n * Derived rather than rebuilt so the two cannot describe different columns —\n * the only difference between creating and updating is which fields you must\n * supply, and that is exactly the one thing removed here.\n */\nfunction buildCollectionUpdateSchema(collection: CollectionConfig): Record<string, unknown> {\n const { required: _required, ...rest } = buildCollectionInputSchema(collection);\n return rest;\n}\n\n/**\n * Build an input schema (for POST/PUT) — excludes auto-generated fields.\n *\n * `excludeFromApi` columns are left out too, and the server now agrees: a write\n * naming one is refused (`write-validation.ts`), which is what the flag's name\n * and the generated SDK's own documentation always said. This comment used to\n * record that such a write was \"still accepted\" — the document was right and\n * the server was the thing that had not caught up.\n */\nexport function buildCollectionInputSchema(collection: CollectionConfig): Record<string, unknown> {\n const properties: Record<string, unknown> = {};\n const required: string[] = [];\n // The write half, not the read half: a field readable by nobody but\n // writable by an `admin` belongs in the input body and not in the row, and\n // the two schemas used to share one exclusion set and so got both wrong.\n const excluded = excludedApiKeys(collection, \"write\");\n const emitted = new Set<string>(excluded);\n const idKey = idPropertyEntry(collection)?.[0] ?? \"id\";\n\n for (const [key, property] of Object.entries(collection.properties)) {\n if (property.type === \"relation\") continue;\n if (!isDocumentedProperty(property, \"write\")) continue;\n\n // Skip auto-value date fields from the input schema\n if (property.type === \"date\" && property.autoValue) continue;\n\n // Skip auto-generated ID fields\n if (\"isId\" in property && property.isId && property.isId !== \"manual\" && property.isId !== true) continue;\n\n properties[key] = convertPropertyToSchema(property);\n emitted.add(key);\n\n if (property.validation?.required) {\n required.push(key);\n }\n }\n\n // Allow explicit ID for create (optional). Typed from the declared primary\n // key rather than as a `string` literal: a serial `id` was `integer` on the\n // read schema and `string` here, for the same column, in every document the\n // generator has ever produced.\n if (!emitted.has(idKey)) {\n properties[idKey] = {\n ...idSchemaFor(collection),\n description: \"Optional: client-assigned ID. If omitted, the server generates one.\"\n };\n emitted.add(idKey);\n }\n\n // The two ways a write may name a `belongsTo` target, both of which the\n // server accepts and the generated SDK's `Insert` already offered: the\n // foreign key under its own wire name (`authorId`), and the relation\n // property (`author`), which the write transformer maps onto that column.\n // Neither reached the document, so the spec described a create that could\n // not set a relation at all.\n emitWritableRelations(collection, properties, emitted);\n annotateTenantField(collection, properties, \"write\");\n\n // The tenant field is never required on input, whatever the property\n // declares: the column is NOT NULL, and the server is what fills it.\n // Listing it would tell every generated client to demand a value its caller\n // is not supposed to compute.\n const tenantField = getTenantConfig(collection)?.field;\n const requiredOnInput = tenantField ? required.filter(key => key !== tenantField) : required;\n\n return {\n type: \"object\",\n required: requiredOnInput.length > 0 ? requiredOnInput : undefined,\n properties\n };\n}\n\n/**\n * The writable half of a collection's relations: `belongsTo` only.\n *\n * A to-many is not writable through the body — the server links rows through\n * the nested routes — so offering `tags: [...]` on a create would describe a\n * write that does nothing. Same rule, same reason, as `emitWritableRelations`\n * in the SDK generator, whose `Insert` type this mirrors key for key.\n *\n * Neither spelling is listed in `required`, even for a relation the collection\n * declares `validation: { required: true }` on: the two keys are alternatives,\n * and a schema naming both would tell a spec-validating gateway to reject a\n * create that the server accepts. (The SDK's `Insert` marks both non-optional\n * for the same relation, which is the same fact stated less carefully; a\n * document is the half that a gateway enforces.)\n */\nfunction emitWritableRelations(\n collection: CollectionConfig,\n properties: Record<string, unknown>,\n emitted: Set<string>\n): void {\n const resolved = resolveRelationsForDocument(collection);\n\n const emit = (key: string, relation: ResolvedRelation): void => {\n if (emitted.has(key)) return;\n properties[key] = {\n ...idSchemaFor(relationTarget(relation)),\n description: `The \\`${relation.targetSlug}\\` row this belongs to.`\n };\n emitted.add(key);\n };\n\n for (const relation of Object.values(resolved)) {\n if (relation.kind === \"belongsTo\" && relation.localKey) {\n emit(fieldKeyForColumn(collection, relation.localKey), relation);\n }\n }\n\n for (const [key, property] of Object.entries(collection.properties ?? {})) {\n if ((property as Property)?.type !== \"relation\") continue;\n const relation = findRelation(resolved, key);\n if (relation?.kind === \"belongsTo\" && relation.localKey) emit(key, relation);\n }\n}\n\n/**\n * Convert a Rebase Property to an OpenAPI 3.0 schema object.\n */\nfunction convertPropertyToSchema(property: Property): Record<string, unknown> {\n const schema = convertPropertyTypeToSchema(property);\n const annotation = accessAnnotation(property);\n if (!annotation) return schema;\n\n const sentence = accessDescription(property);\n return {\n ...schema,\n ...(sentence\n ? { description: schema.description ? `${schema.description} — ${sentence}` : sentence }\n : {}),\n \"x-rebase-access\": annotation\n };\n}\n\n/** The JSON Schema a property's *type* compiles to, before any access annotation. */\nfunction convertPropertyTypeToSchema(property: Property): Record<string, unknown> {\n const base: Record<string, unknown> = {};\n\n if (property.name) {\n base.description = property.name;\n }\n\n switch (property.type) {\n case \"string\": {\n const sp = property as StringProperty;\n base.type = \"string\";\n\n if (sp.enum) {\n const enumValues = resolveEnumValues(sp.enum);\n if (enumValues.length > 0) {\n base.enum = enumValues;\n }\n }\n\n if (sp.validation) {\n if (sp.validation.min !== undefined) base.minLength = sp.validation.min;\n if (sp.validation.max !== undefined) base.maxLength = sp.validation.max;\n if (sp.validation.length !== undefined) {\n base.minLength = sp.validation.length;\n base.maxLength = sp.validation.length;\n }\n if (sp.validation.matches !== undefined) {\n base.pattern = String(sp.validation.matches);\n }\n }\n\n if (sp.email) base.format = \"email\";\n if (sp.url) base.format = \"uri\";\n if (sp.storage) base.format = \"uri\";\n\n return base;\n }\n\n case \"number\": {\n const np = property as NumberProperty;\n // `isId` is on this list because the DDL generator puts it there:\n // a numeric primary key is `INTEGER GENERATED BY DEFAULT AS\n // IDENTITY` for `\"increment\"` and `INTEGER` for every other form\n // (`generate-postgres-ddl-logic.ts`), so the column the scaffold's\n // `posts.id` creates is an integer and the document called it a\n // `number` — which lets a generated client send `1.5` for a row id.\n const isInteger = np.validation?.integer\n || Boolean(np.isId)\n || np.columnType === \"integer\" || np.columnType === \"serial\"\n || np.columnType === \"bigserial\" || np.columnType === \"bigint\";\n base.type = isInteger ? \"integer\" : \"number\";\n\n if (np.enum) {\n const enumValues = resolveEnumValues(np.enum);\n if (enumValues.length > 0) {\n base.enum = enumValues;\n }\n }\n\n if (np.validation) {\n if (np.validation.min !== undefined) base.minimum = np.validation.min;\n if (np.validation.max !== undefined) base.maximum = np.validation.max;\n if (np.validation.moreThan !== undefined) {\n base.minimum = np.validation.moreThan;\n base.exclusiveMinimum = true;\n }\n if (np.validation.lessThan !== undefined) {\n base.maximum = np.validation.lessThan;\n base.exclusiveMaximum = true;\n }\n }\n\n return base;\n }\n\n case \"boolean\":\n base.type = \"boolean\";\n return base;\n\n case \"date\": {\n base.type = \"string\";\n if (property.mode === \"date\") {\n base.format = \"date\";\n } else {\n base.format = \"date-time\";\n }\n if (property.autoValue) {\n base.readOnly = true;\n base.description = (base.description || \"\") +\n (property.autoValue === \"on_create\" ? \" (Auto-set on creation)\" : \" (Auto-updated)\");\n }\n return base;\n }\n\n case \"geopoint\":\n base.type = \"object\";\n base.properties = {\n latitude: { type: \"number\" },\n longitude: { type: \"number\" }\n };\n base.required = [\"latitude\", \"longitude\"];\n return base;\n\n case \"reference\":\n base.type = \"string\";\n base.description = (base.description || \"\") + \" (Reference ID)\";\n return base;\n\n case \"array\": {\n const ap = property as ArrayProperty;\n base.type = \"array\";\n\n if (ap.oneOf) {\n // Discriminated union (e.g., content blocks)\n const typeField = ap.oneOf.typeField || \"type\";\n const valueField = ap.oneOf.valueField || \"value\";\n const variants: Record<string, unknown>[] = [];\n\n for (const [variantKey, variantProp] of Object.entries(ap.oneOf.properties)) {\n variants.push({\n type: \"object\",\n properties: {\n [typeField]: { type: \"string\",\nenum: [variantKey] },\n [valueField]: convertPropertyToSchema(variantProp)\n },\n required: [typeField, valueField]\n });\n }\n\n base.items = { oneOf: variants };\n } else if (ap.of) {\n if (Array.isArray(ap.of)) {\n base.items = { oneOf: ap.of.map(p => convertPropertyToSchema(p)) };\n } else {\n base.items = convertPropertyToSchema(ap.of);\n }\n } else {\n base.items = {};\n }\n\n if (ap.validation) {\n if (ap.validation.min !== undefined) base.minItems = ap.validation.min;\n if (ap.validation.max !== undefined) base.maxItems = ap.validation.max;\n }\n\n return base;\n }\n\n case \"map\": {\n const mp = property as MapProperty;\n base.type = \"object\";\n\n if (mp.properties) {\n const props: Record<string, unknown> = {};\n const req: string[] = [];\n\n for (const [key, subProp] of Object.entries(mp.properties)) {\n props[key] = convertPropertyToSchema(subProp);\n if (subProp.validation?.required) {\n req.push(key);\n }\n }\n\n base.properties = props;\n if (req.length > 0) base.required = req;\n } else if (mp.keyValue) {\n base.additionalProperties = true;\n }\n\n return base;\n }\n\n case \"vector\": {\n const vp = property as VectorProperty;\n base.type = \"array\";\n base.items = { type: \"number\" };\n base.description = (base.description || \"\") + ` (Vector(${vp.dimensions}))`;\n return base;\n }\n case \"binary\": {\n base.type = \"string\";\n base.description = (base.description || \"\") + \" (Binary/Base64)\";\n return base;\n }\n default:\n base.type = \"string\";\n return base;\n }\n}\n\n/**\n * Resolve EnumValues (array or record) into a flat array of enum values.\n */\nfunction resolveEnumValues(enumDef: Record<string | number, unknown> | Array<{ id: string | number }>): Array<string | number> {\n if (Array.isArray(enumDef)) {\n return enumDef.map(e => (typeof e === \"object\" && e !== null && \"id\" in e) ? e.id : e as string | number);\n }\n return Object.keys(enumDef).map(k => {\n // Preserve numeric keys as numbers\n const num = Number(k);\n return isNaN(num) ? k : num;\n });\n}\n\n/**\n * Build PostgREST-style filter parameters for a collection.\n * These are additional query parameters like `?status=eq.active&price=gte.100`.\n *\n * `excludeFromApi` columns are not offered: the server does filter on them, and\n * that is exactly the problem — a filter on a column no response can contain\n * answers questions about the value one row at a time, which is a worse\n * disclosure than the column name alone.\n */\nfunction buildFilterParameters(\n collection: CollectionConfig,\n reservedNames: ReadonlySet<string> = new Set()\n): Array<Record<string, unknown>> {\n const params: Array<Record<string, unknown>> = [];\n\n for (const [key, property] of Object.entries(collection.properties)) {\n if (!isDocumentedProperty(property)) continue;\n // A `relation` property is not a column, so there is nothing to compare\n // against. The foreign key beside it is filterable and is still not\n // offered here — a separate gap from the schema one, and one the query\n // layer has to answer first.\n if (property.type === \"relation\") continue;\n if (property.type === \"map\" || property.type === \"array\" || property.type === \"geopoint\") {\n continue;\n }\n // A column whose name a list parameter already owns is unfilterable\n // over the wire — see `reservedParameterNames`.\n if (reservedNames.has(key)) continue;\n\n params.push({\n name: key,\n in: \"query\",\n required: false,\n schema: { type: \"string\" },\n description:\n `Filter by \\`${key}\\`. Supports PostgREST operators: ` +\n \"`eq.value`, `neq.value`, `gt.value`, `gte.value`, `lt.value`, `lte.value`, \" +\n \"`in.(a,b,c)`, `nin.(a,b,c)`, `cs.value` (array-contains), `csa.(a,b)` (array-contains-any). \" +\n \"Plain values imply equality.\",\n example: property.type === \"string\" ? \"eq.active\" : property.type === \"number\" ? \"gte.100\" : undefined\n });\n }\n\n return params;\n}\n\n/**\n * Standard error responses included on every endpoint.\n */\nfunction errorResponses(requireAuth: boolean): Record<string, unknown> {\n const responses: Record<string, unknown> = {\n 400: {\n description: \"Bad request\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n 500: {\n description: \"Internal server error\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n }\n };\n\n if (requireAuth) {\n responses[401] = {\n description: \"Authentication required or invalid token\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n };\n responses[403] = {\n description: \"Insufficient permissions\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n };\n }\n\n return responses;\n}\n\n/**\n * Prefix a noun with \"a\" or \"an\" based on its leading sound.\n */\nfunction withIndefiniteArticle(noun: string): string {\n return `${/^[aeiou]/i.test(noun) ? \"an\" : \"a\"} ${noun}`;\n}\n\n/**\n * The component-schema name for a collection — and the stem of every\n * `operationId` and `$ref` that mentions it.\n *\n * `toPascalCase` keeps ASCII letters and digits and nothing else, so a name\n * written in a script that has none of them — a Cyrillic or Japanese\n * `singularName`, which the docs' six locales make ordinary rather than exotic\n * — reduced to the empty string. The schema was then stored under `\"\"` and\n * every reference to it read `#/components/schemas/`, an unresolvable pointer:\n * Swagger UI renders the model empty and a strict generator fails outright. So\n * fall through the names until one survives, and keep a constant as the floor.\n *\n * Two collections whose names PascalCase identically still share one component;\n * that needs a disambiguation rule, not a fallback.\n */\nfunction schemaNameFor(collection: CollectionConfig): string {\n return toPascalCase(collection.singularName || \"\")\n || toPascalCase(collection.name || \"\")\n || toPascalCase(collection.slug || \"\")\n || \"Collection\";\n}\n\n/**\n * Convert a string to PascalCase for schema names.\n */\nfunction toPascalCase(str: string): string {\n return str\n .replace(/[^a-zA-Z0-9]+/g, \" \")\n .split(\" \")\n .filter(Boolean)\n .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(\"\");\n}\n"],"mappings":"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"}
@@ -3,7 +3,7 @@ import __rebaseProcess from "process";
3
3
  globalThis.process ??= __rebaseProcess;
4
4
  __rebaseCreateRequire(import.meta.url);
5
5
  import { n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
6
- import { r as logger } from "./logger-DO2PZc4i.js";
6
+ import { r as logger } from "./logger-D-S-hO5e.js";
7
7
  import { Hono } from "hono";
8
8
  import { getConnInfo } from "@hono/node-server/conninfo";
9
9
  //#region src/functions/proxy.ts
@@ -139,4 +139,4 @@ function socketAddress(c) {
139
139
  //#endregion
140
140
  export { proxy_exports as t };
141
141
 
142
- //# sourceMappingURL=proxy-Czngl3p9.js.map
142
+ //# sourceMappingURL=proxy-qRlqeUmO.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"proxy-Czngl3p9.js","names":[],"sources":["../src/functions/proxy.ts"],"sourcesContent":["/**\n * Forwarding `/api/functions/*` to the process that serves it.\n *\n * A split deployment normally puts a reverse proxy in front and routes the path\n * there. This exists so it does not have to: set `REBASE_FUNCTIONS_UPSTREAM` on\n * the API process and the two-container topology presents the identical URL\n * surface as the one-container one, with nothing new to install and nothing for\n * a client or an SDK to know about.\n *\n * It is a transport hop, not a trust boundary. Nothing is authenticated or\n * authorised here: the upstream runs the same auth middleware against the same\n * `JWT_SECRET` and must see the original `Authorization` header, so re-deciding\n * anything on the way through would put two answers where the system has one.\n */\nimport { Hono, type Context } from \"hono\";\nimport { getConnInfo } from \"@hono/node-server/conninfo\";\nimport type { HonoEnv } from \"../api/types\";\nimport { logger } from \"../utils/logger\";\n\n/**\n * Headers that describe *this* connection rather than the request, and so must\n * not be copied onto a new one.\n *\n * `host` is the important one: forwarded unchanged it names the API process, and\n * an upstream doing any virtual-host routing would answer for the wrong service.\n * `content-length` and `transfer-encoding` are recomputed by `fetch` from the\n * body it is actually given, and a stale value is a hang or a truncation.\n */\nconst HOP_BY_HOP = new Set([\n \"connection\",\n \"keep-alive\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"te\",\n \"trailer\",\n \"transfer-encoding\",\n \"upgrade\",\n \"host\",\n \"content-length\"\n]);\n\n/**\n * Additionally dropped from the *response*, because `fetch` has already acted on\n * them by the time we see the body.\n *\n * `Content-Encoding` is the one that matters and the one that is easy to miss:\n * undici decompresses a `gzip` response transparently, but leaves the header on\n * the `Response` object. Copy it onto the body we hand back and the client is\n * told to gunzip bytes that are already plain — which does not fail cleanly. It\n * hangs, or it surfaces as a decode error several layers away from the proxy\n * that caused it. (This is not hypothetical: the runtime compresses its own\n * responses, so *every* forwarded response carries the header.)\n */\nconst RESPONSE_ONLY_DROP = new Set([\"content-encoding\", \"content-length\"]);\n\nexport interface FunctionsProxyOptions {\n /** Base URL of the process serving the functions, e.g. `http://functions:8080`. */\n upstream: string;\n /** The mount path being forwarded, e.g. `/api/functions`. */\n basePath: string;\n}\n\n/**\n * A router that forwards everything under its mount point to `upstream`.\n *\n * Method, path, query string, body and headers travel verbatim, with two\n * deliberate exceptions:\n *\n * 1. Hop-by-hop headers are dropped ({@link HOP_BY_HOP}).\n * 2. `X-Forwarded-For` gains the address this process saw the request come from.\n * This is not decoration — the upstream's rate limiters and the auth routes'\n * IP logging read that chain, and without the append every forwarded request\n * arrives from one address: the API pod's. Every caller would share a single\n * rate-limit bucket and every login would be logged from the same IP.\n *\n * **The upstream must therefore count one more proxy hop than the API process\n * does.** `TRUSTED_PROXY_HOPS` on the functions process is the API's value\n * plus one. Left unset it defaults to 0, and the upstream falls back to the\n * socket address — again, the API pod's — which is safe but useless.\n */\nexport function createFunctionsProxy(options: FunctionsProxyOptions): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n // Trailing slash trimmed once, here: joining it per request is how a proxy\n // ends up asking for `//functions/x`, which some servers route and some 404.\n const upstream = options.upstream.replace(/\\/+$/, \"\");\n\n router.all(\"/*\", async (c) => {\n const incoming = new URL(c.req.url);\n // The path *below* the mount point. Hono gives the full request path, so\n // taking the suffix is what keeps the upstream's own mount identical:\n // `/api/functions/send-invoice` here becomes `/api/functions/send-invoice`\n // there, not `/api/functions/api/functions/send-invoice`.\n const suffix = incoming.pathname.startsWith(options.basePath)\n ? incoming.pathname.slice(options.basePath.length)\n : incoming.pathname;\n const target = `${upstream}${options.basePath}${suffix}${incoming.search}`;\n\n const headers = new Headers();\n for (const [name, value] of c.req.raw.headers) {\n if (!HOP_BY_HOP.has(name.toLowerCase())) headers.set(name, value);\n }\n\n const seenFrom = socketAddress(c);\n if (seenFrom) {\n const chain = c.req.header(\"x-forwarded-for\");\n headers.set(\"x-forwarded-for\", chain ? `${chain}, ${seenFrom}` : seenFrom);\n }\n\n try {\n const response = await fetch(target, {\n method: c.req.method,\n headers,\n // GET and HEAD may not carry one; everything else streams\n // through rather than being buffered, so a large upload does not\n // have to fit in this process's memory on its way past.\n body: c.req.method === \"GET\" || c.req.method === \"HEAD\" ? undefined : c.req.raw.body,\n // Required by undici whenever the body is a stream. Without it\n // the fetch rejects with a message about duplex that says\n // nothing about what to do.\n duplex: \"half\",\n redirect: \"manual\"\n } as RequestInit);\n\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers: responseHeaders(response.headers)\n });\n } catch (err) {\n // The upstream being down is an infrastructure fact, not a bug in\n // the caller's request, and 502 is the one status that says so.\n // Named explicitly in the body because the alternative — a generic\n // 500 from the API process — sends people reading the wrong logs.\n logger.error(\"Functions upstream unreachable\", {\n upstream,\n path: incoming.pathname,\n error: err instanceof Error ? err : new Error(String(err))\n });\n return c.json({\n error: {\n code: \"FUNCTIONS_UPSTREAM_UNREACHABLE\",\n message: \"The process serving custom functions could not be reached from this one.\"\n }\n }, 502);\n }\n });\n\n return router;\n}\n\n/**\n * Response headers, minus the ones that describe the upstream's connection and\n * the ones `fetch` has already consumed on our behalf.\n */\nfunction responseHeaders(headers: Headers): Headers {\n const out = new Headers();\n for (const [name, value] of headers) {\n const lower = name.toLowerCase();\n if (HOP_BY_HOP.has(lower) || RESPONSE_ONLY_DROP.has(lower)) continue;\n out.set(name, value);\n }\n return out;\n}\n\n/** The address this process saw, or undefined on a runtime that cannot say. */\nfunction socketAddress(c: Context<HonoEnv>): string | undefined {\n try {\n return getConnInfo(c).remote.address;\n } catch {\n return undefined;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,IAAM,6BAAa,IAAI,IAAI;CACvB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ,CAAC;;;;;;;;;;;;;AAcD,IAAM,qCAAqB,IAAI,IAAI,CAAC,oBAAoB,gBAAgB,CAAC;;;;;;;;;;;;;;;;;;;AA2BzE,SAAgB,qBAAqB,SAA+C;CAChF,MAAM,SAAS,IAAI,KAAc;CAGjC,MAAM,WAAW,QAAQ,SAAS,QAAQ,QAAQ,EAAE;CAEpD,OAAO,IAAI,MAAM,OAAO,MAAM;EAC1B,MAAM,WAAW,IAAI,IAAI,EAAE,IAAI,GAAG;EAKlC,MAAM,SAAS,SAAS,SAAS,WAAW,QAAQ,QAAQ,IACtD,SAAS,SAAS,MAAM,QAAQ,SAAS,MAAM,IAC/C,SAAS;EACf,MAAM,SAAS,GAAG,WAAW,QAAQ,WAAW,SAAS,SAAS;EAElE,MAAM,UAAU,IAAI,QAAQ;EAC5B,KAAK,MAAM,CAAC,MAAM,UAAU,EAAE,IAAI,IAAI,SAClC,IAAI,CAAC,WAAW,IAAI,KAAK,YAAY,CAAC,GAAG,QAAQ,IAAI,MAAM,KAAK;EAGpE,MAAM,WAAW,cAAc,CAAC;EAChC,IAAI,UAAU;GACV,MAAM,QAAQ,EAAE,IAAI,OAAO,iBAAiB;GAC5C,QAAQ,IAAI,mBAAmB,QAAQ,GAAG,MAAM,IAAI,aAAa,QAAQ;EAC7E;EAEA,IAAI;GACA,MAAM,WAAW,MAAM,MAAM,QAAQ;IACjC,QAAQ,EAAE,IAAI;IACd;IAIA,MAAM,EAAE,IAAI,WAAW,SAAS,EAAE,IAAI,WAAW,SAAS,KAAA,IAAY,EAAE,IAAI,IAAI;IAIhF,QAAQ;IACR,UAAU;GACd,CAAgB;GAEhB,OAAO,IAAI,SAAS,SAAS,MAAM;IAC/B,QAAQ,SAAS;IACjB,YAAY,SAAS;IACrB,SAAS,gBAAgB,SAAS,OAAO;GAC7C,CAAC;EACL,SAAS,KAAK;GAKV,OAAO,MAAM,kCAAkC;IAC3C;IACA,MAAM,SAAS;IACf,OAAO,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;GAC7D,CAAC;GACD,OAAO,EAAE,KAAK,EACV,OAAO;IACH,MAAM;IACN,SAAS;GACb,EACJ,GAAG,GAAG;EACV;CACJ,CAAC;CAED,OAAO;AACX;;;;;AAMA,SAAS,gBAAgB,SAA2B;CAChD,MAAM,MAAM,IAAI,QAAQ;CACxB,KAAK,MAAM,CAAC,MAAM,UAAU,SAAS;EACjC,MAAM,QAAQ,KAAK,YAAY;EAC/B,IAAI,WAAW,IAAI,KAAK,KAAK,mBAAmB,IAAI,KAAK,GAAG;EAC5D,IAAI,IAAI,MAAM,KAAK;CACvB;CACA,OAAO;AACX;;AAGA,SAAS,cAAc,GAAyC;CAC5D,IAAI;EACA,OAAO,YAAY,CAAC,CAAC,CAAC,OAAO;CACjC,QAAQ;EACJ;CACJ;AACJ"}
1
+ {"version":3,"file":"proxy-qRlqeUmO.js","names":[],"sources":["../src/functions/proxy.ts"],"sourcesContent":["/**\n * Forwarding `/api/functions/*` to the process that serves it.\n *\n * A split deployment normally puts a reverse proxy in front and routes the path\n * there. This exists so it does not have to: set `REBASE_FUNCTIONS_UPSTREAM` on\n * the API process and the two-container topology presents the identical URL\n * surface as the one-container one, with nothing new to install and nothing for\n * a client or an SDK to know about.\n *\n * It is a transport hop, not a trust boundary. Nothing is authenticated or\n * authorised here: the upstream runs the same auth middleware against the same\n * `JWT_SECRET` and must see the original `Authorization` header, so re-deciding\n * anything on the way through would put two answers where the system has one.\n */\nimport { Hono, type Context } from \"hono\";\nimport { getConnInfo } from \"@hono/node-server/conninfo\";\nimport type { HonoEnv } from \"../api/types\";\nimport { logger } from \"../utils/logger\";\n\n/**\n * Headers that describe *this* connection rather than the request, and so must\n * not be copied onto a new one.\n *\n * `host` is the important one: forwarded unchanged it names the API process, and\n * an upstream doing any virtual-host routing would answer for the wrong service.\n * `content-length` and `transfer-encoding` are recomputed by `fetch` from the\n * body it is actually given, and a stale value is a hang or a truncation.\n */\nconst HOP_BY_HOP = new Set([\n \"connection\",\n \"keep-alive\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"te\",\n \"trailer\",\n \"transfer-encoding\",\n \"upgrade\",\n \"host\",\n \"content-length\"\n]);\n\n/**\n * Additionally dropped from the *response*, because `fetch` has already acted on\n * them by the time we see the body.\n *\n * `Content-Encoding` is the one that matters and the one that is easy to miss:\n * undici decompresses a `gzip` response transparently, but leaves the header on\n * the `Response` object. Copy it onto the body we hand back and the client is\n * told to gunzip bytes that are already plain — which does not fail cleanly. It\n * hangs, or it surfaces as a decode error several layers away from the proxy\n * that caused it. (This is not hypothetical: the runtime compresses its own\n * responses, so *every* forwarded response carries the header.)\n */\nconst RESPONSE_ONLY_DROP = new Set([\"content-encoding\", \"content-length\"]);\n\nexport interface FunctionsProxyOptions {\n /** Base URL of the process serving the functions, e.g. `http://functions:8080`. */\n upstream: string;\n /** The mount path being forwarded, e.g. `/api/functions`. */\n basePath: string;\n}\n\n/**\n * A router that forwards everything under its mount point to `upstream`.\n *\n * Method, path, query string, body and headers travel verbatim, with two\n * deliberate exceptions:\n *\n * 1. Hop-by-hop headers are dropped ({@link HOP_BY_HOP}).\n * 2. `X-Forwarded-For` gains the address this process saw the request come from.\n * This is not decoration — the upstream's rate limiters and the auth routes'\n * IP logging read that chain, and without the append every forwarded request\n * arrives from one address: the API pod's. Every caller would share a single\n * rate-limit bucket and every login would be logged from the same IP.\n *\n * **The upstream must therefore count one more proxy hop than the API process\n * does.** `TRUSTED_PROXY_HOPS` on the functions process is the API's value\n * plus one. Left unset it defaults to 0, and the upstream falls back to the\n * socket address — again, the API pod's — which is safe but useless.\n */\nexport function createFunctionsProxy(options: FunctionsProxyOptions): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n // Trailing slash trimmed once, here: joining it per request is how a proxy\n // ends up asking for `//functions/x`, which some servers route and some 404.\n const upstream = options.upstream.replace(/\\/+$/, \"\");\n\n router.all(\"/*\", async (c) => {\n const incoming = new URL(c.req.url);\n // The path *below* the mount point. Hono gives the full request path, so\n // taking the suffix is what keeps the upstream's own mount identical:\n // `/api/functions/send-invoice` here becomes `/api/functions/send-invoice`\n // there, not `/api/functions/api/functions/send-invoice`.\n const suffix = incoming.pathname.startsWith(options.basePath)\n ? incoming.pathname.slice(options.basePath.length)\n : incoming.pathname;\n const target = `${upstream}${options.basePath}${suffix}${incoming.search}`;\n\n const headers = new Headers();\n for (const [name, value] of c.req.raw.headers) {\n if (!HOP_BY_HOP.has(name.toLowerCase())) headers.set(name, value);\n }\n\n const seenFrom = socketAddress(c);\n if (seenFrom) {\n const chain = c.req.header(\"x-forwarded-for\");\n headers.set(\"x-forwarded-for\", chain ? `${chain}, ${seenFrom}` : seenFrom);\n }\n\n try {\n const response = await fetch(target, {\n method: c.req.method,\n headers,\n // GET and HEAD may not carry one; everything else streams\n // through rather than being buffered, so a large upload does not\n // have to fit in this process's memory on its way past.\n body: c.req.method === \"GET\" || c.req.method === \"HEAD\" ? undefined : c.req.raw.body,\n // Required by undici whenever the body is a stream. Without it\n // the fetch rejects with a message about duplex that says\n // nothing about what to do.\n duplex: \"half\",\n redirect: \"manual\"\n } as RequestInit);\n\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers: responseHeaders(response.headers)\n });\n } catch (err) {\n // The upstream being down is an infrastructure fact, not a bug in\n // the caller's request, and 502 is the one status that says so.\n // Named explicitly in the body because the alternative — a generic\n // 500 from the API process — sends people reading the wrong logs.\n logger.error(\"Functions upstream unreachable\", {\n upstream,\n path: incoming.pathname,\n error: err instanceof Error ? err : new Error(String(err))\n });\n return c.json({\n error: {\n code: \"FUNCTIONS_UPSTREAM_UNREACHABLE\",\n message: \"The process serving custom functions could not be reached from this one.\"\n }\n }, 502);\n }\n });\n\n return router;\n}\n\n/**\n * Response headers, minus the ones that describe the upstream's connection and\n * the ones `fetch` has already consumed on our behalf.\n */\nfunction responseHeaders(headers: Headers): Headers {\n const out = new Headers();\n for (const [name, value] of headers) {\n const lower = name.toLowerCase();\n if (HOP_BY_HOP.has(lower) || RESPONSE_ONLY_DROP.has(lower)) continue;\n out.set(name, value);\n }\n return out;\n}\n\n/** The address this process saw, or undefined on a runtime that cannot say. */\nfunction socketAddress(c: Context<HonoEnv>): string | undefined {\n try {\n return getConnInfo(c).remote.address;\n } catch {\n return undefined;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,IAAM,6BAAa,IAAI,IAAI;CACvB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ,CAAC;;;;;;;;;;;;;AAcD,IAAM,qCAAqB,IAAI,IAAI,CAAC,oBAAoB,gBAAgB,CAAC;;;;;;;;;;;;;;;;;;;AA2BzE,SAAgB,qBAAqB,SAA+C;CAChF,MAAM,SAAS,IAAI,KAAc;CAGjC,MAAM,WAAW,QAAQ,SAAS,QAAQ,QAAQ,EAAE;CAEpD,OAAO,IAAI,MAAM,OAAO,MAAM;EAC1B,MAAM,WAAW,IAAI,IAAI,EAAE,IAAI,GAAG;EAKlC,MAAM,SAAS,SAAS,SAAS,WAAW,QAAQ,QAAQ,IACtD,SAAS,SAAS,MAAM,QAAQ,SAAS,MAAM,IAC/C,SAAS;EACf,MAAM,SAAS,GAAG,WAAW,QAAQ,WAAW,SAAS,SAAS;EAElE,MAAM,UAAU,IAAI,QAAQ;EAC5B,KAAK,MAAM,CAAC,MAAM,UAAU,EAAE,IAAI,IAAI,SAClC,IAAI,CAAC,WAAW,IAAI,KAAK,YAAY,CAAC,GAAG,QAAQ,IAAI,MAAM,KAAK;EAGpE,MAAM,WAAW,cAAc,CAAC;EAChC,IAAI,UAAU;GACV,MAAM,QAAQ,EAAE,IAAI,OAAO,iBAAiB;GAC5C,QAAQ,IAAI,mBAAmB,QAAQ,GAAG,MAAM,IAAI,aAAa,QAAQ;EAC7E;EAEA,IAAI;GACA,MAAM,WAAW,MAAM,MAAM,QAAQ;IACjC,QAAQ,EAAE,IAAI;IACd;IAIA,MAAM,EAAE,IAAI,WAAW,SAAS,EAAE,IAAI,WAAW,SAAS,KAAA,IAAY,EAAE,IAAI,IAAI;IAIhF,QAAQ;IACR,UAAU;GACd,CAAgB;GAEhB,OAAO,IAAI,SAAS,SAAS,MAAM;IAC/B,QAAQ,SAAS;IACjB,YAAY,SAAS;IACrB,SAAS,gBAAgB,SAAS,OAAO;GAC7C,CAAC;EACL,SAAS,KAAK;GAKV,OAAO,MAAM,kCAAkC;IAC3C;IACA,MAAM,SAAS;IACf,OAAO,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;GAC7D,CAAC;GACD,OAAO,EAAE,KAAK,EACV,OAAO;IACH,MAAM;IACN,SAAS;GACb,EACJ,GAAG,GAAG;EACV;CACJ,CAAC;CAED,OAAO;AACX;;;;;AAMA,SAAS,gBAAgB,SAA2B;CAChD,MAAM,MAAM,IAAI,QAAQ;CACxB,KAAK,MAAM,CAAC,MAAM,UAAU,SAAS;EACjC,MAAM,QAAQ,KAAK,YAAY;EAC/B,IAAI,WAAW,IAAI,KAAK,KAAK,mBAAmB,IAAI,KAAK,GAAG;EAC5D,IAAI,IAAI,MAAM,KAAK;CACvB;CACA,OAAO;AACX;;AAGA,SAAS,cAAc,GAAyC;CAC5D,IAAI;EACA,OAAO,YAAY,CAAC,CAAC,CAAC,OAAO;CACjC,QAAQ;EACJ;CACJ;AACJ"}