@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.
- package/dist/{GCSStorageController-ZXPoNqW3.js → GCSStorageController-CLIJXwGS.js} +2 -2
- package/dist/{GCSStorageController-ZXPoNqW3.js.map → GCSStorageController-CLIJXwGS.js.map} +1 -1
- package/dist/{S3StorageController-5pAXyv31.js → S3StorageController-Dcuf8lMA.js} +2 -2
- package/dist/{S3StorageController-5pAXyv31.js.map → S3StorageController-Dcuf8lMA.js.map} +1 -1
- package/dist/api/errors.d.ts +6 -1
- package/dist/api/rest/api-generator.d.ts +80 -0
- package/dist/api/rest/batch.d.ts +71 -0
- package/dist/api/rest/conflict-target.d.ts +20 -0
- package/dist/api/rest/etag.d.ts +55 -0
- package/dist/api/rest/field-access-query.d.ts +66 -0
- package/dist/api/rest/field-ops.d.ts +86 -0
- package/dist/api/rest/query-parser.d.ts +16 -2
- package/dist/api/rest/soft-delete-params.d.ts +37 -0
- package/dist/api/rest/write-validation.d.ts +51 -8
- package/dist/api/types.d.ts +28 -3
- package/dist/{ast-schema-editor-CNgFJ3NF.js → ast-schema-editor-C6mDz0XN.js} +2 -2
- package/dist/{ast-schema-editor-CNgFJ3NF.js.map → ast-schema-editor-C6mDz0XN.js.map} +1 -1
- package/dist/auth/jwt.d.ts +17 -0
- package/dist/auth/rls-scope.d.ts +1 -0
- package/dist/{auth-BRiOuyq8.js → auth-DJsLXsCR.js} +61 -60
- package/dist/auth-DJsLXsCR.js.map +1 -0
- package/dist/{backup-C8P6Cl3G.js → backup-DGu0v9Ku.js} +2 -2
- package/dist/{backup-C8P6Cl3G.js.map → backup-DGu0v9Ku.js.map} +1 -1
- package/dist/boot/bundle.d.ts +12 -3
- package/dist/boot/driver.d.ts +11 -4
- package/dist/boot/env.d.ts +2 -2
- package/dist/{contract-routes-CusnEB5h.js → contract-routes-eLxV0le1.js} +2 -2
- package/dist/{contract-routes-CusnEB5h.js.map → contract-routes-eLxV0le1.js.map} +1 -1
- package/dist/{cron-loader-DnmIePn_.js → cron-loader-CQjvjpEw.js} +2 -2
- package/dist/{cron-loader-DnmIePn_.js.map → cron-loader-CQjvjpEw.js.map} +1 -1
- package/dist/{cron-routes-D3x2ydMa.js → cron-routes-Bfwni8Zg.js} +3 -3
- package/dist/{cron-routes-D3x2ydMa.js.map → cron-routes-Bfwni8Zg.js.map} +1 -1
- package/dist/{cron-store-CCQXwgVL.js → cron-store-Bsiw4Q6u.js} +3 -3
- package/dist/{cron-store-CCQXwgVL.js.map → cron-store-Bsiw4Q6u.js.map} +1 -1
- package/dist/{errors-HjfaPlvY.js → errors-DMImyqyR.js} +8 -3
- package/dist/errors-DMImyqyR.js.map +1 -0
- package/dist/{function-routes-gQ0EShVG.js → function-routes-C4nB2h0z.js} +2 -2
- package/dist/{function-routes-gQ0EShVG.js.map → function-routes-C4nB2h0z.js.map} +1 -1
- package/dist/functions/index.js +7 -2
- package/dist/functions/index.js.map +1 -1
- package/dist/{history-recorder-B1FwXx9J.js → history-recorder-r5_IzSHK.js} +2 -2
- package/dist/{history-recorder-B1FwXx9J.js.map → history-recorder-r5_IzSHK.js.map} +1 -1
- package/dist/{history-store-LHXaQywp.js → history-store-D4RVK-uZ.js} +2 -2
- package/dist/{history-store-LHXaQywp.js.map → history-store-D4RVK-uZ.js.map} +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.es.js +2429 -7303
- package/dist/index.es.js.map +1 -1
- package/dist/{jobs-DkkD9mPV.js → jobs-CW5lm_Ix.js} +3 -3
- package/dist/{jobs-DkkD9mPV.js.map → jobs-CW5lm_Ix.js.map} +1 -1
- package/dist/{jwt-DkhXwMzR.js → jwt-DATvkKB_.js} +39 -3
- package/dist/{jwt-DkhXwMzR.js.map → jwt-DATvkKB_.js.map} +1 -1
- package/dist/{keys-g8lbVC_o.js → keys-Qfc4XieN.js} +2 -2
- package/dist/{keys-g8lbVC_o.js.map → keys-Qfc4XieN.js.map} +1 -1
- package/dist/{logs-routes-CbsTpozn.js → logs-routes-DnJINsMu.js} +2 -2
- package/dist/{logs-routes-CbsTpozn.js.map → logs-routes-DnJINsMu.js.map} +1 -1
- package/dist/{openapi-generator-BIBbO1Tq.js → openapi-generator-DGyLbISS.js} +374 -48
- package/dist/openapi-generator-DGyLbISS.js.map +1 -0
- package/dist/{query-parser-C68Q9EX4.js → query-parser-BQiPZrM-.js} +257 -17
- package/dist/query-parser-BQiPZrM-.js.map +1 -0
- package/dist/{request-timeout-DESvlfrS.js → request-timeout-BR-OBwES.js} +2 -2
- package/dist/{request-timeout-DESvlfrS.js.map → request-timeout-BR-OBwES.js.map} +1 -1
- package/dist/{schema-editor-routes-CcZKh50q.js → schema-editor-routes-C3TLZqAC.js} +13 -13
- package/dist/{schema-editor-routes-CcZKh50q.js.map → schema-editor-routes-C3TLZqAC.js.map} +1 -1
- package/dist/{src-DHK4fHkw.js → src-Br6ARbs6.js} +26 -2
- package/dist/src-Br6ARbs6.js.map +1 -0
- package/dist/{src-Dq-I3Ybx.js → src-DqZ9YiGA.js} +1387 -94
- package/dist/src-DqZ9YiGA.js.map +1 -0
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/property-limits.d.ts +77 -0
- package/dist/storage/routes.d.ts +13 -0
- package/dist/storage/tus-handler.d.ts +22 -1
- package/package.json +7 -6
- package/dist/auth-BRiOuyq8.js.map +0 -1
- package/dist/errors-HjfaPlvY.js.map +0 -1
- package/dist/openapi-generator-BIBbO1Tq.js.map +0 -1
- package/dist/query-parser-C68Q9EX4.js.map +0 -1
- package/dist/schemas-C3234HWE.js +0 -6827
- package/dist/schemas-C3234HWE.js.map +0 -1
- package/dist/src-DHK4fHkw.js.map +0 -1
- 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 {
|
|
6
|
-
import "./src-
|
|
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
|
|
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
|
|
91
|
-
example: "author,
|
|
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
|
|
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
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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:
|
|
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-
|
|
1435
|
+
//# sourceMappingURL=openapi-generator-DGyLbISS.js.map
|