@rebasepro/server 0.19.1 → 0.19.2-canary.g09316f6

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 (80) hide show
  1. package/dist/{GCSStorageController-ZXPoNqW3.js → GCSStorageController-CLIJXwGS.js} +2 -2
  2. package/dist/{GCSStorageController-ZXPoNqW3.js.map → GCSStorageController-CLIJXwGS.js.map} +1 -1
  3. package/dist/{S3StorageController-5pAXyv31.js → S3StorageController-Dcuf8lMA.js} +2 -2
  4. package/dist/{S3StorageController-5pAXyv31.js.map → S3StorageController-Dcuf8lMA.js.map} +1 -1
  5. package/dist/api/errors.d.ts +6 -1
  6. package/dist/api/rest/api-generator.d.ts +80 -0
  7. package/dist/api/rest/batch.d.ts +71 -0
  8. package/dist/api/rest/conflict-target.d.ts +20 -0
  9. package/dist/api/rest/etag.d.ts +55 -0
  10. package/dist/api/rest/field-access-query.d.ts +66 -0
  11. package/dist/api/rest/field-ops.d.ts +86 -0
  12. package/dist/api/rest/query-parser.d.ts +16 -2
  13. package/dist/api/rest/soft-delete-params.d.ts +37 -0
  14. package/dist/api/rest/write-validation.d.ts +51 -8
  15. package/dist/api/types.d.ts +28 -3
  16. package/dist/{ast-schema-editor-CNgFJ3NF.js → ast-schema-editor-C6mDz0XN.js} +2 -2
  17. package/dist/{ast-schema-editor-CNgFJ3NF.js.map → ast-schema-editor-C6mDz0XN.js.map} +1 -1
  18. package/dist/auth/jwt.d.ts +17 -0
  19. package/dist/auth/rls-scope.d.ts +1 -0
  20. package/dist/{auth-BRiOuyq8.js → auth-DJsLXsCR.js} +61 -60
  21. package/dist/auth-DJsLXsCR.js.map +1 -0
  22. package/dist/{backup-C8P6Cl3G.js → backup-DGu0v9Ku.js} +2 -2
  23. package/dist/{backup-C8P6Cl3G.js.map → backup-DGu0v9Ku.js.map} +1 -1
  24. package/dist/boot/bundle.d.ts +12 -3
  25. package/dist/boot/driver.d.ts +11 -4
  26. package/dist/boot/env.d.ts +2 -2
  27. package/dist/{contract-routes-CusnEB5h.js → contract-routes-eLxV0le1.js} +2 -2
  28. package/dist/{contract-routes-CusnEB5h.js.map → contract-routes-eLxV0le1.js.map} +1 -1
  29. package/dist/{cron-loader-DnmIePn_.js → cron-loader-CQjvjpEw.js} +2 -2
  30. package/dist/{cron-loader-DnmIePn_.js.map → cron-loader-CQjvjpEw.js.map} +1 -1
  31. package/dist/{cron-routes-D3x2ydMa.js → cron-routes-Bfwni8Zg.js} +3 -3
  32. package/dist/{cron-routes-D3x2ydMa.js.map → cron-routes-Bfwni8Zg.js.map} +1 -1
  33. package/dist/{cron-store-CCQXwgVL.js → cron-store-Bsiw4Q6u.js} +3 -3
  34. package/dist/{cron-store-CCQXwgVL.js.map → cron-store-Bsiw4Q6u.js.map} +1 -1
  35. package/dist/{errors-HjfaPlvY.js → errors-DMImyqyR.js} +8 -3
  36. package/dist/errors-DMImyqyR.js.map +1 -0
  37. package/dist/{function-routes-gQ0EShVG.js → function-routes-C4nB2h0z.js} +2 -2
  38. package/dist/{function-routes-gQ0EShVG.js.map → function-routes-C4nB2h0z.js.map} +1 -1
  39. package/dist/functions/index.js +7 -2
  40. package/dist/functions/index.js.map +1 -1
  41. package/dist/{history-recorder-B1FwXx9J.js → history-recorder-r5_IzSHK.js} +2 -2
  42. package/dist/{history-recorder-B1FwXx9J.js.map → history-recorder-r5_IzSHK.js.map} +1 -1
  43. package/dist/{history-store-LHXaQywp.js → history-store-D4RVK-uZ.js} +2 -2
  44. package/dist/{history-store-LHXaQywp.js.map → history-store-D4RVK-uZ.js.map} +1 -1
  45. package/dist/index.d.ts +5 -0
  46. package/dist/index.es.js +2429 -7303
  47. package/dist/index.es.js.map +1 -1
  48. package/dist/{jobs-DkkD9mPV.js → jobs-CW5lm_Ix.js} +3 -3
  49. package/dist/{jobs-DkkD9mPV.js.map → jobs-CW5lm_Ix.js.map} +1 -1
  50. package/dist/{jwt-DkhXwMzR.js → jwt-DATvkKB_.js} +39 -3
  51. package/dist/{jwt-DkhXwMzR.js.map → jwt-DATvkKB_.js.map} +1 -1
  52. package/dist/{keys-g8lbVC_o.js → keys-Qfc4XieN.js} +2 -2
  53. package/dist/{keys-g8lbVC_o.js.map → keys-Qfc4XieN.js.map} +1 -1
  54. package/dist/{logs-routes-CbsTpozn.js → logs-routes-DnJINsMu.js} +2 -2
  55. package/dist/{logs-routes-CbsTpozn.js.map → logs-routes-DnJINsMu.js.map} +1 -1
  56. package/dist/{openapi-generator-BIBbO1Tq.js → openapi-generator-DGyLbISS.js} +374 -48
  57. package/dist/openapi-generator-DGyLbISS.js.map +1 -0
  58. package/dist/{query-parser-C68Q9EX4.js → query-parser-BQiPZrM-.js} +257 -17
  59. package/dist/query-parser-BQiPZrM-.js.map +1 -0
  60. package/dist/{request-timeout-DESvlfrS.js → request-timeout-BR-OBwES.js} +2 -2
  61. package/dist/{request-timeout-DESvlfrS.js.map → request-timeout-BR-OBwES.js.map} +1 -1
  62. package/dist/{schema-editor-routes-CcZKh50q.js → schema-editor-routes-C3TLZqAC.js} +13 -13
  63. package/dist/{schema-editor-routes-CcZKh50q.js.map → schema-editor-routes-C3TLZqAC.js.map} +1 -1
  64. package/dist/{src-DHK4fHkw.js → src-Br6ARbs6.js} +26 -2
  65. package/dist/src-Br6ARbs6.js.map +1 -0
  66. package/dist/{src-Dq-I3Ybx.js → src-DqZ9YiGA.js} +1387 -94
  67. package/dist/src-DqZ9YiGA.js.map +1 -0
  68. package/dist/storage/index.d.ts +2 -0
  69. package/dist/storage/property-limits.d.ts +77 -0
  70. package/dist/storage/routes.d.ts +13 -0
  71. package/dist/storage/tus-handler.d.ts +22 -1
  72. package/package.json +7 -6
  73. package/dist/auth-BRiOuyq8.js.map +0 -1
  74. package/dist/errors-HjfaPlvY.js.map +0 -1
  75. package/dist/openapi-generator-BIBbO1Tq.js.map +0 -1
  76. package/dist/query-parser-C68Q9EX4.js.map +0 -1
  77. package/dist/schemas-C3234HWE.js +0 -6827
  78. package/dist/schemas-C3234HWE.js.map +0 -1
  79. package/dist/src-DHK4fHkw.js.map +0 -1
  80. package/dist/src-Dq-I3Ybx.js.map +0 -1
@@ -2,8 +2,8 @@ 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 { S as resolveCollectionRelations, v as fieldKeyForColumn, x as isRelationRequired, y as findRelation } from "./src-Dq-I3Ybx.js";
6
- import "./src-DHK4fHkw.js";
5
+ import { B as resolveCollectionRelations, E as effectiveAccess, I as fieldKeyForColumn, L as findRelation, P as getTenantConfig, z as isRelationRequired } from "./src-DqZ9YiGA.js";
6
+ import "./src-Br6ARbs6.js";
7
7
  //#region ../types/src/types/relations.ts
8
8
  /** @group Models */
9
9
  function isToMany(relation) {
@@ -55,12 +55,18 @@ function generateOpenApiSpec(collections, options = {}) {
55
55
  },
56
56
  description: "Page number (alternative to offset). Calculates offset as (page-1)*limit"
57
57
  },
58
+ {
59
+ name: "after",
60
+ in: "query",
61
+ schema: { type: "string" },
62
+ description: "Keyset cursor: continue after the row the previous page ended on. Pass back `meta.nextCursor` from that response, unchanged — it is opaque, and encodes both the sort keys and the last row's values for them. Unlike `offset`, a row inserted or deleted before the cursor cannot shift the window, so a walk neither repeats nor skips rows. Cannot be combined with `offset`/`page` (400 CURSOR_WITH_OFFSET), and an `orderBy` different from the one the cursor was issued under is refused (400 CURSOR_ORDER_MISMATCH) rather than seeked in an order nobody asked for."
63
+ },
58
64
  {
59
65
  name: "orderBy",
60
66
  in: "query",
61
67
  schema: { type: "string" },
62
- description: "Sort field and direction. Accepts `field:asc` or `field:desc`, or a JSON array `[{\"field\":\"name\",\"direction\":\"asc\"}]` — several entries sort by each in turn, the second breaking ties on the first.",
63
- example: "created_at:desc"
68
+ description: "Sort field and direction. Accepts `field:asc`, `field:desc`, or `field:desc:last` — the third segment places NULLs (`first`/`last`), defaulting to Postgres's own convention (last ascending, first descending). Also accepts a JSON array `[{\"field\":\"name\",\"direction\":\"asc\",\"nulls\":\"last\"}]` — several entries sort by each in turn, the second breaking ties on the first.",
69
+ example: "created_at:desc:last"
64
70
  },
65
71
  {
66
72
  name: "where",
@@ -83,20 +89,34 @@ function generateOpenApiSpec(collections, options = {}) {
83
89
  description: "Conjunction of conditions, AND-ed with `where` and `searchString`. Ignored when `or` is also present.",
84
90
  example: "(views.gte.10,status.eq.draft)"
85
91
  },
92
+ {
93
+ name: "not",
94
+ in: "query",
95
+ schema: { type: "string" },
96
+ description: "Negation, AND-ed with `where` and `searchString`. Negates the **conjunction** of its conditions: `not(a)` is `NOT a`, `not(a,b)` is `NOT (a AND b)`. Groups nest, so `not(or(a,b))` is the De Morgan case. Compiles to a real SQL `NOT (...)`, which — three-valued logic — also excludes rows whose column is NULL. Ignored when `or` or `and` is also present.",
97
+ example: "(status.eq.draft,views.gte.10)"
98
+ },
86
99
  {
87
100
  name: "include",
88
101
  in: "query",
89
102
  schema: { type: "string" },
90
- description: "Comma-separated list of relations to include (eager-load). Use `*` for all relations.",
91
- example: "author,tags"
103
+ description: "Relations to load, in either of two spellings. **Comma-separated names or dotted paths** — `author,comments.author`, up to 3 hops deep; `*` loads every relation one hop deep. **JSON**, when a relation needs narrowing — `{\"comments\":{\"limit\":5,\"where\":{\"published\":[\"==\",true]},\"orderBy\":\"created_at:desc\",\"fields\":\"id,body\",\"include\":{\"author\":true}}}`. A value starting with `{` is read as the JSON form. A name that is not a relation of the collection is a 400 UNKNOWN_RELATION, not a silently missing field.",
104
+ example: "author,comments.author"
92
105
  },
93
106
  {
94
107
  name: "fields",
95
108
  in: "query",
96
109
  schema: { type: "string" },
97
- description: "Comma-separated list of fields to return (field selection)",
110
+ description: "Comma-separated columns to return. A projection pushed into the SELECT, so a query that needs two fields of a wide row reads two columns. The primary key is always returned (a row that cannot be addressed cannot be updated, deleted, or paged past), and `excludeFromApi` columns stay hidden whether or not they are named here. An unknown column is a 400 UNKNOWN_FIELD.",
98
111
  example: "id,name,created_at"
99
112
  },
113
+ {
114
+ name: "distinct",
115
+ in: "query",
116
+ schema: { type: "boolean" },
117
+ description: "`SELECT DISTINCT` over the returned columns. Only meaningful alongside `fields`: the primary key is always in the projection, so without narrowing it every row is already distinct. `meta.total` counts distinct rows too. Refused (400) alongside `searchString` or a vector search, which attach a per-row score that makes every row distinct by construction, and (400 DISTINCT_ORDER_BY_NOT_SELECTED) when `orderBy` names a column `fields` does not return.",
118
+ example: "true"
119
+ },
100
120
  {
101
121
  name: "searchString",
102
122
  in: "query",
@@ -182,6 +202,10 @@ function generateOpenApiSpec(collections, options = {}) {
182
202
  hasMore: {
183
203
  type: "boolean",
184
204
  description: "Whether more records exist beyond this page"
205
+ },
206
+ nextCursor: {
207
+ type: "string",
208
+ description: "Opaque keyset cursor continuing this listing — pass it back as `?after=`. Present when `hasMore` is true and the page returned at least one row; absent on the last page and on an ordering no cursor can describe (relevance, whose scores are computed per query and not stored). Do not parse it: the encoding exists to be changed."
185
209
  }
186
210
  }
187
211
  }
@@ -204,6 +228,160 @@ function generateOpenApiSpec(collections, options = {}) {
204
228
  const tags = spec.tags;
205
229
  const reservedParameterNames = new Set(listQueryParameters().map((p) => p.name));
206
230
  const registeredSchemas = new Set((collections || []).map(schemaNameFor));
231
+ /**
232
+ * `Prefer: return=minimal`, on every route that would otherwise send a row
233
+ * back. Documented rather than left implicit because a client generated
234
+ * from this spec cannot send a header the spec does not mention.
235
+ */
236
+ const preferHeader = {
237
+ name: "Prefer",
238
+ in: "header",
239
+ required: false,
240
+ schema: {
241
+ type: "string",
242
+ enum: ["return=minimal"]
243
+ },
244
+ description: "`return=minimal` asks the server not to send the written row back. Single writes then answer `204 No Content`; bulk and batch writes answer `200` carrying the ids only. The response repeats it in `Preference-Applied` when it was honoured."
245
+ };
246
+ const ifMatchHeader = {
247
+ name: "If-Match",
248
+ in: "header",
249
+ required: false,
250
+ schema: { type: "string" },
251
+ description: "The `ETag` this edit was made against, from the `GET` that read the row. The write is refused with `412` if the row has changed since — which is the difference between \"update the row I read\" and \"overwrite whatever is there now\". `*` means only that the row must exist."
252
+ };
253
+ const preconditionFailed = { 412: {
254
+ description: "The row changed since the ETag in `If-Match` was issued. Nothing was written: re-read the row, re-apply the change, and send the new ETag",
255
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
256
+ } };
257
+ const minimalResponse = { 204: {
258
+ description: "Written. `Prefer: return=minimal` was honoured, so there is no body",
259
+ headers: { "Preference-Applied": {
260
+ schema: { type: "string" },
261
+ description: "`return=minimal`"
262
+ } }
263
+ } };
264
+ if ((collections || []).length > 0) {
265
+ paths["/data/_batch"] = { post: {
266
+ tags: ["Data"],
267
+ summary: "Write across collections in one transaction",
268
+ description: "All-or-nothing across collections — an order and its line items, a user and their membership row. `/bulk` is one collection at a time, and sending the two halves as separate requests is exactly the sequence that can half-succeed.\n\nOperations run in order, each through the same pipeline its single-row route uses: the same validation, callbacks and row-level security, as the same role. An operation may name itself with `ref`, and a later one may stand `{ \"$ref\": \"order.id\" }` wherever a value goes — in `values`, at any depth, or as an `id`. Only backward references resolve.\n\nCapped at the same number of entries as a bulk write, because one batch is one transaction and holds its locks for the whole of it.",
269
+ operationId: "batchWrite",
270
+ parameters: [{
271
+ name: "Idempotency-Key",
272
+ in: "header",
273
+ required: false,
274
+ schema: { type: "string" },
275
+ description: "Names this batch so a retry is recognised instead of repeated. Without it a client that lost the response cannot tell a replay from a second batch, and the whole batch is written twice."
276
+ }, preferHeader],
277
+ requestBody: {
278
+ required: true,
279
+ content: { "application/json": { schema: {
280
+ type: "object",
281
+ required: ["operations"],
282
+ properties: { operations: {
283
+ type: "array",
284
+ items: { $ref: "#/components/schemas/BatchOperation" }
285
+ } }
286
+ } } }
287
+ },
288
+ responses: {
289
+ 200: {
290
+ description: "One entry per operation, in order: the written row for a create, update or upsert, and `null` for a delete",
291
+ content: { "application/json": { schema: {
292
+ type: "object",
293
+ properties: {
294
+ data: {
295
+ type: "array",
296
+ items: {
297
+ type: "object",
298
+ nullable: true
299
+ }
300
+ },
301
+ meta: {
302
+ type: "object",
303
+ properties: { operations: { type: "integer" } }
304
+ }
305
+ }
306
+ } } }
307
+ },
308
+ 400: {
309
+ description: "Malformed body, an unknown collection or field, an illegal field operation or conflict target, a forward `$ref`, or more operations than the limit. Nothing was written",
310
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
311
+ },
312
+ 404: {
313
+ description: "An `update` or `delete` names a row that does not exist; nothing was written",
314
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
315
+ },
316
+ 409: {
317
+ description: "A request with the same Idempotency-Key is still in flight",
318
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
319
+ },
320
+ 422: {
321
+ description: "The Idempotency-Key was already used for a different request",
322
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
323
+ },
324
+ ...errorResponses(requireAuth)
325
+ }
326
+ } };
327
+ schemas.FieldOperation = {
328
+ type: "object",
329
+ description: "A change to the column's current value rather than the value to store. Exactly one operator per field. Only on an update: an operation over a value that does not exist yet is refused with a 400.",
330
+ properties: {
331
+ $inc: {
332
+ type: "number",
333
+ description: "Add to a `number` column; negative to subtract."
334
+ },
335
+ $push: { description: "Append a value, or each of an array of values, to an `array` column." },
336
+ $pull: { description: "Remove every occurrence of a value from an `array` column." },
337
+ $merge: {
338
+ type: "object",
339
+ description: "Shallow-merge an object into a `map` column."
340
+ }
341
+ }
342
+ };
343
+ schemas.BatchOperation = {
344
+ type: "object",
345
+ required: ["op", "collection"],
346
+ properties: {
347
+ op: {
348
+ type: "string",
349
+ enum: [
350
+ "create",
351
+ "update",
352
+ "upsert",
353
+ "delete"
354
+ ]
355
+ },
356
+ collection: {
357
+ type: "string",
358
+ description: "The collection slug this operation writes to."
359
+ },
360
+ id: {
361
+ description: "Required for `update` and `delete`. May instead be a reference marker — an object whose single key is `$ref` and whose value is `<ref name>.<field>`, e.g. `{ \"$ref\": \"order.id\" }` — naming a column of the row an earlier operation wrote. (Described rather than declared as a schema: `$ref` is a reserved word to every OpenAPI reader, and a property named `$ref` is read as a reference and mangled.)",
362
+ oneOf: [
363
+ { type: "string" },
364
+ { type: "integer" },
365
+ { type: "object" }
366
+ ]
367
+ },
368
+ values: {
369
+ type: "object",
370
+ description: "The row's fields. Values may be `$ref` markers; an `update` may also carry field operations.",
371
+ additionalProperties: true
372
+ },
373
+ onConflict: {
374
+ type: "array",
375
+ items: { type: "string" },
376
+ description: "`upsert` only: the columns the conflict is matched on. Must carry a declared uniqueness guarantee. Defaults to the primary key."
377
+ },
378
+ ref: {
379
+ type: "string",
380
+ description: "Names this operation's result, so a later one can `$ref` its columns."
381
+ }
382
+ }
383
+ };
384
+ }
207
385
  for (const collection of collections || []) {
208
386
  const schemaName = schemaNameFor(collection);
209
387
  const slug = collection.slug;
@@ -307,6 +485,14 @@ function generateOpenApiSpec(collections, options = {}) {
307
485
  tags: [collection.name],
308
486
  summary: `Create ${collection.singularName || collection.name}`,
309
487
  operationId: `create${schemaName}`,
488
+ parameters: [{
489
+ name: "on_conflict",
490
+ in: "query",
491
+ required: false,
492
+ schema: { type: "string" },
493
+ description: "Comma-separated columns to upsert on, turning the create into INSERT ... ON CONFLICT DO UPDATE. They must carry a declared uniqueness guarantee — `validation.unique`, a `unique` index, or the primary key — or the request is refused with a 400 naming the targets that do exist. Left off, this is a plain insert and a duplicate key still raises.",
494
+ example: "email"
495
+ }, preferHeader],
310
496
  requestBody: {
311
497
  required: true,
312
498
  content: { "application/json": { schema: { $ref: `#/components/schemas/${schemaName}Input` } } }
@@ -316,6 +502,7 @@ function generateOpenApiSpec(collections, options = {}) {
316
502
  description: "Created entity",
317
503
  content: { "application/json": { schema: { $ref: `#/components/schemas/${schemaName}` } } }
318
504
  },
505
+ ...minimalResponse,
319
506
  ...errorResponses(requireAuth)
320
507
  }
321
508
  }
@@ -348,7 +535,7 @@ function generateOpenApiSpec(collections, options = {}) {
348
535
  summary: `Create many ${collection.name} in one transaction`,
349
536
  description: "All-or-nothing: if any row is rejected none of them land, and the error names the offending index. Every row still runs callbacks, relations and row-level security. Capped server-side because one batch holds its locks for its whole duration.",
350
537
  operationId: `createMany${schemaName}`,
351
- parameters: [idempotencyHeader],
538
+ parameters: [idempotencyHeader, preferHeader],
352
539
  requestBody: {
353
540
  required: true,
354
541
  content: { "application/json": { schema: {
@@ -361,7 +548,12 @@ function generateOpenApiSpec(collections, options = {}) {
361
548
  },
362
549
  upsert: {
363
550
  type: "boolean",
364
- description: "Write each row as INSERT ... ON CONFLICT DO UPDATE on the primary key."
551
+ description: "Write each row as INSERT ... ON CONFLICT DO UPDATE."
552
+ },
553
+ onConflict: {
554
+ type: "array",
555
+ items: { type: "string" },
556
+ description: "The columns the conflict is matched on, instead of the primary key. They must carry a declared uniqueness guarantee. Naming them without `upsert: true` is a 400 rather than a silently ignored field."
365
557
  }
366
558
  }
367
559
  } } }
@@ -391,7 +583,7 @@ function generateOpenApiSpec(collections, options = {}) {
391
583
  summary: `Update many ${collection.name} in one transaction`,
392
584
  description: "Each entry names its row and the fields to change. `{ id, data }` rather than flat rows carrying their own key, because on a table keyed on something other than `id` a flat row cannot say whether a column is the address or a value to write. An id matching no row fails the batch.",
393
585
  operationId: `updateMany${schemaName}`,
394
- parameters: [idempotencyHeader],
586
+ parameters: [idempotencyHeader, preferHeader],
395
587
  requestBody: {
396
588
  required: true,
397
589
  content: { "application/json": { schema: {
@@ -479,22 +671,36 @@ function generateOpenApiSpec(collections, options = {}) {
479
671
  tags: [collection.name],
480
672
  summary: `Get ${collection.singularName || collection.name} by ID`,
481
673
  operationId: `get${schemaName}ById`,
482
- parameters: [{
483
- name: "id",
484
- in: "path",
485
- required: true,
486
- schema: { type: "string" },
487
- description: "Entity ID"
488
- }, {
489
- name: "include",
490
- in: "query",
491
- schema: { type: "string" },
492
- description: "Comma-separated list of relations to include",
493
- example: "author,tags"
494
- }],
674
+ parameters: [
675
+ {
676
+ name: "id",
677
+ in: "path",
678
+ required: true,
679
+ schema: { type: "string" },
680
+ description: "Entity ID"
681
+ },
682
+ {
683
+ name: "include",
684
+ in: "query",
685
+ schema: { type: "string" },
686
+ description: "Relations to load: comma-separated names or dotted paths (`author,comments.author`, up to 3 hops), `*` for all one hop deep, or the JSON form for per-relation `limit`/`where`/`orderBy`/`fields`. An unknown name is a 400 UNKNOWN_RELATION.",
687
+ example: "author,comments.author"
688
+ },
689
+ {
690
+ name: "fields",
691
+ in: "query",
692
+ schema: { type: "string" },
693
+ description: "Comma-separated columns to return, as a SELECT projection. The primary key always survives and `excludeFromApi` columns stay hidden.",
694
+ example: "id,title"
695
+ }
696
+ ],
495
697
  responses: {
496
698
  200: {
497
699
  description: "Entity found",
700
+ headers: { ETag: {
701
+ schema: { type: "string" },
702
+ description: "This row's version. Send it back as `If-Match` on a later PATCH or DELETE to have the write refused if the row has changed in between."
703
+ } },
498
704
  content: { "application/json": { schema: { $ref: `#/components/schemas/${schemaName}` } } }
499
705
  },
500
706
  404: {
@@ -504,24 +710,48 @@ function generateOpenApiSpec(collections, options = {}) {
504
710
  ...errorResponses(requireAuth)
505
711
  }
506
712
  },
507
- patch: updateOperation(collection, schemaName, requireAuth),
713
+ patch: updateOperation(collection, schemaName, requireAuth, {
714
+ ifMatchHeader,
715
+ preferHeader,
716
+ preconditionFailed,
717
+ minimalResponse
718
+ }),
508
719
  delete: {
509
720
  tags: [collection.name],
510
721
  summary: `Delete ${collection.singularName || collection.name}`,
511
722
  operationId: `delete${schemaName}`,
512
- parameters: [{
513
- name: "id",
514
- in: "path",
515
- required: true,
516
- schema: { type: "string" },
517
- description: "Entity ID"
518
- }],
723
+ parameters: [
724
+ {
725
+ name: "id",
726
+ in: "path",
727
+ required: true,
728
+ schema: { type: "string" },
729
+ description: "Entity ID"
730
+ },
731
+ ifMatchHeader,
732
+ {
733
+ name: "Idempotency-Key",
734
+ in: "header",
735
+ required: false,
736
+ schema: { type: "string" },
737
+ 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."
738
+ }
739
+ ],
519
740
  responses: {
520
741
  204: { description: "Deleted successfully" },
521
742
  404: {
522
743
  description: "Entity not found",
523
744
  content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
524
745
  },
746
+ 409: {
747
+ description: "A request with the same Idempotency-Key is still in flight",
748
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
749
+ },
750
+ 422: {
751
+ description: "The Idempotency-Key was already used for a different request",
752
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
753
+ },
754
+ ...preconditionFailed,
525
755
  ...errorResponses(requireAuth)
526
756
  }
527
757
  }
@@ -594,8 +824,38 @@ function generateOpenApiSpec(collections, options = {}) {
594
824
  * being fixed in one loop at a time: this is the same rule the SDK generator
595
825
  * applies to its `Row` type (`packages/codegen/src/generate-types.ts`).
596
826
  */
597
- function isDocumentedProperty(property) {
598
- return !property.excludeFromApi;
827
+ function isDocumentedProperty(property, direction = "read") {
828
+ return effectiveAccess(property)?.[direction]?.length !== 0;
829
+ }
830
+ /**
831
+ * The `x-rebase-access` annotation, or nothing for a field with no rule.
832
+ *
833
+ * A vendor extension rather than a schema keyword because OpenAPI has no way to
834
+ * say "this property is present for some callers" — `readOnly` is about the
835
+ * direction of a field, not about who. Generators ignore what they do not know,
836
+ * so a client built from this spec still compiles; a *human* reading `/docs`, or
837
+ * a gateway that wants to enforce the same rule at the edge, gets the role lists
838
+ * verbatim. Emitted for the role case only: a field closed to everybody is
839
+ * absent from the document entirely, which is a stronger statement.
840
+ */
841
+ function accessAnnotation(property) {
842
+ const access = effectiveAccess(property);
843
+ if (!access) return void 0;
844
+ const annotation = {};
845
+ if (access.read !== void 0) annotation.read = [...access.read];
846
+ if (access.write !== void 0) annotation.write = [...access.write];
847
+ return Object.keys(annotation).length > 0 ? annotation : void 0;
848
+ }
849
+ /** The sentence `x-rebase-access` deserves in prose, for a reader of `/docs`. */
850
+ function accessDescription(property) {
851
+ const access = effectiveAccess(property);
852
+ if (!access) return void 0;
853
+ const parts = [];
854
+ const phrase = (roles) => roles.length === 0 ? "nobody through the API" : roles.map((r) => `\`${r}\``).join(", ") + " (and `admin`)";
855
+ if (access.read !== void 0) parts.push(`readable by ${phrase(access.read)}`);
856
+ if (access.write !== void 0) parts.push(`writable by ${phrase(access.write)}`);
857
+ if (parts.length === 0) return void 0;
858
+ return `Field access: ${parts.join("; ")}. A caller without the role does not receive the field at all — it is absent, not null.`;
599
859
  }
600
860
  /**
601
861
  * The keys `stripExcluded` deletes: the property name *and* its column name.
@@ -605,10 +865,10 @@ function isDocumentedProperty(property) {
605
865
  * other name. Same pair, same reason, as `excludedApiKeys` in the SDK
606
866
  * generator.
607
867
  */
608
- function excludedApiKeys(collection) {
868
+ function excludedApiKeys(collection, direction = "read") {
609
869
  const excluded = /* @__PURE__ */ new Set();
610
870
  for (const [key, property] of Object.entries(collection.properties ?? {})) {
611
- if (!property?.excludeFromApi) continue;
871
+ if (isDocumentedProperty(property, direction)) continue;
612
872
  excluded.add(key);
613
873
  const columnName = property.columnName;
614
874
  if (typeof columnName === "string") excluded.add(columnName);
@@ -750,6 +1010,7 @@ function buildCollectionSchema(collection, registeredSchemas) {
750
1010
  if (property.validation?.required && key !== idKey) required.push(key);
751
1011
  }
752
1012
  emitRelationProperties(collection, properties, required, emitted, registeredSchemas);
1013
+ annotateTenantField(collection, properties, "read");
753
1014
  return {
754
1015
  type: "object",
755
1016
  required: required.length > 0 ? required : void 0,
@@ -757,6 +1018,35 @@ function buildCollectionSchema(collection, registeredSchemas) {
757
1018
  };
758
1019
  }
759
1020
  /**
1021
+ * Mark the tenant field, on whichever schema is being built.
1022
+ *
1023
+ * A vendor extension for the same reason `x-rebase-access` is one: OpenAPI has
1024
+ * no keyword for "the server fills this in from who you are, and refuses a
1025
+ * value that is not yours". `readOnly` is the closest and it is wrong — the
1026
+ * field *is* writable, by a caller sending their own tenant, and a bypass role
1027
+ * may send any. Generators ignore what they do not know, so a client built from
1028
+ * this document still compiles; a human reading `/docs`, or a gateway wanting
1029
+ * to enforce the same boundary at the edge, learns the field is special and
1030
+ * why.
1031
+ *
1032
+ * Applied after the property loops rather than inside `convertPropertyToSchema`
1033
+ * because tenancy is a fact about the *collection*, and that function is handed
1034
+ * a property with no idea which collection it came from.
1035
+ */
1036
+ function annotateTenantField(collection, properties, direction) {
1037
+ const tenant = getTenantConfig(collection);
1038
+ if (!tenant) return;
1039
+ const schema = properties[tenant.field];
1040
+ if (!schema || typeof schema !== "object") return;
1041
+ const existing = schema.description;
1042
+ const sentence = direction === "write" ? "The tenant this row belongs to. Omit it and the server stamps the tenant you are calling as; send another tenant's and the write is refused with `TENANT_MISMATCH`. It cannot be changed on an update (`TENANT_IMMUTABLE`)." : "The tenant this row belongs to. Rows of other tenants are not returned at all.";
1043
+ properties[tenant.field] = {
1044
+ ...schema,
1045
+ description: typeof existing === "string" && existing ? `${existing} — ${sentence}` : sentence,
1046
+ "x-rebase-tenant": true
1047
+ };
1048
+ }
1049
+ /**
760
1050
  * The PATCH/PUT operation for `/data/{slug}/{id}`.
761
1051
  *
762
1052
  * Split out because both verbs serve it and they must not drift: the update
@@ -767,19 +1057,30 @@ function buildCollectionSchema(collection, registeredSchemas) {
767
1057
  * that spec demanded fields the server does not, and a spec-validating gateway
768
1058
  * would have rejected partial updates the server accepts.
769
1059
  */
770
- function updateOperation(collection, schemaName, requireAuth) {
1060
+ function updateOperation(collection, schemaName, requireAuth, shared) {
771
1061
  return {
772
1062
  tags: [collection.name],
773
1063
  summary: `Update ${collection.singularName || collection.name}`,
774
- description: "Partial update: only the properties present in the body are written; the rest are left unchanged.",
1064
+ description: "Partial update: only the properties present in the body are written; the rest are left unchanged.\n\nA property's value may instead be a field operation — `{ \"views\": { \"$inc\": 1 } }`, `{ \"tags\": { \"$push\": \"new\" } }`, `{ \"tags\": { \"$pull\": \"old\" } }`, `{ \"meta\": { \"$merge\": { \"seen\": true } } }` — which is applied inside the statement holding the row lock. That is the difference between a counter that is correct under concurrency and one that silently loses increments, because expressing the same change as a value means reading it first. `$inc` needs a `number` property, `$push`/`$pull` an `array`, `$merge` a `map`; anything else is a 400. See the `FieldOperation` schema.",
775
1065
  operationId: `update${schemaName}`,
776
- parameters: [{
777
- name: "id",
778
- in: "path",
779
- required: true,
780
- schema: { type: "string" },
781
- description: "Entity ID"
782
- }],
1066
+ parameters: [
1067
+ {
1068
+ name: "id",
1069
+ in: "path",
1070
+ required: true,
1071
+ schema: { type: "string" },
1072
+ description: "Entity ID"
1073
+ },
1074
+ shared.ifMatchHeader,
1075
+ shared.preferHeader,
1076
+ {
1077
+ name: "Idempotency-Key",
1078
+ in: "header",
1079
+ required: false,
1080
+ schema: { type: "string" },
1081
+ description: "Names this update so a retry replays its answer instead of applying the edit again. A PATCH is not naturally idempotent — a field operation emphatically is not — so a retry after a lost response applies it twice."
1082
+ }
1083
+ ],
783
1084
  requestBody: {
784
1085
  required: true,
785
1086
  content: { "application/json": { schema: { $ref: `#/components/schemas/${schemaName}Update` } } }
@@ -793,6 +1094,16 @@ function updateOperation(collection, schemaName, requireAuth) {
793
1094
  description: "Entity not found",
794
1095
  content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
795
1096
  },
1097
+ 409: {
1098
+ description: "A request with the same Idempotency-Key is still in flight",
1099
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
1100
+ },
1101
+ 422: {
1102
+ description: "The Idempotency-Key was already used for a different request",
1103
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
1104
+ },
1105
+ ...shared.preconditionFailed,
1106
+ ...shared.minimalResponse,
796
1107
  ...errorResponses(requireAuth)
797
1108
  }
798
1109
  };
@@ -820,12 +1131,12 @@ function buildCollectionUpdateSchema(collection) {
820
1131
  function buildCollectionInputSchema(collection) {
821
1132
  const properties = {};
822
1133
  const required = [];
823
- const excluded = excludedApiKeys(collection);
1134
+ const excluded = excludedApiKeys(collection, "write");
824
1135
  const emitted = new Set(excluded);
825
1136
  const idKey = idPropertyEntry(collection)?.[0] ?? "id";
826
1137
  for (const [key, property] of Object.entries(collection.properties)) {
827
1138
  if (property.type === "relation") continue;
828
- if (!isDocumentedProperty(property)) continue;
1139
+ if (!isDocumentedProperty(property, "write")) continue;
829
1140
  if (property.type === "date" && property.autoValue) continue;
830
1141
  if ("isId" in property && property.isId && property.isId !== "manual" && property.isId !== true) continue;
831
1142
  properties[key] = convertPropertyToSchema(property);
@@ -840,9 +1151,12 @@ function buildCollectionInputSchema(collection) {
840
1151
  emitted.add(idKey);
841
1152
  }
842
1153
  emitWritableRelations(collection, properties, emitted);
1154
+ annotateTenantField(collection, properties, "write");
1155
+ const tenantField = getTenantConfig(collection)?.field;
1156
+ const requiredOnInput = tenantField ? required.filter((key) => key !== tenantField) : required;
843
1157
  return {
844
1158
  type: "object",
845
- required: required.length > 0 ? required : void 0,
1159
+ required: requiredOnInput.length > 0 ? requiredOnInput : void 0,
846
1160
  properties
847
1161
  };
848
1162
  }
@@ -882,6 +1196,18 @@ function emitWritableRelations(collection, properties, emitted) {
882
1196
  * Convert a Rebase Property to an OpenAPI 3.0 schema object.
883
1197
  */
884
1198
  function convertPropertyToSchema(property) {
1199
+ const schema = convertPropertyTypeToSchema(property);
1200
+ const annotation = accessAnnotation(property);
1201
+ if (!annotation) return schema;
1202
+ const sentence = accessDescription(property);
1203
+ return {
1204
+ ...schema,
1205
+ ...sentence ? { description: schema.description ? `${schema.description} — ${sentence}` : sentence } : {},
1206
+ "x-rebase-access": annotation
1207
+ };
1208
+ }
1209
+ /** The JSON Schema a property's *type* compiles to, before any access annotation. */
1210
+ function convertPropertyTypeToSchema(property) {
885
1211
  const base = {};
886
1212
  if (property.name) base.description = property.name;
887
1213
  switch (property.type) {
@@ -1106,4 +1432,4 @@ function toPascalCase(str) {
1106
1432
  //#endregion
1107
1433
  export { generateOpenApiSpec };
1108
1434
 
1109
- //# sourceMappingURL=openapi-generator-BIBbO1Tq.js.map
1435
+ //# sourceMappingURL=openapi-generator-DGyLbISS.js.map