@rebasepro/server 0.13.0 → 0.13.1-canary.g06dbe5b
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/{admin_block-CcSGdH7M.js → admin_block-H86dCPsj.js} +62 -2
- package/dist/admin_block-H86dCPsj.js.map +1 -0
- package/dist/api/ast-schema-editor.d.ts +74 -6
- package/dist/api/errors.d.ts +0 -6
- package/dist/api/openapi-generator.d.ts +14 -0
- package/dist/api/rest/api-generator.d.ts +1 -1
- package/dist/api/rest/idempotency.d.ts +62 -4
- package/dist/api/rest/query-parser.d.ts +15 -1
- package/dist/api/rest/write-validation.d.ts +52 -9
- package/dist/auth/adapter-middleware.d.ts +3 -1
- package/dist/auth/apple-oauth.d.ts +16 -11
- package/dist/auth/bitbucket-oauth.d.ts +2 -4
- package/dist/auth/builtin-auth-adapter.d.ts +2 -0
- package/dist/auth/discord-oauth.d.ts +5 -6
- package/dist/auth/facebook-oauth.d.ts +2 -4
- package/dist/auth/github-oauth.d.ts +2 -4
- package/dist/auth/gitlab-oauth.d.ts +6 -4
- package/dist/auth/google-oauth.d.ts +1 -0
- package/dist/auth/index.d.ts +8 -1
- package/dist/auth/interfaces.d.ts +68 -1
- package/dist/auth/jwt.d.ts +40 -0
- package/dist/auth/linkedin-oauth.d.ts +2 -4
- package/dist/auth/mfa-gate.d.ts +42 -0
- package/dist/auth/mfa-routes.d.ts +26 -1
- package/dist/auth/mfa.d.ts +16 -0
- package/dist/auth/microsoft-oauth.d.ts +19 -4
- package/dist/auth/oauth-code-flow.d.ts +66 -0
- package/dist/auth/oauth-signin-policy.d.ts +61 -0
- package/dist/auth/oidc-id-token.d.ts +61 -0
- package/dist/auth/rate-limiter.d.ts +37 -2
- package/dist/auth/rls-scope.d.ts +25 -0
- package/dist/auth/routes.d.ts +8 -0
- package/dist/auth/slack-oauth.d.ts +2 -4
- package/dist/auth/spotify-oauth.d.ts +2 -4
- package/dist/auth/twitter-oauth.d.ts +2 -5
- package/dist/{auth-CuC9M2x6.js → auth-DkAHqvf5.js} +1604 -391
- package/dist/auth-DkAHqvf5.js.map +1 -0
- package/dist/{backup-CVggVhR2.js → backup-DLluVyuA.js} +2 -2
- package/dist/{backup-CVggVhR2.js.map → backup-DLluVyuA.js.map} +1 -1
- package/dist/boot/boot.d.ts +19 -0
- package/dist/boot/ddl-bootstrap.d.ts +70 -0
- package/dist/{contract-routes-Dj8i5AiM.js → contract-routes-BB1U05sS.js} +3 -3
- package/dist/{contract-routes-Dj8i5AiM.js.map → contract-routes-BB1U05sS.js.map} +1 -1
- package/dist/cron/cron-scheduler.d.ts +8 -3
- package/dist/cron/define-cron.d.ts +17 -3
- package/dist/{cron-loader-B1S2MCSl.js → cron-loader-3U5aGILy.js} +2 -2
- package/dist/{cron-loader-B1S2MCSl.js.map → cron-loader-3U5aGILy.js.map} +1 -1
- package/dist/{cron-routes-CrQ0tK-_.js → cron-routes-rZgwlOz4.js} +2 -2
- package/dist/{cron-routes-CrQ0tK-_.js.map → cron-routes-rZgwlOz4.js.map} +1 -1
- package/dist/{cron-scheduler-B3RFt0HS.js → cron-scheduler-FJAaCAXm.js} +30 -5
- package/dist/cron-scheduler-FJAaCAXm.js.map +1 -0
- package/dist/{cron-store-BywZsyfZ.js → cron-store-9NmUDfzL.js} +66 -47
- package/dist/cron-store-9NmUDfzL.js.map +1 -0
- package/dist/ddl-bootstrap-BhXbTnBl.js +183 -0
- package/dist/ddl-bootstrap-BhXbTnBl.js.map +1 -0
- package/dist/email/html.d.ts +54 -0
- package/dist/email/index.d.ts +3 -0
- package/dist/email/link-base.d.ts +39 -0
- package/dist/email/smtp-email-service.d.ts +7 -1
- package/dist/email/templates.d.ts +9 -1
- package/dist/email/types.d.ts +11 -1
- package/dist/{errors-CgkCzoj7.js → errors-B1WZEdsK.js} +3 -11
- package/dist/errors-B1WZEdsK.js.map +1 -0
- package/dist/function-loader-D1SwtCa5.js +139 -0
- package/dist/function-loader-D1SwtCa5.js.map +1 -0
- package/dist/{function-routes-C0cLIy3N.js → function-routes-Btcez1T-.js} +18 -6
- package/dist/function-routes-Btcez1T-.js.map +1 -0
- package/dist/functions/define-function.d.ts +25 -6
- package/dist/functions/function-loader.d.ts +23 -0
- package/dist/functions/function-routes.d.ts +7 -1
- package/dist/functions/request-timeout.d.ts +34 -0
- package/dist/history/history-routes.d.ts +6 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.es.js +1960 -794
- package/dist/index.es.js.map +1 -1
- package/dist/init/docs.d.ts +10 -1
- package/dist/init/process-safety.d.ts +25 -0
- package/dist/init.d.ts +27 -0
- package/dist/{jwt-DD6EtpGj.js → jwt-Cuq6MwNy.js} +70 -8
- package/dist/jwt-Cuq6MwNy.js.map +1 -0
- package/dist/logger-DfvF_8r-.js +190 -0
- package/dist/logger-DfvF_8r-.js.map +1 -0
- package/dist/openapi-generator-BQxxxDpb.js +865 -0
- package/dist/openapi-generator-BQxxxDpb.js.map +1 -0
- package/dist/request-timeout-RivJsME0.js +65 -0
- package/dist/request-timeout-RivJsME0.js.map +1 -0
- package/dist/schema-editor-routes-DZFKGXgq.js +437 -0
- package/dist/schema-editor-routes-DZFKGXgq.js.map +1 -0
- package/dist/serve-spa.d.ts +5 -4
- package/dist/services/outbound-url-guard.d.ts +53 -0
- package/dist/services/routed-realtime-service.d.ts +10 -2
- package/dist/services/webhook-service.d.ts +76 -1
- package/dist/singleton.d.ts +25 -8
- package/dist/{src-Cum9kox5.js → src-By48Ffg0.js} +322 -70
- package/dist/src-By48Ffg0.js.map +1 -0
- package/dist/{src-_qQ3RNCK.js → src-Ca6NhxKs.js} +79 -2
- package/dist/src-Ca6NhxKs.js.map +1 -0
- package/dist/storage/LocalStorageController.d.ts +12 -1
- package/dist/storage/image-transform.d.ts +57 -0
- package/dist/storage/keys.d.ts +98 -0
- package/dist/storage/routes.d.ts +11 -0
- package/dist/storage/tus-handler.d.ts +7 -1
- package/dist/utils/logger.d.ts +18 -0
- package/dist/utils/logging.d.ts +0 -4
- package/dist/utils/sql.d.ts +11 -6
- package/package.json +7 -6
- package/dist/admin_block-CcSGdH7M.js.map +0 -1
- package/dist/auth-CuC9M2x6.js.map +0 -1
- package/dist/backend-CIxN4FVm.js +0 -15
- package/dist/backend-CIxN4FVm.js.map +0 -1
- package/dist/cron-scheduler-B3RFt0HS.js.map +0 -1
- package/dist/cron-store-BywZsyfZ.js.map +0 -1
- package/dist/errors-CgkCzoj7.js.map +0 -1
- package/dist/function-loader-B_1fYfUY.js +0 -86
- package/dist/function-loader-B_1fYfUY.js.map +0 -1
- package/dist/function-routes-C0cLIy3N.js.map +0 -1
- package/dist/jwt-DD6EtpGj.js.map +0 -1
- package/dist/logger-BYU66ENZ.js +0 -94
- package/dist/logger-BYU66ENZ.js.map +0 -1
- package/dist/openapi-generator-BEwyiaAr.js +0 -597
- package/dist/openapi-generator-BEwyiaAr.js.map +0 -1
- package/dist/schema-editor-routes-CbF20Mf1.js +0 -248
- package/dist/schema-editor-routes-CbF20Mf1.js.map +0 -1
- package/dist/src-Cum9kox5.js.map +0 -1
- package/dist/src-_qQ3RNCK.js.map +0 -1
|
@@ -0,0 +1,865 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from "module";
|
|
2
|
+
import "process";
|
|
3
|
+
__createRequire(import.meta.url);
|
|
4
|
+
import { h as resolveCollectionRelations } from "./src-By48Ffg0.js";
|
|
5
|
+
import "./src-Ca6NhxKs.js";
|
|
6
|
+
//#region ../types/src/types/relations.ts
|
|
7
|
+
/** @group Models */
|
|
8
|
+
function isToMany(relation) {
|
|
9
|
+
return relation.cardinality === "many";
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/api/openapi-generator.ts
|
|
13
|
+
function generateOpenApiSpec(collections, options = {}) {
|
|
14
|
+
const basePath = options.basePath ?? "/api";
|
|
15
|
+
const requireAuth = options.requireAuth ?? true;
|
|
16
|
+
const defaultLimit = options.listLimits?.defaultLimit ?? 50;
|
|
17
|
+
const maxLimit = options.listLimits?.maxLimit ?? 1e3;
|
|
18
|
+
/**
|
|
19
|
+
* The query parameters every list endpoint honours.
|
|
20
|
+
*
|
|
21
|
+
* Written once because it was written twice: the root listing named eight
|
|
22
|
+
* and the subcollection listing named four, though both go through the same
|
|
23
|
+
* `parseQueryOptions` and the same fetch. Four capabilities were therefore
|
|
24
|
+
* unreachable from a generated client on nested routes, and `or`/`and` were
|
|
25
|
+
* undocumented on both.
|
|
26
|
+
*/
|
|
27
|
+
const listQueryParameters = () => [
|
|
28
|
+
{
|
|
29
|
+
name: "limit",
|
|
30
|
+
in: "query",
|
|
31
|
+
schema: {
|
|
32
|
+
type: "integer",
|
|
33
|
+
default: defaultLimit,
|
|
34
|
+
minimum: 1,
|
|
35
|
+
maximum: maxLimit
|
|
36
|
+
},
|
|
37
|
+
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\`.`
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "offset",
|
|
41
|
+
in: "query",
|
|
42
|
+
schema: {
|
|
43
|
+
type: "integer",
|
|
44
|
+
default: 0
|
|
45
|
+
},
|
|
46
|
+
description: "Number of records to skip"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "page",
|
|
50
|
+
in: "query",
|
|
51
|
+
schema: {
|
|
52
|
+
type: "integer",
|
|
53
|
+
minimum: 1
|
|
54
|
+
},
|
|
55
|
+
description: "Page number (alternative to offset). Calculates offset as (page-1)*limit"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "orderBy",
|
|
59
|
+
in: "query",
|
|
60
|
+
schema: { type: "string" },
|
|
61
|
+
description: "Sort field and direction. Accepts `field:asc` or `field:desc`, or a JSON array `[{\"field\":\"name\",\"direction\":\"asc\"}]`",
|
|
62
|
+
example: "created_at:desc"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "where",
|
|
66
|
+
in: "query",
|
|
67
|
+
schema: { type: "string" },
|
|
68
|
+
description: "JSON object filter, mapping each field to a `[operator, value]` tuple. Combines with the per-field `?field=op.value` parameters below; on the same field, the per-field parameter wins.",
|
|
69
|
+
example: "{\"status\":[\"==\",\"active\"]}"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "or",
|
|
73
|
+
in: "query",
|
|
74
|
+
schema: { type: "string" },
|
|
75
|
+
description: "Disjunction of conditions, AND-ed with `where` and `searchString`.",
|
|
76
|
+
example: "(status.eq.draft,status.eq.review)"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "and",
|
|
80
|
+
in: "query",
|
|
81
|
+
schema: { type: "string" },
|
|
82
|
+
description: "Conjunction of conditions, AND-ed with `where` and `searchString`. Ignored when `or` is also present.",
|
|
83
|
+
example: "(views.gte.10,status.eq.draft)"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "include",
|
|
87
|
+
in: "query",
|
|
88
|
+
schema: { type: "string" },
|
|
89
|
+
description: "Comma-separated list of relations to include (eager-load). Use `*` for all relations.",
|
|
90
|
+
example: "author,tags"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "fields",
|
|
94
|
+
in: "query",
|
|
95
|
+
schema: { type: "string" },
|
|
96
|
+
description: "Comma-separated list of fields to return (field selection)",
|
|
97
|
+
example: "id,name,created_at"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "searchString",
|
|
101
|
+
in: "query",
|
|
102
|
+
schema: { type: "string" },
|
|
103
|
+
description: "Text search. By default a case-insensitive substring match OR-ed across the collection's top-level string properties. A collection declaring a `search` block gets ranked full-text matching over the fields it names, and rows carry a `_score`."
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "vector_search",
|
|
107
|
+
in: "query",
|
|
108
|
+
schema: { type: "string" },
|
|
109
|
+
description: "Name of the `vector` property to run a nearest-neighbour search against. Requires `vector`.",
|
|
110
|
+
example: "embedding"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "vector",
|
|
114
|
+
in: "query",
|
|
115
|
+
schema: { type: "string" },
|
|
116
|
+
description: "The query embedding, as a JSON array of numbers. Its length must match the property's declared `dimensions`.",
|
|
117
|
+
example: "[0.12,-0.04,0.98]"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "vector_distance",
|
|
121
|
+
in: "query",
|
|
122
|
+
schema: {
|
|
123
|
+
type: "string",
|
|
124
|
+
enum: [
|
|
125
|
+
"cosine",
|
|
126
|
+
"l2",
|
|
127
|
+
"inner_product"
|
|
128
|
+
],
|
|
129
|
+
default: "cosine"
|
|
130
|
+
},
|
|
131
|
+
description: "Distance function used for ordering."
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: "vector_threshold",
|
|
135
|
+
in: "query",
|
|
136
|
+
schema: { type: "number" },
|
|
137
|
+
description: "Drop rows farther than this distance. Rows are returned closest-first with a `_distance` field."
|
|
138
|
+
}
|
|
139
|
+
];
|
|
140
|
+
const spec = {
|
|
141
|
+
openapi: "3.0.3",
|
|
142
|
+
info: {
|
|
143
|
+
title: "Rebase API",
|
|
144
|
+
version: "1.0.0",
|
|
145
|
+
description: "Auto-generated REST API from Rebase collection definitions. This is the same API consumed by the `@rebasepro/client` SDK."
|
|
146
|
+
},
|
|
147
|
+
servers: [{
|
|
148
|
+
url: basePath,
|
|
149
|
+
description: "API Server"
|
|
150
|
+
}],
|
|
151
|
+
paths: {},
|
|
152
|
+
components: {
|
|
153
|
+
schemas: {
|
|
154
|
+
ErrorResponse: {
|
|
155
|
+
type: "object",
|
|
156
|
+
properties: { error: {
|
|
157
|
+
type: "object",
|
|
158
|
+
required: ["message", "code"],
|
|
159
|
+
properties: {
|
|
160
|
+
message: { type: "string" },
|
|
161
|
+
code: { type: "string" },
|
|
162
|
+
details: {}
|
|
163
|
+
}
|
|
164
|
+
} }
|
|
165
|
+
},
|
|
166
|
+
PaginationMeta: {
|
|
167
|
+
type: "object",
|
|
168
|
+
properties: {
|
|
169
|
+
total: {
|
|
170
|
+
type: "integer",
|
|
171
|
+
description: "Total number of matching records"
|
|
172
|
+
},
|
|
173
|
+
limit: {
|
|
174
|
+
type: "integer",
|
|
175
|
+
description: "Page size used for this query"
|
|
176
|
+
},
|
|
177
|
+
offset: {
|
|
178
|
+
type: "integer",
|
|
179
|
+
description: "Number of records skipped"
|
|
180
|
+
},
|
|
181
|
+
hasMore: {
|
|
182
|
+
type: "boolean",
|
|
183
|
+
description: "Whether more records exist beyond this page"
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
securitySchemes: {}
|
|
189
|
+
},
|
|
190
|
+
tags: []
|
|
191
|
+
};
|
|
192
|
+
if (requireAuth) {
|
|
193
|
+
spec.components.securitySchemes = { bearerAuth: {
|
|
194
|
+
type: "http",
|
|
195
|
+
scheme: "bearer",
|
|
196
|
+
bearerFormat: "JWT",
|
|
197
|
+
description: "JWT access token obtained from `POST /auth/login` or `POST /auth/register`. Can also be a static service key for server-to-server authentication."
|
|
198
|
+
} };
|
|
199
|
+
spec.security = [{ bearerAuth: [] }];
|
|
200
|
+
}
|
|
201
|
+
const paths = spec.paths;
|
|
202
|
+
const schemas = spec.components.schemas;
|
|
203
|
+
const tags = spec.tags;
|
|
204
|
+
const reservedParameterNames = new Set(listQueryParameters().map((p) => p.name));
|
|
205
|
+
for (const collection of collections || []) {
|
|
206
|
+
const schemaName = schemaNameFor(collection);
|
|
207
|
+
const slug = collection.slug;
|
|
208
|
+
tags.push({
|
|
209
|
+
name: collection.name,
|
|
210
|
+
description: collection.description || `CRUD operations for ${collection.name}`
|
|
211
|
+
});
|
|
212
|
+
schemas[schemaName] = buildCollectionSchema(collection);
|
|
213
|
+
schemas[`${schemaName}Input`] = buildCollectionInputSchema(collection);
|
|
214
|
+
schemas[`${schemaName}Update`] = buildCollectionUpdateSchema(collection);
|
|
215
|
+
const dataPath = `/data/${slug}`;
|
|
216
|
+
paths[dataPath] = {
|
|
217
|
+
get: {
|
|
218
|
+
tags: [collection.name],
|
|
219
|
+
summary: `List ${collection.name}`,
|
|
220
|
+
operationId: `list${schemaName}`,
|
|
221
|
+
parameters: [...listQueryParameters(), ...buildFilterParameters(collection, reservedParameterNames)],
|
|
222
|
+
responses: {
|
|
223
|
+
200: {
|
|
224
|
+
description: "Paginated list of entities",
|
|
225
|
+
content: { "application/json": { schema: {
|
|
226
|
+
type: "object",
|
|
227
|
+
properties: {
|
|
228
|
+
data: {
|
|
229
|
+
type: "array",
|
|
230
|
+
items: { $ref: `#/components/schemas/${schemaName}` }
|
|
231
|
+
},
|
|
232
|
+
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
233
|
+
}
|
|
234
|
+
} } }
|
|
235
|
+
},
|
|
236
|
+
...errorResponses(requireAuth)
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
post: {
|
|
240
|
+
tags: [collection.name],
|
|
241
|
+
summary: `Create ${collection.singularName || collection.name}`,
|
|
242
|
+
operationId: `create${schemaName}`,
|
|
243
|
+
requestBody: {
|
|
244
|
+
required: true,
|
|
245
|
+
content: { "application/json": { schema: { $ref: `#/components/schemas/${schemaName}Input` } } }
|
|
246
|
+
},
|
|
247
|
+
responses: {
|
|
248
|
+
201: {
|
|
249
|
+
description: "Created entity",
|
|
250
|
+
content: { "application/json": { schema: { $ref: `#/components/schemas/${schemaName}` } } }
|
|
251
|
+
},
|
|
252
|
+
...errorResponses(requireAuth)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
const idempotencyHeader = {
|
|
257
|
+
name: "Idempotency-Key",
|
|
258
|
+
in: "header",
|
|
259
|
+
required: false,
|
|
260
|
+
schema: { type: "string" },
|
|
261
|
+
description: "Names this write so a retry is recognised instead of repeated. Without it a client that lost the response cannot distinguish a replay from a second genuine batch, and the whole batch is written twice. A key names one request: re-send the identical request to replay its answer, and use a new key for a different one."
|
|
262
|
+
};
|
|
263
|
+
const bulkErrors = {
|
|
264
|
+
400: {
|
|
265
|
+
description: "Malformed body, an unknown field, or more rows than the per-batch limit",
|
|
266
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
|
|
267
|
+
},
|
|
268
|
+
409: {
|
|
269
|
+
description: "A request with the same Idempotency-Key is still in flight. Retry it: the first attempt's result is replayed once it lands",
|
|
270
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
|
|
271
|
+
},
|
|
272
|
+
422: {
|
|
273
|
+
description: "The Idempotency-Key was already used for a different request",
|
|
274
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
|
|
275
|
+
},
|
|
276
|
+
...errorResponses(requireAuth)
|
|
277
|
+
};
|
|
278
|
+
paths[`/data/${slug}/bulk`] = {
|
|
279
|
+
post: {
|
|
280
|
+
tags: [collection.name],
|
|
281
|
+
summary: `Create many ${collection.name} in one transaction`,
|
|
282
|
+
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.",
|
|
283
|
+
operationId: `createMany${schemaName}`,
|
|
284
|
+
parameters: [idempotencyHeader],
|
|
285
|
+
requestBody: {
|
|
286
|
+
required: true,
|
|
287
|
+
content: { "application/json": { schema: {
|
|
288
|
+
type: "object",
|
|
289
|
+
required: ["rows"],
|
|
290
|
+
properties: {
|
|
291
|
+
rows: {
|
|
292
|
+
type: "array",
|
|
293
|
+
items: { $ref: `#/components/schemas/${schemaName}Input` }
|
|
294
|
+
},
|
|
295
|
+
upsert: {
|
|
296
|
+
type: "boolean",
|
|
297
|
+
description: "Write each row as INSERT ... ON CONFLICT DO UPDATE on the primary key."
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
} } }
|
|
301
|
+
},
|
|
302
|
+
responses: {
|
|
303
|
+
200: {
|
|
304
|
+
description: "The written rows, in the order given",
|
|
305
|
+
content: { "application/json": { schema: {
|
|
306
|
+
type: "object",
|
|
307
|
+
properties: {
|
|
308
|
+
data: {
|
|
309
|
+
type: "array",
|
|
310
|
+
items: { $ref: `#/components/schemas/${schemaName}` }
|
|
311
|
+
},
|
|
312
|
+
meta: {
|
|
313
|
+
type: "object",
|
|
314
|
+
properties: { written: { type: "integer" } }
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
} } }
|
|
318
|
+
},
|
|
319
|
+
...bulkErrors
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
patch: {
|
|
323
|
+
tags: [collection.name],
|
|
324
|
+
summary: `Update many ${collection.name} in one transaction`,
|
|
325
|
+
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.",
|
|
326
|
+
operationId: `updateMany${schemaName}`,
|
|
327
|
+
parameters: [idempotencyHeader],
|
|
328
|
+
requestBody: {
|
|
329
|
+
required: true,
|
|
330
|
+
content: { "application/json": { schema: {
|
|
331
|
+
type: "object",
|
|
332
|
+
required: ["updates"],
|
|
333
|
+
properties: { updates: {
|
|
334
|
+
type: "array",
|
|
335
|
+
items: {
|
|
336
|
+
type: "object",
|
|
337
|
+
required: ["id", "data"],
|
|
338
|
+
properties: {
|
|
339
|
+
id: {
|
|
340
|
+
type: "string",
|
|
341
|
+
description: "The row to update"
|
|
342
|
+
},
|
|
343
|
+
data: { $ref: `#/components/schemas/${schemaName}Update` }
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
} }
|
|
347
|
+
} } }
|
|
348
|
+
},
|
|
349
|
+
responses: {
|
|
350
|
+
200: {
|
|
351
|
+
description: "The updated rows, in the order given",
|
|
352
|
+
content: { "application/json": { schema: {
|
|
353
|
+
type: "object",
|
|
354
|
+
properties: {
|
|
355
|
+
data: {
|
|
356
|
+
type: "array",
|
|
357
|
+
items: { $ref: `#/components/schemas/${schemaName}` }
|
|
358
|
+
},
|
|
359
|
+
meta: {
|
|
360
|
+
type: "object",
|
|
361
|
+
properties: { written: { type: "integer" } }
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
} } }
|
|
365
|
+
},
|
|
366
|
+
404: {
|
|
367
|
+
description: "One of the ids matches no row; nothing was written",
|
|
368
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
|
|
369
|
+
},
|
|
370
|
+
...bulkErrors
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
paths[`/data/${slug}/bulk/delete`] = { post: {
|
|
375
|
+
tags: [collection.name],
|
|
376
|
+
summary: `Delete many ${collection.name} in one transaction`,
|
|
377
|
+
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.",
|
|
378
|
+
operationId: `deleteMany${schemaName}`,
|
|
379
|
+
parameters: [idempotencyHeader],
|
|
380
|
+
requestBody: {
|
|
381
|
+
required: true,
|
|
382
|
+
content: { "application/json": { schema: {
|
|
383
|
+
type: "object",
|
|
384
|
+
required: ["ids"],
|
|
385
|
+
properties: { ids: {
|
|
386
|
+
type: "array",
|
|
387
|
+
items: { oneOf: [{ type: "string" }, { type: "integer" }] }
|
|
388
|
+
} }
|
|
389
|
+
} } }
|
|
390
|
+
},
|
|
391
|
+
responses: {
|
|
392
|
+
200: {
|
|
393
|
+
description: "How many rows were deleted",
|
|
394
|
+
content: { "application/json": { schema: {
|
|
395
|
+
type: "object",
|
|
396
|
+
properties: { meta: {
|
|
397
|
+
type: "object",
|
|
398
|
+
properties: { deleted: { type: "integer" } }
|
|
399
|
+
} }
|
|
400
|
+
} } }
|
|
401
|
+
},
|
|
402
|
+
404: {
|
|
403
|
+
description: "One of the ids matches no row; nothing was deleted",
|
|
404
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
|
|
405
|
+
},
|
|
406
|
+
...bulkErrors
|
|
407
|
+
}
|
|
408
|
+
} };
|
|
409
|
+
const entityPath = `/data/${slug}/{id}`;
|
|
410
|
+
paths[entityPath] = {
|
|
411
|
+
get: {
|
|
412
|
+
tags: [collection.name],
|
|
413
|
+
summary: `Get ${collection.singularName || collection.name} by ID`,
|
|
414
|
+
operationId: `get${schemaName}ById`,
|
|
415
|
+
parameters: [{
|
|
416
|
+
name: "id",
|
|
417
|
+
in: "path",
|
|
418
|
+
required: true,
|
|
419
|
+
schema: { type: "string" },
|
|
420
|
+
description: "Entity ID"
|
|
421
|
+
}, {
|
|
422
|
+
name: "include",
|
|
423
|
+
in: "query",
|
|
424
|
+
schema: { type: "string" },
|
|
425
|
+
description: "Comma-separated list of relations to include",
|
|
426
|
+
example: "author,tags"
|
|
427
|
+
}],
|
|
428
|
+
responses: {
|
|
429
|
+
200: {
|
|
430
|
+
description: "Entity found",
|
|
431
|
+
content: { "application/json": { schema: { $ref: `#/components/schemas/${schemaName}` } } }
|
|
432
|
+
},
|
|
433
|
+
404: {
|
|
434
|
+
description: "Entity not found",
|
|
435
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
|
|
436
|
+
},
|
|
437
|
+
...errorResponses(requireAuth)
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
patch: updateOperation(collection, schemaName, requireAuth),
|
|
441
|
+
put: {
|
|
442
|
+
...updateOperation(collection, schemaName, requireAuth),
|
|
443
|
+
operationId: `update${schemaName}ViaPut`,
|
|
444
|
+
deprecated: true
|
|
445
|
+
},
|
|
446
|
+
delete: {
|
|
447
|
+
tags: [collection.name],
|
|
448
|
+
summary: `Delete ${collection.singularName || collection.name}`,
|
|
449
|
+
operationId: `delete${schemaName}`,
|
|
450
|
+
parameters: [{
|
|
451
|
+
name: "id",
|
|
452
|
+
in: "path",
|
|
453
|
+
required: true,
|
|
454
|
+
schema: { type: "string" },
|
|
455
|
+
description: "Entity ID"
|
|
456
|
+
}],
|
|
457
|
+
responses: {
|
|
458
|
+
204: { description: "Deleted successfully" },
|
|
459
|
+
404: {
|
|
460
|
+
description: "Entity not found",
|
|
461
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
|
|
462
|
+
},
|
|
463
|
+
...errorResponses(requireAuth)
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
for (const collection of collections || []) {
|
|
469
|
+
const slug = collection.slug;
|
|
470
|
+
const schemaName = schemaNameFor(collection);
|
|
471
|
+
const relations = Object.values(resolveCollectionRelations(collection)).filter(isToMany);
|
|
472
|
+
for (const relation of relations) {
|
|
473
|
+
const relationName = relation.relationName;
|
|
474
|
+
const targetSchema = schemaNameFor(relation.target());
|
|
475
|
+
const subPath = `/data/${slug}/{parentId}/${relationName}`;
|
|
476
|
+
paths[subPath] = { get: {
|
|
477
|
+
tags: [collection.name],
|
|
478
|
+
summary: `List ${relationName} for ${withIndefiniteArticle(collection.singularName || collection.name)}`,
|
|
479
|
+
operationId: `list${schemaName}${toPascalCase(relationName)}`,
|
|
480
|
+
parameters: [{
|
|
481
|
+
name: "parentId",
|
|
482
|
+
in: "path",
|
|
483
|
+
required: true,
|
|
484
|
+
schema: { type: "string" },
|
|
485
|
+
description: `${collection.singularName || collection.name} ID`
|
|
486
|
+
}, ...listQueryParameters()],
|
|
487
|
+
responses: {
|
|
488
|
+
200: {
|
|
489
|
+
description: `List of related ${relationName}`,
|
|
490
|
+
content: { "application/json": { schema: {
|
|
491
|
+
type: "object",
|
|
492
|
+
properties: {
|
|
493
|
+
data: {
|
|
494
|
+
type: "array",
|
|
495
|
+
items: schemas[targetSchema] ? { $ref: `#/components/schemas/${targetSchema}` } : { type: "object" }
|
|
496
|
+
},
|
|
497
|
+
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
498
|
+
}
|
|
499
|
+
} } }
|
|
500
|
+
},
|
|
501
|
+
...errorResponses(requireAuth)
|
|
502
|
+
}
|
|
503
|
+
} };
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return spec;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Is this property part of the shape the document may describe?
|
|
510
|
+
*
|
|
511
|
+
* Two exclusions, and both are the document telling the truth about what the
|
|
512
|
+
* server does:
|
|
513
|
+
*
|
|
514
|
+
* - a `relation` property is virtual: the row carries the owning side's foreign
|
|
515
|
+
* key column, never the property itself, so it is not a field of any payload.
|
|
516
|
+
* (The FK column *is* readable, writable and filterable and is still absent
|
|
517
|
+
* from every schema here — a separate gap, not this rule.)
|
|
518
|
+
* - `excludeFromApi` is a server-side guarantee that the column "is stripped
|
|
519
|
+
* from every row the API serves, for every caller, including admins and
|
|
520
|
+
* service keys" — `stripExcluded` in the row pipeline enforces it. A schema
|
|
521
|
+
* that lists such a column describes a field that is never present, and it
|
|
522
|
+
* describes it to everyone: `/docs` is mounted on the app, not on the data
|
|
523
|
+
* router, so it carries none of the auth middleware `{basePath}/data` does.
|
|
524
|
+
* Every project scaffolded by `rebase init` published its `users`
|
|
525
|
+
* collection's `passwordHash` and `emailVerificationToken` this way.
|
|
526
|
+
*
|
|
527
|
+
* Written as one predicate rather than three `continue`s because it kept being
|
|
528
|
+
* fixed in one loop at a time: this is the same rule the SDK generator applies
|
|
529
|
+
* to its `Row` type (`packages/codegen/src/generate-types.ts`).
|
|
530
|
+
*/
|
|
531
|
+
function isDocumentedProperty(property) {
|
|
532
|
+
return property.type !== "relation" && !property.excludeFromApi;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Build the component schema for a collection (output / read shape).
|
|
536
|
+
*
|
|
537
|
+
* Every declared property except the ones {@link isDocumentedProperty} rules
|
|
538
|
+
* out.
|
|
539
|
+
*/
|
|
540
|
+
function buildCollectionSchema(collection) {
|
|
541
|
+
const properties = { id: {
|
|
542
|
+
type: "string",
|
|
543
|
+
description: "Unique identifier"
|
|
544
|
+
} };
|
|
545
|
+
const required = ["id"];
|
|
546
|
+
for (const [key, property] of Object.entries(collection.properties)) {
|
|
547
|
+
if (!isDocumentedProperty(property)) continue;
|
|
548
|
+
properties[key] = convertPropertyToSchema(property);
|
|
549
|
+
if (property.validation?.required) required.push(key);
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
type: "object",
|
|
553
|
+
required: required.length > 0 ? required : void 0,
|
|
554
|
+
properties
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* The PATCH/PUT operation for `/data/{slug}/{id}`.
|
|
559
|
+
*
|
|
560
|
+
* Split out because both verbs serve it and they must not drift: the update
|
|
561
|
+
* body is a **partial**, and describing it with the create schema was the bug
|
|
562
|
+
* this replaces. `<Name>Input` marks every `validation.required` property as
|
|
563
|
+
* required — correct for POST, wrong for an update, where omitting a field
|
|
564
|
+
* means "leave it alone" rather than "I forgot it". A client generated from
|
|
565
|
+
* that spec demanded fields the server does not, and a spec-validating gateway
|
|
566
|
+
* would have rejected partial updates the server accepts.
|
|
567
|
+
*/
|
|
568
|
+
function updateOperation(collection, schemaName, requireAuth) {
|
|
569
|
+
return {
|
|
570
|
+
tags: [collection.name],
|
|
571
|
+
summary: `Update ${collection.singularName || collection.name}`,
|
|
572
|
+
description: "Partial update: only the properties present in the body are written; the rest are left unchanged.",
|
|
573
|
+
operationId: `update${schemaName}`,
|
|
574
|
+
parameters: [{
|
|
575
|
+
name: "id",
|
|
576
|
+
in: "path",
|
|
577
|
+
required: true,
|
|
578
|
+
schema: { type: "string" },
|
|
579
|
+
description: "Entity ID"
|
|
580
|
+
}],
|
|
581
|
+
requestBody: {
|
|
582
|
+
required: true,
|
|
583
|
+
content: { "application/json": { schema: { $ref: `#/components/schemas/${schemaName}Update` } } }
|
|
584
|
+
},
|
|
585
|
+
responses: {
|
|
586
|
+
200: {
|
|
587
|
+
description: "Updated entity",
|
|
588
|
+
content: { "application/json": { schema: { $ref: `#/components/schemas/${schemaName}` } } }
|
|
589
|
+
},
|
|
590
|
+
404: {
|
|
591
|
+
description: "Entity not found",
|
|
592
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
|
|
593
|
+
},
|
|
594
|
+
...errorResponses(requireAuth)
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* The update body: the create schema with `required` dropped.
|
|
600
|
+
*
|
|
601
|
+
* Derived rather than rebuilt so the two cannot describe different columns —
|
|
602
|
+
* the only difference between creating and updating is which fields you must
|
|
603
|
+
* supply, and that is exactly the one thing removed here.
|
|
604
|
+
*/
|
|
605
|
+
function buildCollectionUpdateSchema(collection) {
|
|
606
|
+
const { required: _required, ...rest } = buildCollectionInputSchema(collection);
|
|
607
|
+
return rest;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Build an input schema (for POST/PUT) — excludes auto-generated fields.
|
|
611
|
+
*
|
|
612
|
+
* `excludeFromApi` columns are left out too, though a write naming one is still
|
|
613
|
+
* accepted: a create the document invites and whose result it then cannot show
|
|
614
|
+
* is not a round trip anyone can verify, and naming the column here discloses
|
|
615
|
+
* it just as loudly as the read schema does — this document is served
|
|
616
|
+
* unauthenticated.
|
|
617
|
+
*/
|
|
618
|
+
function buildCollectionInputSchema(collection) {
|
|
619
|
+
const properties = {};
|
|
620
|
+
const required = [];
|
|
621
|
+
for (const [key, property] of Object.entries(collection.properties)) {
|
|
622
|
+
if (!isDocumentedProperty(property)) continue;
|
|
623
|
+
if (property.type === "date" && property.autoValue) continue;
|
|
624
|
+
if ("isId" in property && property.isId && property.isId !== "manual" && property.isId !== true) continue;
|
|
625
|
+
properties[key] = convertPropertyToSchema(property);
|
|
626
|
+
if (property.validation?.required) required.push(key);
|
|
627
|
+
}
|
|
628
|
+
properties["id"] = {
|
|
629
|
+
type: "string",
|
|
630
|
+
description: "Optional: client-assigned ID. If omitted, the server generates one."
|
|
631
|
+
};
|
|
632
|
+
return {
|
|
633
|
+
type: "object",
|
|
634
|
+
required: required.length > 0 ? required : void 0,
|
|
635
|
+
properties
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Convert a Rebase Property to an OpenAPI 3.0 schema object.
|
|
640
|
+
*/
|
|
641
|
+
function convertPropertyToSchema(property) {
|
|
642
|
+
const base = {};
|
|
643
|
+
if (property.name) base.description = property.name;
|
|
644
|
+
switch (property.type) {
|
|
645
|
+
case "string": {
|
|
646
|
+
const sp = property;
|
|
647
|
+
base.type = "string";
|
|
648
|
+
if (sp.enum) {
|
|
649
|
+
const enumValues = resolveEnumValues(sp.enum);
|
|
650
|
+
if (enumValues.length > 0) base.enum = enumValues;
|
|
651
|
+
}
|
|
652
|
+
if (sp.validation) {
|
|
653
|
+
if (sp.validation.min !== void 0) base.minLength = sp.validation.min;
|
|
654
|
+
if (sp.validation.max !== void 0) base.maxLength = sp.validation.max;
|
|
655
|
+
if (sp.validation.length !== void 0) {
|
|
656
|
+
base.minLength = sp.validation.length;
|
|
657
|
+
base.maxLength = sp.validation.length;
|
|
658
|
+
}
|
|
659
|
+
if (sp.validation.matches !== void 0) base.pattern = String(sp.validation.matches);
|
|
660
|
+
}
|
|
661
|
+
if (sp.email) base.format = "email";
|
|
662
|
+
if (sp.url) base.format = "uri";
|
|
663
|
+
if (sp.storage) base.format = "uri";
|
|
664
|
+
return base;
|
|
665
|
+
}
|
|
666
|
+
case "number": {
|
|
667
|
+
const np = property;
|
|
668
|
+
base.type = np.validation?.integer || np.columnType === "integer" || np.columnType === "serial" || np.columnType === "bigserial" || np.columnType === "bigint" ? "integer" : "number";
|
|
669
|
+
if (np.enum) {
|
|
670
|
+
const enumValues = resolveEnumValues(np.enum);
|
|
671
|
+
if (enumValues.length > 0) base.enum = enumValues;
|
|
672
|
+
}
|
|
673
|
+
if (np.validation) {
|
|
674
|
+
if (np.validation.min !== void 0) base.minimum = np.validation.min;
|
|
675
|
+
if (np.validation.max !== void 0) base.maximum = np.validation.max;
|
|
676
|
+
if (np.validation.moreThan !== void 0) {
|
|
677
|
+
base.minimum = np.validation.moreThan;
|
|
678
|
+
base.exclusiveMinimum = true;
|
|
679
|
+
}
|
|
680
|
+
if (np.validation.lessThan !== void 0) {
|
|
681
|
+
base.maximum = np.validation.lessThan;
|
|
682
|
+
base.exclusiveMaximum = true;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return base;
|
|
686
|
+
}
|
|
687
|
+
case "boolean":
|
|
688
|
+
base.type = "boolean";
|
|
689
|
+
return base;
|
|
690
|
+
case "date":
|
|
691
|
+
base.type = "string";
|
|
692
|
+
if (property.mode === "date") base.format = "date";
|
|
693
|
+
else base.format = "date-time";
|
|
694
|
+
if (property.autoValue) {
|
|
695
|
+
base.readOnly = true;
|
|
696
|
+
base.description = (base.description || "") + (property.autoValue === "on_create" ? " (Auto-set on creation)" : " (Auto-updated)");
|
|
697
|
+
}
|
|
698
|
+
return base;
|
|
699
|
+
case "geopoint":
|
|
700
|
+
base.type = "object";
|
|
701
|
+
base.properties = {
|
|
702
|
+
latitude: { type: "number" },
|
|
703
|
+
longitude: { type: "number" }
|
|
704
|
+
};
|
|
705
|
+
base.required = ["latitude", "longitude"];
|
|
706
|
+
return base;
|
|
707
|
+
case "reference":
|
|
708
|
+
base.type = "string";
|
|
709
|
+
base.description = (base.description || "") + " (Reference ID)";
|
|
710
|
+
return base;
|
|
711
|
+
case "array": {
|
|
712
|
+
const ap = property;
|
|
713
|
+
base.type = "array";
|
|
714
|
+
if (ap.oneOf) {
|
|
715
|
+
const typeField = ap.oneOf.typeField || "type";
|
|
716
|
+
const valueField = ap.oneOf.valueField || "value";
|
|
717
|
+
const variants = [];
|
|
718
|
+
for (const [variantKey, variantProp] of Object.entries(ap.oneOf.properties)) variants.push({
|
|
719
|
+
type: "object",
|
|
720
|
+
properties: {
|
|
721
|
+
[typeField]: {
|
|
722
|
+
type: "string",
|
|
723
|
+
enum: [variantKey]
|
|
724
|
+
},
|
|
725
|
+
[valueField]: convertPropertyToSchema(variantProp)
|
|
726
|
+
},
|
|
727
|
+
required: [typeField, valueField]
|
|
728
|
+
});
|
|
729
|
+
base.items = { oneOf: variants };
|
|
730
|
+
} else if (ap.of) if (Array.isArray(ap.of)) base.items = { oneOf: ap.of.map((p) => convertPropertyToSchema(p)) };
|
|
731
|
+
else base.items = convertPropertyToSchema(ap.of);
|
|
732
|
+
else base.items = {};
|
|
733
|
+
if (ap.validation) {
|
|
734
|
+
if (ap.validation.min !== void 0) base.minItems = ap.validation.min;
|
|
735
|
+
if (ap.validation.max !== void 0) base.maxItems = ap.validation.max;
|
|
736
|
+
}
|
|
737
|
+
return base;
|
|
738
|
+
}
|
|
739
|
+
case "map": {
|
|
740
|
+
const mp = property;
|
|
741
|
+
base.type = "object";
|
|
742
|
+
if (mp.properties) {
|
|
743
|
+
const props = {};
|
|
744
|
+
const req = [];
|
|
745
|
+
for (const [key, subProp] of Object.entries(mp.properties)) {
|
|
746
|
+
props[key] = convertPropertyToSchema(subProp);
|
|
747
|
+
if (subProp.validation?.required) req.push(key);
|
|
748
|
+
}
|
|
749
|
+
base.properties = props;
|
|
750
|
+
if (req.length > 0) base.required = req;
|
|
751
|
+
} else if (mp.keyValue) base.additionalProperties = true;
|
|
752
|
+
return base;
|
|
753
|
+
}
|
|
754
|
+
case "vector": {
|
|
755
|
+
const vp = property;
|
|
756
|
+
base.type = "array";
|
|
757
|
+
base.items = { type: "number" };
|
|
758
|
+
base.description = (base.description || "") + ` (Vector(${vp.dimensions}))`;
|
|
759
|
+
return base;
|
|
760
|
+
}
|
|
761
|
+
case "binary":
|
|
762
|
+
base.type = "string";
|
|
763
|
+
base.description = (base.description || "") + " (Binary/Base64)";
|
|
764
|
+
return base;
|
|
765
|
+
default:
|
|
766
|
+
base.type = "string";
|
|
767
|
+
return base;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Resolve EnumValues (array or record) into a flat array of enum values.
|
|
772
|
+
*/
|
|
773
|
+
function resolveEnumValues(enumDef) {
|
|
774
|
+
if (Array.isArray(enumDef)) return enumDef.map((e) => typeof e === "object" && e !== null && "id" in e ? e.id : e);
|
|
775
|
+
return Object.keys(enumDef).map((k) => {
|
|
776
|
+
const num = Number(k);
|
|
777
|
+
return isNaN(num) ? k : num;
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Build PostgREST-style filter parameters for a collection.
|
|
782
|
+
* These are additional query parameters like `?status=eq.active&price=gte.100`.
|
|
783
|
+
*
|
|
784
|
+
* `excludeFromApi` columns are not offered: the server does filter on them, and
|
|
785
|
+
* that is exactly the problem — a filter on a column no response can contain
|
|
786
|
+
* answers questions about the value one row at a time, which is a worse
|
|
787
|
+
* disclosure than the column name alone.
|
|
788
|
+
*/
|
|
789
|
+
function buildFilterParameters(collection, reservedNames = /* @__PURE__ */ new Set()) {
|
|
790
|
+
const params = [];
|
|
791
|
+
for (const [key, property] of Object.entries(collection.properties)) {
|
|
792
|
+
if (!isDocumentedProperty(property)) continue;
|
|
793
|
+
if (property.type === "map" || property.type === "array" || property.type === "geopoint") continue;
|
|
794
|
+
if (reservedNames.has(key)) continue;
|
|
795
|
+
params.push({
|
|
796
|
+
name: key,
|
|
797
|
+
in: "query",
|
|
798
|
+
required: false,
|
|
799
|
+
schema: { type: "string" },
|
|
800
|
+
description: `Filter by \`${key}\`. Supports PostgREST operators: \`eq.value\`, \`neq.value\`, \`gt.value\`, \`gte.value\`, \`lt.value\`, \`lte.value\`, \`in.(a,b,c)\`, \`nin.(a,b,c)\`, \`cs.value\` (array-contains), \`csa.(a,b)\` (array-contains-any). Plain values imply equality.`,
|
|
801
|
+
example: property.type === "string" ? "eq.active" : property.type === "number" ? "gte.100" : void 0
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
return params;
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Standard error responses included on every endpoint.
|
|
808
|
+
*/
|
|
809
|
+
function errorResponses(requireAuth) {
|
|
810
|
+
const responses = {
|
|
811
|
+
400: {
|
|
812
|
+
description: "Bad request",
|
|
813
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
|
|
814
|
+
},
|
|
815
|
+
500: {
|
|
816
|
+
description: "Internal server error",
|
|
817
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
if (requireAuth) {
|
|
821
|
+
responses[401] = {
|
|
822
|
+
description: "Authentication required or invalid token",
|
|
823
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
|
|
824
|
+
};
|
|
825
|
+
responses[403] = {
|
|
826
|
+
description: "Insufficient permissions",
|
|
827
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
return responses;
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Prefix a noun with "a" or "an" based on its leading sound.
|
|
834
|
+
*/
|
|
835
|
+
function withIndefiniteArticle(noun) {
|
|
836
|
+
return `${/^[aeiou]/i.test(noun) ? "an" : "a"} ${noun}`;
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* The component-schema name for a collection — and the stem of every
|
|
840
|
+
* `operationId` and `$ref` that mentions it.
|
|
841
|
+
*
|
|
842
|
+
* `toPascalCase` keeps ASCII letters and digits and nothing else, so a name
|
|
843
|
+
* written in a script that has none of them — a Cyrillic or Japanese
|
|
844
|
+
* `singularName`, which the docs' six locales make ordinary rather than exotic
|
|
845
|
+
* — reduced to the empty string. The schema was then stored under `""` and
|
|
846
|
+
* every reference to it read `#/components/schemas/`, an unresolvable pointer:
|
|
847
|
+
* Swagger UI renders the model empty and a strict generator fails outright. So
|
|
848
|
+
* fall through the names until one survives, and keep a constant as the floor.
|
|
849
|
+
*
|
|
850
|
+
* Two collections whose names PascalCase identically still share one component;
|
|
851
|
+
* that needs a disambiguation rule, not a fallback.
|
|
852
|
+
*/
|
|
853
|
+
function schemaNameFor(collection) {
|
|
854
|
+
return toPascalCase(collection.singularName || "") || toPascalCase(collection.name || "") || toPascalCase(collection.slug || "") || "Collection";
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Convert a string to PascalCase for schema names.
|
|
858
|
+
*/
|
|
859
|
+
function toPascalCase(str) {
|
|
860
|
+
return str.replace(/[^a-zA-Z0-9]+/g, " ").split(" ").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
861
|
+
}
|
|
862
|
+
//#endregion
|
|
863
|
+
export { generateOpenApiSpec };
|
|
864
|
+
|
|
865
|
+
//# sourceMappingURL=openapi-generator-BQxxxDpb.js.map
|