@questpie/openapi 3.0.18 → 3.0.20
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/README.md +5 -5
- package/dist/modules/openapi.d.mts +2 -0
- package/dist/modules/openapi.mjs +3 -0
- package/dist/plugin-CbmRC5nO.mjs +30 -0
- package/dist/plugin.mjs +1 -28
- package/dist/server-C8504hNI.d.mts +186 -0
- package/dist/server-DsUIkt4E.mjs +1205 -0
- package/dist/server.d.mts +2 -186
- package/dist/server.mjs +1 -1203
- package/package.json +25 -10
package/dist/server.mjs
CHANGED
|
@@ -1,1205 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { module, route } from "questpie";
|
|
3
|
-
import { z } from "zod";
|
|
1
|
+
import { a as openApiRoute, i as openApiModule, n as generateOpenApiSpec, o as server_default, r as openApiConfig, t as docsRoute } from "./server-DsUIkt4E.mjs";
|
|
4
2
|
|
|
5
|
-
//#region src/generator/schemas.ts
|
|
6
|
-
/**
|
|
7
|
-
* Shared schema helpers for OpenAPI spec generation.
|
|
8
|
-
* Provides $ref utilities, common schemas (pagination, errors), and Zod conversion helpers.
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* Create a $ref pointer to a component schema.
|
|
12
|
-
*/
|
|
13
|
-
function ref(name) {
|
|
14
|
-
return { $ref: `#/components/schemas/${name}` };
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Standard error response schema.
|
|
18
|
-
*/
|
|
19
|
-
function errorResponseSchema() {
|
|
20
|
-
return {
|
|
21
|
-
type: "object",
|
|
22
|
-
properties: { error: {
|
|
23
|
-
type: "object",
|
|
24
|
-
properties: {
|
|
25
|
-
code: { type: "string" },
|
|
26
|
-
message: { type: "string" },
|
|
27
|
-
details: {}
|
|
28
|
-
},
|
|
29
|
-
required: ["code", "message"]
|
|
30
|
-
} },
|
|
31
|
-
required: ["error"]
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Paginated response wrapper for a given item schema.
|
|
36
|
-
*/
|
|
37
|
-
function paginatedResponseSchema(itemRef) {
|
|
38
|
-
return {
|
|
39
|
-
type: "object",
|
|
40
|
-
properties: {
|
|
41
|
-
docs: {
|
|
42
|
-
type: "array",
|
|
43
|
-
items: itemRef
|
|
44
|
-
},
|
|
45
|
-
totalDocs: { type: "integer" },
|
|
46
|
-
limit: { type: "integer" },
|
|
47
|
-
page: { type: "integer" },
|
|
48
|
-
totalPages: { type: "integer" },
|
|
49
|
-
hasNextPage: { type: "boolean" },
|
|
50
|
-
hasPrevPage: { type: "boolean" },
|
|
51
|
-
nextPage: { type: ["integer", "null"] },
|
|
52
|
-
prevPage: { type: ["integer", "null"] }
|
|
53
|
-
},
|
|
54
|
-
required: [
|
|
55
|
-
"docs",
|
|
56
|
-
"totalDocs",
|
|
57
|
-
"limit",
|
|
58
|
-
"page",
|
|
59
|
-
"totalPages"
|
|
60
|
-
]
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Common query parameters for collection list endpoints.
|
|
65
|
-
*/
|
|
66
|
-
function listQueryParameters() {
|
|
67
|
-
return [
|
|
68
|
-
{
|
|
69
|
-
name: "limit",
|
|
70
|
-
in: "query",
|
|
71
|
-
schema: {
|
|
72
|
-
type: "integer",
|
|
73
|
-
default: 10
|
|
74
|
-
},
|
|
75
|
-
description: "Number of records to return"
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
name: "page",
|
|
79
|
-
in: "query",
|
|
80
|
-
schema: {
|
|
81
|
-
type: "integer",
|
|
82
|
-
default: 1
|
|
83
|
-
},
|
|
84
|
-
description: "Page number"
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
name: "offset",
|
|
88
|
-
in: "query",
|
|
89
|
-
schema: { type: "integer" },
|
|
90
|
-
description: "Number of records to skip"
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
name: "where",
|
|
94
|
-
in: "query",
|
|
95
|
-
schema: { type: "string" },
|
|
96
|
-
description: "Filter conditions (JSON encoded)"
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
name: "orderBy",
|
|
100
|
-
in: "query",
|
|
101
|
-
schema: { type: "string" },
|
|
102
|
-
description: "Sort configuration (JSON encoded)"
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
name: "locale",
|
|
106
|
-
in: "query",
|
|
107
|
-
schema: { type: "string" },
|
|
108
|
-
description: "Content locale"
|
|
109
|
-
},
|
|
110
|
-
stageQueryParameter()
|
|
111
|
-
];
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Common query parameters for single-record endpoints.
|
|
115
|
-
*/
|
|
116
|
-
function singleQueryParameters() {
|
|
117
|
-
return [{
|
|
118
|
-
name: "locale",
|
|
119
|
-
in: "query",
|
|
120
|
-
schema: { type: "string" },
|
|
121
|
-
description: "Content locale"
|
|
122
|
-
}, stageQueryParameter()];
|
|
123
|
-
}
|
|
124
|
-
function stageQueryParameter() {
|
|
125
|
-
return {
|
|
126
|
-
name: "stage",
|
|
127
|
-
in: "query",
|
|
128
|
-
schema: { type: "string" },
|
|
129
|
-
description: "Workflow stage"
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Standard JSON responses helper.
|
|
134
|
-
*/
|
|
135
|
-
function jsonResponse(schema, description = "Successful response") {
|
|
136
|
-
return {
|
|
137
|
-
"200": {
|
|
138
|
-
description,
|
|
139
|
-
content: { "application/json": { schema } }
|
|
140
|
-
},
|
|
141
|
-
"400": {
|
|
142
|
-
description: "Bad request",
|
|
143
|
-
content: { "application/json": { schema: ref("ErrorResponse") } }
|
|
144
|
-
},
|
|
145
|
-
"401": {
|
|
146
|
-
description: "Unauthorized",
|
|
147
|
-
content: { "application/json": { schema: ref("ErrorResponse") } }
|
|
148
|
-
},
|
|
149
|
-
"404": {
|
|
150
|
-
description: "Not found",
|
|
151
|
-
content: { "application/json": { schema: ref("ErrorResponse") } }
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* JSON request body helper.
|
|
157
|
-
*/
|
|
158
|
-
function jsonRequestBody(schema, description) {
|
|
159
|
-
return {
|
|
160
|
-
description,
|
|
161
|
-
required: true,
|
|
162
|
-
content: { "application/json": { schema } }
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* Safely convert a Zod schema to JSON Schema.
|
|
167
|
-
* Falls back to a permissive object schema on failure.
|
|
168
|
-
*/
|
|
169
|
-
function zodToJsonSchema(schema) {
|
|
170
|
-
try {
|
|
171
|
-
if (schema && typeof schema === "object" && "_def" in schema) return z.toJSONSchema(schema);
|
|
172
|
-
} catch {}
|
|
173
|
-
return {
|
|
174
|
-
type: "object",
|
|
175
|
-
description: "Schema could not be generated"
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Build the base component schemas shared across all endpoints.
|
|
180
|
-
*/
|
|
181
|
-
function baseComponentSchemas() {
|
|
182
|
-
return {
|
|
183
|
-
ErrorResponse: errorResponseSchema(),
|
|
184
|
-
SuccessResponse: {
|
|
185
|
-
type: "object",
|
|
186
|
-
properties: { success: { type: "boolean" } },
|
|
187
|
-
required: ["success"]
|
|
188
|
-
},
|
|
189
|
-
CountResponse: {
|
|
190
|
-
type: "object",
|
|
191
|
-
properties: { count: { type: "integer" } },
|
|
192
|
-
required: ["count"]
|
|
193
|
-
},
|
|
194
|
-
DeleteManyResponse: {
|
|
195
|
-
type: "object",
|
|
196
|
-
properties: {
|
|
197
|
-
success: { type: "boolean" },
|
|
198
|
-
count: { type: "integer" }
|
|
199
|
-
},
|
|
200
|
-
required: ["success"]
|
|
201
|
-
}
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
//#endregion
|
|
206
|
-
//#region src/generator/auth.ts
|
|
207
|
-
/**
|
|
208
|
-
* Generate OpenAPI paths for Better Auth endpoints.
|
|
209
|
-
*/
|
|
210
|
-
function generateAuthPaths(config) {
|
|
211
|
-
if (config.auth === false) return {
|
|
212
|
-
paths: {},
|
|
213
|
-
tags: []
|
|
214
|
-
};
|
|
215
|
-
const basePath = config.basePath ?? "/";
|
|
216
|
-
const tag = "Auth";
|
|
217
|
-
const paths = {};
|
|
218
|
-
paths[`${basePath}/auth/sign-in/email`] = { post: {
|
|
219
|
-
operationId: "auth_signInEmail",
|
|
220
|
-
summary: "Sign in with email and password",
|
|
221
|
-
tags: [tag],
|
|
222
|
-
requestBody: jsonRequestBody({
|
|
223
|
-
type: "object",
|
|
224
|
-
properties: {
|
|
225
|
-
email: {
|
|
226
|
-
type: "string",
|
|
227
|
-
format: "email"
|
|
228
|
-
},
|
|
229
|
-
password: { type: "string" }
|
|
230
|
-
},
|
|
231
|
-
required: ["email", "password"]
|
|
232
|
-
}),
|
|
233
|
-
responses: jsonResponse({
|
|
234
|
-
type: "object",
|
|
235
|
-
properties: {
|
|
236
|
-
user: { type: "object" },
|
|
237
|
-
session: { type: "object" }
|
|
238
|
-
}
|
|
239
|
-
}, "Authentication successful")
|
|
240
|
-
} };
|
|
241
|
-
paths[`${basePath}/auth/sign-up/email`] = { post: {
|
|
242
|
-
operationId: "auth_signUpEmail",
|
|
243
|
-
summary: "Sign up with email and password",
|
|
244
|
-
tags: [tag],
|
|
245
|
-
requestBody: jsonRequestBody({
|
|
246
|
-
type: "object",
|
|
247
|
-
properties: {
|
|
248
|
-
email: {
|
|
249
|
-
type: "string",
|
|
250
|
-
format: "email"
|
|
251
|
-
},
|
|
252
|
-
password: { type: "string" },
|
|
253
|
-
name: { type: "string" }
|
|
254
|
-
},
|
|
255
|
-
required: [
|
|
256
|
-
"email",
|
|
257
|
-
"password",
|
|
258
|
-
"name"
|
|
259
|
-
]
|
|
260
|
-
}),
|
|
261
|
-
responses: jsonResponse({
|
|
262
|
-
type: "object",
|
|
263
|
-
properties: {
|
|
264
|
-
user: { type: "object" },
|
|
265
|
-
session: { type: "object" }
|
|
266
|
-
}
|
|
267
|
-
}, "Registration successful")
|
|
268
|
-
} };
|
|
269
|
-
paths[`${basePath}/auth/get-session`] = { get: {
|
|
270
|
-
operationId: "auth_getSession",
|
|
271
|
-
summary: "Get current session",
|
|
272
|
-
tags: [tag],
|
|
273
|
-
responses: jsonResponse({
|
|
274
|
-
type: "object",
|
|
275
|
-
properties: {
|
|
276
|
-
user: { type: "object" },
|
|
277
|
-
session: { type: "object" }
|
|
278
|
-
}
|
|
279
|
-
}, "Current session")
|
|
280
|
-
} };
|
|
281
|
-
paths[`${basePath}/auth/sign-out`] = { post: {
|
|
282
|
-
operationId: "auth_signOut",
|
|
283
|
-
summary: "Sign out",
|
|
284
|
-
tags: [tag],
|
|
285
|
-
responses: jsonResponse({
|
|
286
|
-
type: "object",
|
|
287
|
-
properties: { success: { type: "boolean" } }
|
|
288
|
-
}, "Signed out")
|
|
289
|
-
} };
|
|
290
|
-
return {
|
|
291
|
-
paths,
|
|
292
|
-
tags: [{
|
|
293
|
-
name: tag,
|
|
294
|
-
description: "Authentication endpoints (Better Auth)"
|
|
295
|
-
}]
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
//#endregion
|
|
300
|
-
//#region src/generator/collections.ts
|
|
301
|
-
/**
|
|
302
|
-
* Generate OpenAPI paths and component schemas for all collections.
|
|
303
|
-
*/
|
|
304
|
-
function generateCollectionPaths(app, config) {
|
|
305
|
-
const collections = app.getCollections();
|
|
306
|
-
const basePath = config.basePath ?? "/";
|
|
307
|
-
const excluded = new Set(config.exclude?.collections ?? []);
|
|
308
|
-
const paths = {};
|
|
309
|
-
const schemas = {};
|
|
310
|
-
const tags = [];
|
|
311
|
-
for (const [name, collection] of Object.entries(collections)) {
|
|
312
|
-
if (excluded.has(name)) continue;
|
|
313
|
-
const state = collection.state;
|
|
314
|
-
if (!state) continue;
|
|
315
|
-
const tag = `Collections: ${name}`;
|
|
316
|
-
tags.push({
|
|
317
|
-
name: tag,
|
|
318
|
-
description: `CRUD operations for ${name}`
|
|
319
|
-
});
|
|
320
|
-
const pascalName = toPascalCase$1(name);
|
|
321
|
-
const documentSchemaName = `${pascalName}Document`;
|
|
322
|
-
const insertSchemaName = `${pascalName}Insert`;
|
|
323
|
-
const updateSchemaName = `${pascalName}Update`;
|
|
324
|
-
const fieldDefinitionSchema = buildSchemaFromFieldDefinitions(state.fieldDefinitions);
|
|
325
|
-
if (state.validation?.insertSchema) try {
|
|
326
|
-
schemas[insertSchemaName] = z.toJSONSchema(state.validation.insertSchema, { unrepresentable: "any" });
|
|
327
|
-
} catch {
|
|
328
|
-
schemas[insertSchemaName] = {
|
|
329
|
-
type: "object",
|
|
330
|
-
description: `Insert schema for ${name}`
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
else if (fieldDefinitionSchema != null) schemas[insertSchemaName] = fieldDefinitionSchema.insert;
|
|
334
|
-
else schemas[insertSchemaName] = {
|
|
335
|
-
type: "object",
|
|
336
|
-
description: `Insert schema for ${name}`
|
|
337
|
-
};
|
|
338
|
-
if (state.validation?.updateSchema) try {
|
|
339
|
-
schemas[updateSchemaName] = z.toJSONSchema(state.validation.updateSchema, { unrepresentable: "any" });
|
|
340
|
-
} catch {
|
|
341
|
-
schemas[updateSchemaName] = {
|
|
342
|
-
type: "object",
|
|
343
|
-
description: `Update schema for ${name}`
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
else if (fieldDefinitionSchema != null) schemas[updateSchemaName] = fieldDefinitionSchema.update;
|
|
347
|
-
else schemas[updateSchemaName] = {
|
|
348
|
-
type: "object",
|
|
349
|
-
description: `Update schema for ${name}`
|
|
350
|
-
};
|
|
351
|
-
schemas[documentSchemaName] = buildDocumentSchema(name, state, insertSchemaName);
|
|
352
|
-
const prefix = `${basePath}/${name}`;
|
|
353
|
-
paths[prefix] = {
|
|
354
|
-
get: {
|
|
355
|
-
operationId: `${name}_find`,
|
|
356
|
-
summary: `List ${name}`,
|
|
357
|
-
tags: [tag],
|
|
358
|
-
parameters: listQueryParameters(),
|
|
359
|
-
responses: jsonResponse(paginatedResponseSchema(ref(documentSchemaName)), `Paginated list of ${name}`)
|
|
360
|
-
},
|
|
361
|
-
post: {
|
|
362
|
-
operationId: `${name}_create`,
|
|
363
|
-
summary: `Create ${name}`,
|
|
364
|
-
tags: [tag],
|
|
365
|
-
parameters: [stageQueryParameter()],
|
|
366
|
-
requestBody: jsonRequestBody(ref(insertSchemaName)),
|
|
367
|
-
responses: jsonResponse(ref(documentSchemaName), `Created ${name} record`)
|
|
368
|
-
}
|
|
369
|
-
};
|
|
370
|
-
paths[`${prefix}/count`] = { get: {
|
|
371
|
-
operationId: `${name}_count`,
|
|
372
|
-
summary: `Count ${name}`,
|
|
373
|
-
tags: [tag],
|
|
374
|
-
parameters: [{
|
|
375
|
-
name: "where",
|
|
376
|
-
in: "query",
|
|
377
|
-
schema: { type: "string" },
|
|
378
|
-
description: "Filter conditions (JSON encoded)"
|
|
379
|
-
}],
|
|
380
|
-
responses: jsonResponse(ref("CountResponse"), `Count of ${name}`)
|
|
381
|
-
} };
|
|
382
|
-
paths[`${prefix}/delete-many`] = { post: {
|
|
383
|
-
operationId: `${name}_deleteMany`,
|
|
384
|
-
summary: `Delete many ${name}`,
|
|
385
|
-
tags: [tag],
|
|
386
|
-
requestBody: jsonRequestBody({
|
|
387
|
-
type: "object",
|
|
388
|
-
properties: { where: {
|
|
389
|
-
type: "object",
|
|
390
|
-
description: "Filter conditions for records to delete"
|
|
391
|
-
} }
|
|
392
|
-
}),
|
|
393
|
-
responses: jsonResponse(ref("DeleteManyResponse"), `Delete multiple ${name} records`)
|
|
394
|
-
} };
|
|
395
|
-
if (state.upload) paths[`${prefix}/upload`] = { post: {
|
|
396
|
-
operationId: `${name}_upload`,
|
|
397
|
-
summary: `Upload file to ${name}`,
|
|
398
|
-
tags: [tag],
|
|
399
|
-
requestBody: {
|
|
400
|
-
required: true,
|
|
401
|
-
content: { "multipart/form-data": { schema: {
|
|
402
|
-
type: "object",
|
|
403
|
-
properties: { file: {
|
|
404
|
-
type: "string",
|
|
405
|
-
format: "binary"
|
|
406
|
-
} },
|
|
407
|
-
required: ["file"]
|
|
408
|
-
} } }
|
|
409
|
-
},
|
|
410
|
-
responses: jsonResponse(ref(documentSchemaName), `Uploaded file record`)
|
|
411
|
-
} };
|
|
412
|
-
paths[`${prefix}/schema`] = { get: {
|
|
413
|
-
operationId: `${name}_schema`,
|
|
414
|
-
summary: `Get ${name} introspection schema`,
|
|
415
|
-
tags: [tag],
|
|
416
|
-
responses: jsonResponse({
|
|
417
|
-
type: "object",
|
|
418
|
-
description: "Introspected collection schema"
|
|
419
|
-
}, `Introspection schema for ${name}`)
|
|
420
|
-
} };
|
|
421
|
-
paths[`${prefix}/meta`] = { get: {
|
|
422
|
-
operationId: `${name}_meta`,
|
|
423
|
-
summary: `Get ${name} metadata`,
|
|
424
|
-
tags: [tag],
|
|
425
|
-
responses: jsonResponse({
|
|
426
|
-
type: "object",
|
|
427
|
-
description: "Collection metadata"
|
|
428
|
-
}, `Metadata for ${name}`)
|
|
429
|
-
} };
|
|
430
|
-
const idParam = {
|
|
431
|
-
name: "id",
|
|
432
|
-
in: "path",
|
|
433
|
-
required: true,
|
|
434
|
-
schema: { type: "string" },
|
|
435
|
-
description: "Record ID"
|
|
436
|
-
};
|
|
437
|
-
paths[`${prefix}/{id}`] = {
|
|
438
|
-
get: {
|
|
439
|
-
operationId: `${name}_findOne`,
|
|
440
|
-
summary: `Get ${name} by ID`,
|
|
441
|
-
tags: [tag],
|
|
442
|
-
parameters: [idParam, ...singleQueryParameters()],
|
|
443
|
-
responses: jsonResponse(ref(documentSchemaName), `Single ${name} record`)
|
|
444
|
-
},
|
|
445
|
-
patch: {
|
|
446
|
-
operationId: `${name}_update`,
|
|
447
|
-
summary: `Update ${name}`,
|
|
448
|
-
tags: [tag],
|
|
449
|
-
parameters: [idParam, stageQueryParameter()],
|
|
450
|
-
requestBody: jsonRequestBody(ref(updateSchemaName)),
|
|
451
|
-
responses: jsonResponse(ref(documentSchemaName), `Updated ${name} record`)
|
|
452
|
-
},
|
|
453
|
-
delete: {
|
|
454
|
-
operationId: `${name}_delete`,
|
|
455
|
-
summary: `Delete ${name}`,
|
|
456
|
-
tags: [tag],
|
|
457
|
-
parameters: [idParam, stageQueryParameter()],
|
|
458
|
-
responses: jsonResponse(ref("SuccessResponse"), `Deleted ${name} record`)
|
|
459
|
-
}
|
|
460
|
-
};
|
|
461
|
-
if (state.options?.softDelete) paths[`${prefix}/{id}/restore`] = { post: {
|
|
462
|
-
operationId: `${name}_restore`,
|
|
463
|
-
summary: `Restore deleted ${name}`,
|
|
464
|
-
tags: [tag],
|
|
465
|
-
parameters: [idParam, stageQueryParameter()],
|
|
466
|
-
responses: jsonResponse(ref(documentSchemaName), `Restored ${name} record`)
|
|
467
|
-
} };
|
|
468
|
-
paths[`${prefix}/{id}/versions`] = { get: {
|
|
469
|
-
operationId: `${name}_findVersions`,
|
|
470
|
-
summary: `List ${name} versions`,
|
|
471
|
-
tags: [tag],
|
|
472
|
-
parameters: [
|
|
473
|
-
idParam,
|
|
474
|
-
{
|
|
475
|
-
name: "limit",
|
|
476
|
-
in: "query",
|
|
477
|
-
schema: { type: "number" },
|
|
478
|
-
description: "Maximum number of versions to return"
|
|
479
|
-
},
|
|
480
|
-
{
|
|
481
|
-
name: "offset",
|
|
482
|
-
in: "query",
|
|
483
|
-
schema: { type: "number" },
|
|
484
|
-
description: "Number of versions to skip"
|
|
485
|
-
}
|
|
486
|
-
],
|
|
487
|
-
responses: jsonResponse({
|
|
488
|
-
type: "array",
|
|
489
|
-
items: {
|
|
490
|
-
type: "object",
|
|
491
|
-
properties: {
|
|
492
|
-
id: { type: "string" },
|
|
493
|
-
versionId: { type: "string" },
|
|
494
|
-
versionNumber: { type: "number" },
|
|
495
|
-
versionOperation: { type: "string" },
|
|
496
|
-
versionUserId: { type: ["string", "null"] },
|
|
497
|
-
versionCreatedAt: {
|
|
498
|
-
type: "string",
|
|
499
|
-
format: "date-time"
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
}, `Version history for ${name}`)
|
|
504
|
-
} };
|
|
505
|
-
paths[`${prefix}/{id}/revert`] = { post: {
|
|
506
|
-
operationId: `${name}_revertToVersion`,
|
|
507
|
-
summary: `Revert ${name} to a version`,
|
|
508
|
-
tags: [tag],
|
|
509
|
-
parameters: [idParam, stageQueryParameter()],
|
|
510
|
-
requestBody: jsonRequestBody({
|
|
511
|
-
type: "object",
|
|
512
|
-
properties: {
|
|
513
|
-
version: { type: "number" },
|
|
514
|
-
versionId: { type: "string" }
|
|
515
|
-
}
|
|
516
|
-
}),
|
|
517
|
-
responses: jsonResponse(ref(documentSchemaName), `Reverted ${name} record`)
|
|
518
|
-
} };
|
|
519
|
-
const versioningOpts = state.options?.versioning;
|
|
520
|
-
if (versioningOpts && typeof versioningOpts === "object" && versioningOpts.workflow) paths[`${prefix}/{id}/transition`] = { post: {
|
|
521
|
-
operationId: `${name}_transition`,
|
|
522
|
-
summary: `Transition ${name} workflow stage`,
|
|
523
|
-
tags: [tag],
|
|
524
|
-
parameters: [idParam],
|
|
525
|
-
requestBody: jsonRequestBody({
|
|
526
|
-
type: "object",
|
|
527
|
-
required: ["stage"],
|
|
528
|
-
properties: { stage: {
|
|
529
|
-
type: "string",
|
|
530
|
-
description: "Target workflow stage"
|
|
531
|
-
} }
|
|
532
|
-
}),
|
|
533
|
-
responses: jsonResponse(ref(documentSchemaName), `Transitioned ${name} record`)
|
|
534
|
-
} };
|
|
535
|
-
}
|
|
536
|
-
return {
|
|
537
|
-
paths,
|
|
538
|
-
schemas,
|
|
539
|
-
tags
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
/**
|
|
543
|
-
* Build a document response schema that extends the insert schema with
|
|
544
|
-
* standard fields (id, timestamps).
|
|
545
|
-
*/
|
|
546
|
-
function buildDocumentSchema(name, state, insertSchemaName) {
|
|
547
|
-
const properties = { id: { type: "string" } };
|
|
548
|
-
if (state.options?.timestamps !== false) {
|
|
549
|
-
properties.createdAt = {
|
|
550
|
-
type: "string",
|
|
551
|
-
format: "date-time"
|
|
552
|
-
};
|
|
553
|
-
properties.updatedAt = {
|
|
554
|
-
type: "string",
|
|
555
|
-
format: "date-time"
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
if (state.options?.softDelete) properties.deletedAt = {
|
|
559
|
-
type: ["string", "null"],
|
|
560
|
-
format: "date-time"
|
|
561
|
-
};
|
|
562
|
-
return {
|
|
563
|
-
allOf: [{
|
|
564
|
-
type: "object",
|
|
565
|
-
properties,
|
|
566
|
-
required: ["id"]
|
|
567
|
-
}, ref(insertSchemaName)],
|
|
568
|
-
description: `${name} document`
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
function toPascalCase$1(str) {
|
|
572
|
-
return str.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replace(/^(.)/, (_, c) => c.toUpperCase());
|
|
573
|
-
}
|
|
574
|
-
function buildSchemaFromFieldDefinitions(fieldDefinitions) {
|
|
575
|
-
if (!fieldDefinitions || typeof fieldDefinitions !== "object") return null;
|
|
576
|
-
const shape = {};
|
|
577
|
-
for (const [fieldName, fieldDefinition] of Object.entries(fieldDefinitions)) {
|
|
578
|
-
const fd = fieldDefinition;
|
|
579
|
-
if (typeof fd.toZodSchema !== "function") continue;
|
|
580
|
-
try {
|
|
581
|
-
const schema = fd.toZodSchema();
|
|
582
|
-
if (schema && typeof schema === "object" && "_def" in schema) shape[fieldName] = schema;
|
|
583
|
-
} catch {}
|
|
584
|
-
}
|
|
585
|
-
if (Object.keys(shape).length === 0) return null;
|
|
586
|
-
const insertSchema = z.object(shape);
|
|
587
|
-
const updateSchema = insertSchema.partial();
|
|
588
|
-
return {
|
|
589
|
-
insert: z.toJSONSchema(insertSchema, { unrepresentable: "any" }),
|
|
590
|
-
update: z.toJSONSchema(updateSchema, { unrepresentable: "any" })
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
//#endregion
|
|
595
|
-
//#region src/generator/globals.ts
|
|
596
|
-
/**
|
|
597
|
-
* Generate OpenAPI paths and component schemas for all globals.
|
|
598
|
-
*/
|
|
599
|
-
function generateGlobalPaths(app, config) {
|
|
600
|
-
const globals = app.getGlobals();
|
|
601
|
-
const basePath = config.basePath ?? "/";
|
|
602
|
-
const excluded = new Set(config.exclude?.globals ?? []);
|
|
603
|
-
const paths = {};
|
|
604
|
-
const schemas = {};
|
|
605
|
-
const tags = [];
|
|
606
|
-
for (const [name, global] of Object.entries(globals)) {
|
|
607
|
-
if (excluded.has(name)) continue;
|
|
608
|
-
const state = global.state;
|
|
609
|
-
if (!state) continue;
|
|
610
|
-
const tag = `Globals: ${name}`;
|
|
611
|
-
tags.push({
|
|
612
|
-
name: tag,
|
|
613
|
-
description: `Operations for ${name} global`
|
|
614
|
-
});
|
|
615
|
-
const pascalName = toPascalCase(name);
|
|
616
|
-
const valueSchemaName = `${pascalName}Global`;
|
|
617
|
-
const updateSchemaName = `${pascalName}GlobalUpdate`;
|
|
618
|
-
const fieldDefinitionSchema = buildGlobalSchemaFromFieldDefinitions(state.fieldDefinitions);
|
|
619
|
-
if (state.validation?.updateSchema) try {
|
|
620
|
-
schemas[updateSchemaName] = z.toJSONSchema(state.validation.updateSchema, { unrepresentable: "any" });
|
|
621
|
-
} catch {
|
|
622
|
-
schemas[updateSchemaName] = {
|
|
623
|
-
type: "object",
|
|
624
|
-
description: `Update schema for ${name} global`
|
|
625
|
-
};
|
|
626
|
-
}
|
|
627
|
-
else if (fieldDefinitionSchema != null) schemas[updateSchemaName] = fieldDefinitionSchema;
|
|
628
|
-
else schemas[updateSchemaName] = {
|
|
629
|
-
type: "object",
|
|
630
|
-
description: `Update schema for ${name} global`
|
|
631
|
-
};
|
|
632
|
-
const properties = { id: { type: "string" } };
|
|
633
|
-
if (state.options?.timestamps !== false) {
|
|
634
|
-
properties.createdAt = {
|
|
635
|
-
type: "string",
|
|
636
|
-
format: "date-time"
|
|
637
|
-
};
|
|
638
|
-
properties.updatedAt = {
|
|
639
|
-
type: "string",
|
|
640
|
-
format: "date-time"
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
schemas[valueSchemaName] = {
|
|
644
|
-
allOf: [{
|
|
645
|
-
type: "object",
|
|
646
|
-
properties,
|
|
647
|
-
required: ["id"]
|
|
648
|
-
}, ref(updateSchemaName)],
|
|
649
|
-
description: `${name} global value`
|
|
650
|
-
};
|
|
651
|
-
const prefix = `${basePath}/globals/${name}`;
|
|
652
|
-
paths[prefix] = {
|
|
653
|
-
get: {
|
|
654
|
-
operationId: `global_${name}_get`,
|
|
655
|
-
summary: `Get ${name} global`,
|
|
656
|
-
tags: [tag],
|
|
657
|
-
parameters: [{
|
|
658
|
-
name: "locale",
|
|
659
|
-
in: "query",
|
|
660
|
-
schema: { type: "string" },
|
|
661
|
-
description: "Content locale"
|
|
662
|
-
}, stageQueryParameter()],
|
|
663
|
-
responses: jsonResponse(ref(valueSchemaName), `Current value of ${name} global`)
|
|
664
|
-
},
|
|
665
|
-
patch: {
|
|
666
|
-
operationId: `global_${name}_update`,
|
|
667
|
-
summary: `Update ${name} global`,
|
|
668
|
-
tags: [tag],
|
|
669
|
-
parameters: [stageQueryParameter()],
|
|
670
|
-
requestBody: jsonRequestBody(ref(updateSchemaName)),
|
|
671
|
-
responses: jsonResponse(ref(valueSchemaName), `Updated ${name} global`)
|
|
672
|
-
}
|
|
673
|
-
};
|
|
674
|
-
paths[`${prefix}/schema`] = { get: {
|
|
675
|
-
operationId: `global_${name}_schema`,
|
|
676
|
-
summary: `Get ${name} global introspection schema`,
|
|
677
|
-
tags: [tag],
|
|
678
|
-
responses: jsonResponse({
|
|
679
|
-
type: "object",
|
|
680
|
-
description: "Introspected global schema"
|
|
681
|
-
}, `Introspection schema for ${name} global`)
|
|
682
|
-
} };
|
|
683
|
-
paths[`${prefix}/versions`] = { get: {
|
|
684
|
-
operationId: `global_${name}_findVersions`,
|
|
685
|
-
summary: `List ${name} global versions`,
|
|
686
|
-
tags: [tag],
|
|
687
|
-
parameters: [
|
|
688
|
-
{
|
|
689
|
-
name: "id",
|
|
690
|
-
in: "query",
|
|
691
|
-
schema: { type: "string" },
|
|
692
|
-
description: "Global record ID"
|
|
693
|
-
},
|
|
694
|
-
{
|
|
695
|
-
name: "limit",
|
|
696
|
-
in: "query",
|
|
697
|
-
schema: { type: "number" },
|
|
698
|
-
description: "Maximum number of versions to return"
|
|
699
|
-
},
|
|
700
|
-
{
|
|
701
|
-
name: "offset",
|
|
702
|
-
in: "query",
|
|
703
|
-
schema: { type: "number" },
|
|
704
|
-
description: "Number of versions to skip"
|
|
705
|
-
}
|
|
706
|
-
],
|
|
707
|
-
responses: jsonResponse({
|
|
708
|
-
type: "array",
|
|
709
|
-
items: {
|
|
710
|
-
type: "object",
|
|
711
|
-
properties: {
|
|
712
|
-
id: { type: "string" },
|
|
713
|
-
versionId: { type: "string" },
|
|
714
|
-
versionNumber: { type: "number" },
|
|
715
|
-
versionOperation: { type: "string" },
|
|
716
|
-
versionUserId: { type: ["string", "null"] },
|
|
717
|
-
versionCreatedAt: {
|
|
718
|
-
type: "string",
|
|
719
|
-
format: "date-time"
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
}, `Version history for ${name} global`)
|
|
724
|
-
} };
|
|
725
|
-
paths[`${prefix}/revert`] = { post: {
|
|
726
|
-
operationId: `global_${name}_revertToVersion`,
|
|
727
|
-
summary: `Revert ${name} global to a version`,
|
|
728
|
-
tags: [tag],
|
|
729
|
-
parameters: [stageQueryParameter()],
|
|
730
|
-
requestBody: jsonRequestBody({
|
|
731
|
-
type: "object",
|
|
732
|
-
properties: {
|
|
733
|
-
id: { type: "string" },
|
|
734
|
-
version: { type: "number" },
|
|
735
|
-
versionId: { type: "string" }
|
|
736
|
-
}
|
|
737
|
-
}),
|
|
738
|
-
responses: jsonResponse(ref(valueSchemaName), `Reverted ${name} global value`)
|
|
739
|
-
} };
|
|
740
|
-
const versioningOpts = state.options?.versioning;
|
|
741
|
-
if (versioningOpts && typeof versioningOpts === "object" && versioningOpts.workflow) paths[`${prefix}/transition`] = { post: {
|
|
742
|
-
operationId: `global_${name}_transition`,
|
|
743
|
-
summary: `Transition ${name} global workflow stage`,
|
|
744
|
-
tags: [tag],
|
|
745
|
-
requestBody: jsonRequestBody({
|
|
746
|
-
type: "object",
|
|
747
|
-
required: ["stage"],
|
|
748
|
-
properties: { stage: {
|
|
749
|
-
type: "string",
|
|
750
|
-
description: "Target workflow stage"
|
|
751
|
-
} }
|
|
752
|
-
}),
|
|
753
|
-
responses: jsonResponse(ref(valueSchemaName), `Transitioned ${name} global value`)
|
|
754
|
-
} };
|
|
755
|
-
}
|
|
756
|
-
return {
|
|
757
|
-
paths,
|
|
758
|
-
schemas,
|
|
759
|
-
tags
|
|
760
|
-
};
|
|
761
|
-
}
|
|
762
|
-
function toPascalCase(str) {
|
|
763
|
-
return str.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replace(/^(.)/, (_, c) => c.toUpperCase());
|
|
764
|
-
}
|
|
765
|
-
function buildGlobalSchemaFromFieldDefinitions(fieldDefinitions) {
|
|
766
|
-
if (!fieldDefinitions || typeof fieldDefinitions !== "object") return null;
|
|
767
|
-
const shape = {};
|
|
768
|
-
for (const [fieldName, fieldDefinition] of Object.entries(fieldDefinitions)) {
|
|
769
|
-
const fd = fieldDefinition;
|
|
770
|
-
if (typeof fd.toZodSchema !== "function") continue;
|
|
771
|
-
try {
|
|
772
|
-
const schema = fd.toZodSchema();
|
|
773
|
-
if (schema && typeof schema === "object" && "_def" in schema) shape[fieldName] = schema;
|
|
774
|
-
} catch {}
|
|
775
|
-
}
|
|
776
|
-
if (Object.keys(shape).length === 0) return null;
|
|
777
|
-
return z.toJSONSchema(z.object(shape).partial(), { unrepresentable: "any" });
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
//#endregion
|
|
781
|
-
//#region src/generator/routes.ts
|
|
782
|
-
/**
|
|
783
|
-
* Convert camelCase to kebab-case, matching the HTTP adapter's URL generation.
|
|
784
|
-
* e.g. "createBooking" → "create-booking"
|
|
785
|
-
*/
|
|
786
|
-
function camelToKebab(str) {
|
|
787
|
-
return str.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
788
|
-
}
|
|
789
|
-
/**
|
|
790
|
-
* Flatten a routes tree into a list of { path, definition }.
|
|
791
|
-
*/
|
|
792
|
-
function flattenRoutesTree(tree, prefix = []) {
|
|
793
|
-
const entries = [];
|
|
794
|
-
for (const [key, value] of Object.entries(tree)) {
|
|
795
|
-
const segments = [...prefix, key];
|
|
796
|
-
if (value && typeof value === "object" && "handler" in value && typeof value.handler === "function") entries.push({
|
|
797
|
-
path: segments.join("/"),
|
|
798
|
-
segments,
|
|
799
|
-
definition: value
|
|
800
|
-
});
|
|
801
|
-
else if (value && typeof value === "object") entries.push(...flattenRoutesTree(value, segments));
|
|
802
|
-
}
|
|
803
|
-
return entries;
|
|
804
|
-
}
|
|
805
|
-
/**
|
|
806
|
-
* Generate OpenAPI paths for all routes in the routes tree.
|
|
807
|
-
*/
|
|
808
|
-
function generateRoutePaths(routes, config) {
|
|
809
|
-
const paths = {};
|
|
810
|
-
const schemas = {};
|
|
811
|
-
const tags = [];
|
|
812
|
-
if (!routes) return {
|
|
813
|
-
paths,
|
|
814
|
-
schemas,
|
|
815
|
-
tags
|
|
816
|
-
};
|
|
817
|
-
const basePath = config.basePath ?? "/";
|
|
818
|
-
const entries = flattenRoutesTree(routes);
|
|
819
|
-
const tagSet = /* @__PURE__ */ new Set();
|
|
820
|
-
for (const entry of entries) {
|
|
821
|
-
const def = entry.definition;
|
|
822
|
-
const isRaw = def.mode === "raw";
|
|
823
|
-
const method = (def.method ?? "post").toLowerCase();
|
|
824
|
-
const topLevel = entry.segments[0] ?? "routes";
|
|
825
|
-
if (!tagSet.has(topLevel)) {
|
|
826
|
-
tagSet.add(topLevel);
|
|
827
|
-
tags.push({
|
|
828
|
-
name: `Routes: ${topLevel}`,
|
|
829
|
-
description: `Routes under ${topLevel}`
|
|
830
|
-
});
|
|
831
|
-
}
|
|
832
|
-
const operationId = `route_${entry.segments.join("_")}`;
|
|
833
|
-
const routePath = `${basePath}/${entry.segments.map(camelToKebab).join("/")}`;
|
|
834
|
-
const operation = {
|
|
835
|
-
operationId,
|
|
836
|
-
summary: entry.path,
|
|
837
|
-
tags: [`Routes: ${topLevel}`],
|
|
838
|
-
responses: {}
|
|
839
|
-
};
|
|
840
|
-
if (isRaw) {
|
|
841
|
-
operation.description = "Raw route - accepts any request body and returns a raw response.";
|
|
842
|
-
operation.requestBody = { content: {
|
|
843
|
-
"application/json": { schema: {} },
|
|
844
|
-
"application/octet-stream": { schema: {
|
|
845
|
-
type: "string",
|
|
846
|
-
format: "binary"
|
|
847
|
-
} }
|
|
848
|
-
} };
|
|
849
|
-
operation.responses = {
|
|
850
|
-
"200": { description: "Raw response" },
|
|
851
|
-
"401": {
|
|
852
|
-
description: "Unauthorized",
|
|
853
|
-
content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
|
|
854
|
-
}
|
|
855
|
-
};
|
|
856
|
-
} else {
|
|
857
|
-
let inputSchema = {};
|
|
858
|
-
let outputSchema = { type: "object" };
|
|
859
|
-
if (def.schema) {
|
|
860
|
-
const schemaName = `${operationId}_Input`;
|
|
861
|
-
schemas[schemaName] = zodToJsonSchema(def.schema);
|
|
862
|
-
inputSchema = { $ref: `#/components/schemas/${schemaName}` };
|
|
863
|
-
}
|
|
864
|
-
if (def.outputSchema) {
|
|
865
|
-
const schemaName = `${operationId}_Output`;
|
|
866
|
-
schemas[schemaName] = zodToJsonSchema(def.outputSchema);
|
|
867
|
-
outputSchema = { $ref: `#/components/schemas/${schemaName}` };
|
|
868
|
-
}
|
|
869
|
-
operation.requestBody = jsonRequestBody(inputSchema, "Route input");
|
|
870
|
-
operation.responses = jsonResponse(outputSchema, "Route output");
|
|
871
|
-
}
|
|
872
|
-
paths[routePath] = { [method]: operation };
|
|
873
|
-
}
|
|
874
|
-
return {
|
|
875
|
-
paths,
|
|
876
|
-
schemas,
|
|
877
|
-
tags
|
|
878
|
-
};
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
//#endregion
|
|
882
|
-
//#region src/generator/search.ts
|
|
883
|
-
/**
|
|
884
|
-
* Generate OpenAPI paths for search endpoints.
|
|
885
|
-
*/
|
|
886
|
-
function generateSearchPaths(config) {
|
|
887
|
-
if (config.search === false) return {
|
|
888
|
-
paths: {},
|
|
889
|
-
tags: []
|
|
890
|
-
};
|
|
891
|
-
const basePath = config.basePath ?? "/";
|
|
892
|
-
const tag = "Search";
|
|
893
|
-
const paths = {};
|
|
894
|
-
paths[`${basePath}/search`] = { post: {
|
|
895
|
-
operationId: "search",
|
|
896
|
-
summary: "Search across collections",
|
|
897
|
-
tags: [tag],
|
|
898
|
-
requestBody: jsonRequestBody({
|
|
899
|
-
type: "object",
|
|
900
|
-
properties: {
|
|
901
|
-
query: {
|
|
902
|
-
type: "string",
|
|
903
|
-
description: "Search query"
|
|
904
|
-
},
|
|
905
|
-
collections: {
|
|
906
|
-
type: "array",
|
|
907
|
-
items: { type: "string" },
|
|
908
|
-
description: "Collections to search (omit for all)"
|
|
909
|
-
},
|
|
910
|
-
limit: {
|
|
911
|
-
type: "integer",
|
|
912
|
-
default: 10
|
|
913
|
-
},
|
|
914
|
-
offset: {
|
|
915
|
-
type: "integer",
|
|
916
|
-
default: 0
|
|
917
|
-
}
|
|
918
|
-
},
|
|
919
|
-
required: ["query"]
|
|
920
|
-
}),
|
|
921
|
-
responses: jsonResponse({
|
|
922
|
-
type: "object",
|
|
923
|
-
properties: {
|
|
924
|
-
results: {
|
|
925
|
-
type: "array",
|
|
926
|
-
items: {
|
|
927
|
-
type: "object",
|
|
928
|
-
properties: {
|
|
929
|
-
collection: { type: "string" },
|
|
930
|
-
doc: { type: "object" },
|
|
931
|
-
_search: {
|
|
932
|
-
type: "object",
|
|
933
|
-
properties: {
|
|
934
|
-
score: { type: "number" },
|
|
935
|
-
highlights: { type: "object" },
|
|
936
|
-
indexedTitle: { type: "string" }
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
},
|
|
942
|
-
totalResults: { type: "integer" }
|
|
943
|
-
}
|
|
944
|
-
}, "Search results")
|
|
945
|
-
} };
|
|
946
|
-
paths[`${basePath}/search/reindex/{collection}`] = { post: {
|
|
947
|
-
operationId: "search_reindex",
|
|
948
|
-
summary: "Reindex a collection",
|
|
949
|
-
description: "Requires admin authentication.",
|
|
950
|
-
tags: [tag],
|
|
951
|
-
parameters: [{
|
|
952
|
-
name: "collection",
|
|
953
|
-
in: "path",
|
|
954
|
-
required: true,
|
|
955
|
-
schema: { type: "string" },
|
|
956
|
-
description: "Collection name to reindex"
|
|
957
|
-
}],
|
|
958
|
-
responses: jsonResponse({
|
|
959
|
-
type: "object",
|
|
960
|
-
properties: {
|
|
961
|
-
success: { type: "boolean" },
|
|
962
|
-
collection: { type: "string" }
|
|
963
|
-
}
|
|
964
|
-
}, "Reindex started")
|
|
965
|
-
} };
|
|
966
|
-
return {
|
|
967
|
-
paths,
|
|
968
|
-
tags: [{
|
|
969
|
-
name: tag,
|
|
970
|
-
description: "Full-text search endpoints"
|
|
971
|
-
}]
|
|
972
|
-
};
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
//#endregion
|
|
976
|
-
//#region src/generator/index.ts
|
|
977
|
-
/**
|
|
978
|
-
* Generate a complete OpenAPI 3.1 spec from a Questpie app instance and optional routes tree.
|
|
979
|
-
*/
|
|
980
|
-
function generateOpenApiSpec$1(app, routes, config = {}) {
|
|
981
|
-
const allPaths = {};
|
|
982
|
-
const allSchemas = { ...baseComponentSchemas() };
|
|
983
|
-
const allTags = [];
|
|
984
|
-
const collections = generateCollectionPaths(app, config);
|
|
985
|
-
Object.assign(allPaths, collections.paths);
|
|
986
|
-
Object.assign(allSchemas, collections.schemas);
|
|
987
|
-
allTags.push(...collections.tags);
|
|
988
|
-
const globals = generateGlobalPaths(app, config);
|
|
989
|
-
Object.assign(allPaths, globals.paths);
|
|
990
|
-
Object.assign(allSchemas, globals.schemas);
|
|
991
|
-
allTags.push(...globals.tags);
|
|
992
|
-
const routeResult = generateRoutePaths(routes, config);
|
|
993
|
-
Object.assign(allPaths, routeResult.paths);
|
|
994
|
-
Object.assign(allSchemas, routeResult.schemas);
|
|
995
|
-
allTags.push(...routeResult.tags);
|
|
996
|
-
const auth = generateAuthPaths(config);
|
|
997
|
-
Object.assign(allPaths, auth.paths);
|
|
998
|
-
allTags.push(...auth.tags);
|
|
999
|
-
const search = generateSearchPaths(config);
|
|
1000
|
-
Object.assign(allPaths, search.paths);
|
|
1001
|
-
allTags.push(...search.tags);
|
|
1002
|
-
return {
|
|
1003
|
-
openapi: "3.1.0",
|
|
1004
|
-
info: {
|
|
1005
|
-
title: config.info?.title ?? "QUESTPIE API",
|
|
1006
|
-
version: config.info?.version ?? "1.0.0",
|
|
1007
|
-
description: config.info?.description
|
|
1008
|
-
},
|
|
1009
|
-
servers: config.servers,
|
|
1010
|
-
paths: allPaths,
|
|
1011
|
-
components: {
|
|
1012
|
-
schemas: allSchemas,
|
|
1013
|
-
securitySchemes: {
|
|
1014
|
-
bearerAuth: {
|
|
1015
|
-
type: "http",
|
|
1016
|
-
scheme: "bearer"
|
|
1017
|
-
},
|
|
1018
|
-
cookieAuth: {
|
|
1019
|
-
type: "apiKey",
|
|
1020
|
-
in: "cookie",
|
|
1021
|
-
name: "better-auth.session_token"
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
},
|
|
1025
|
-
tags: allTags,
|
|
1026
|
-
security: [{ bearerAuth: [] }, { cookieAuth: [] }]
|
|
1027
|
-
};
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
//#endregion
|
|
1031
|
-
//#region src/scalar.ts
|
|
1032
|
-
/**
|
|
1033
|
-
* Generate an HTML page that renders Scalar API reference UI.
|
|
1034
|
-
* The spec is inlined as JSON and Scalar is loaded from CDN.
|
|
1035
|
-
*/
|
|
1036
|
-
function serveScalarUI(spec, config) {
|
|
1037
|
-
const title = config?.title ?? spec.info.title ?? "API Reference";
|
|
1038
|
-
const scalarConfig = JSON.stringify({
|
|
1039
|
-
theme: config?.theme ?? "purple",
|
|
1040
|
-
hideDownloadButton: config?.hideDownloadButton,
|
|
1041
|
-
defaultHttpClient: config?.defaultHttpClient,
|
|
1042
|
-
customCss: config?.customCss,
|
|
1043
|
-
content: spec
|
|
1044
|
-
});
|
|
1045
|
-
const html = `<!DOCTYPE html>
|
|
1046
|
-
<html>
|
|
1047
|
-
<head>
|
|
1048
|
-
<title>${escapeHtml(title)}</title>
|
|
1049
|
-
<meta charset="utf-8" />
|
|
1050
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1051
|
-
</head>
|
|
1052
|
-
<body>
|
|
1053
|
-
<script id="api-reference" data-configuration='${escapeAttr(scalarConfig)}'><\/script>
|
|
1054
|
-
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"><\/script>
|
|
1055
|
-
</body>
|
|
1056
|
-
</html>`;
|
|
1057
|
-
return new Response(html, { headers: { "Content-Type": "text/html; charset=utf-8" } });
|
|
1058
|
-
}
|
|
1059
|
-
function escapeHtml(str) {
|
|
1060
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1061
|
-
}
|
|
1062
|
-
function escapeAttr(str) {
|
|
1063
|
-
return str.replace(/&/g, "&").replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">");
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
//#endregion
|
|
1067
|
-
//#region src/server.ts
|
|
1068
|
-
/**
|
|
1069
|
-
* Identity factory for `config/openapi.ts` — provides type inference.
|
|
1070
|
-
*
|
|
1071
|
-
* @example
|
|
1072
|
-
* ```ts
|
|
1073
|
-
* // config/openapi.ts
|
|
1074
|
-
* import { openApiConfig } from "@questpie/openapi";
|
|
1075
|
-
*
|
|
1076
|
-
* export default openApiConfig({
|
|
1077
|
-
* info: { title: "My API", version: "1.0.0" },
|
|
1078
|
-
* scalar: { theme: "purple" },
|
|
1079
|
-
* });
|
|
1080
|
-
* ```
|
|
1081
|
-
*/
|
|
1082
|
-
function openApiConfig(config) {
|
|
1083
|
-
return config;
|
|
1084
|
-
}
|
|
1085
|
-
/**
|
|
1086
|
-
* Generate a complete OpenAPI 3.1 spec from a QUESTPIE app instance.
|
|
1087
|
-
* Routes are read from `app.config.routes` automatically.
|
|
1088
|
-
*
|
|
1089
|
-
* @example
|
|
1090
|
-
* ```ts
|
|
1091
|
-
* import { generateOpenApiSpec } from "@questpie/openapi";
|
|
1092
|
-
*
|
|
1093
|
-
* const spec = generateOpenApiSpec(app, {
|
|
1094
|
-
* info: { title: "My API", version: "1.0.0" },
|
|
1095
|
-
* });
|
|
1096
|
-
* ```
|
|
1097
|
-
*/
|
|
1098
|
-
function generateOpenApiSpec(app, config) {
|
|
1099
|
-
const routes = app.config?.routes;
|
|
1100
|
-
return generateOpenApiSpec$1(app, routes, config);
|
|
1101
|
-
}
|
|
1102
|
-
const specCache = /* @__PURE__ */ new WeakMap();
|
|
1103
|
-
function getCachedSpec(app, config) {
|
|
1104
|
-
const appObj = app;
|
|
1105
|
-
let cached = specCache.get(appObj);
|
|
1106
|
-
if (!cached) {
|
|
1107
|
-
const spec = generateOpenApiSpec(app, config);
|
|
1108
|
-
const json = JSON.stringify(spec);
|
|
1109
|
-
let hash = 0;
|
|
1110
|
-
for (let i = 0; i < json.length; i++) hash = (hash << 5) - hash + json.charCodeAt(i) | 0;
|
|
1111
|
-
cached = {
|
|
1112
|
-
json,
|
|
1113
|
-
etag: `"oapi-${(hash >>> 0).toString(36)}"`
|
|
1114
|
-
};
|
|
1115
|
-
specCache.set(appObj, cached);
|
|
1116
|
-
}
|
|
1117
|
-
return cached;
|
|
1118
|
-
}
|
|
1119
|
-
/**
|
|
1120
|
-
* Read OpenAPI config from `app.state.config.openapi` (set via config/openapi.ts).
|
|
1121
|
-
* Falls back to explicit config parameter if provided.
|
|
1122
|
-
*/
|
|
1123
|
-
function resolveOpenApiConfig(app, explicitConfig) {
|
|
1124
|
-
if (explicitConfig) return explicitConfig;
|
|
1125
|
-
return (app.state?.config)?.openapi;
|
|
1126
|
-
}
|
|
1127
|
-
/**
|
|
1128
|
-
* Create a route that serves the OpenAPI 3.1 JSON spec.
|
|
1129
|
-
* Place in your `routes/` directory for automatic discovery.
|
|
1130
|
-
*
|
|
1131
|
-
* Spec is lazy-generated on first request and cached with ETag.
|
|
1132
|
-
* Config is read from `config/openapi.ts` at request time, or from
|
|
1133
|
-
* explicit parameter if provided.
|
|
1134
|
-
*
|
|
1135
|
-
* @example
|
|
1136
|
-
* ```ts title="routes/openapi-spec.ts"
|
|
1137
|
-
* import { openApiRoute } from "@questpie/openapi";
|
|
1138
|
-
*
|
|
1139
|
-
* export default openApiRoute();
|
|
1140
|
-
* ```
|
|
1141
|
-
*/
|
|
1142
|
-
function openApiRoute(config) {
|
|
1143
|
-
return route().get().raw().handler(async (ctx) => {
|
|
1144
|
-
const app = ctx.app;
|
|
1145
|
-
const { json, etag } = getCachedSpec(app, resolveOpenApiConfig(app, config));
|
|
1146
|
-
if (ctx.request.headers.get("if-none-match") === etag) return new Response(null, { status: 304 });
|
|
1147
|
-
return new Response(json, { headers: {
|
|
1148
|
-
"Content-Type": "application/json",
|
|
1149
|
-
"Cache-Control": "public, max-age=3600, stale-while-revalidate=43200",
|
|
1150
|
-
ETag: etag,
|
|
1151
|
-
"Access-Control-Allow-Origin": "*"
|
|
1152
|
-
} });
|
|
1153
|
-
});
|
|
1154
|
-
}
|
|
1155
|
-
/**
|
|
1156
|
-
* Create a route that serves the Scalar interactive API docs.
|
|
1157
|
-
* Place in your `routes/` directory for automatic discovery.
|
|
1158
|
-
*
|
|
1159
|
-
* @example
|
|
1160
|
-
* ```ts title="routes/docs.ts"
|
|
1161
|
-
* import { docsRoute } from "@questpie/openapi";
|
|
1162
|
-
*
|
|
1163
|
-
* export default docsRoute();
|
|
1164
|
-
* ```
|
|
1165
|
-
*/
|
|
1166
|
-
function docsRoute(config) {
|
|
1167
|
-
const { scalar: scalarConfig, ...openApiConfig$1 } = config ?? {};
|
|
1168
|
-
return route().get().raw().handler(async (ctx) => {
|
|
1169
|
-
const app = ctx.app;
|
|
1170
|
-
const resolved = resolveOpenApiConfig(app, openApiConfig$1);
|
|
1171
|
-
const scalarOpts = scalarConfig ?? resolved?.scalar;
|
|
1172
|
-
return serveScalarUI(generateOpenApiSpec(app, resolved), scalarOpts);
|
|
1173
|
-
});
|
|
1174
|
-
}
|
|
1175
|
-
/**
|
|
1176
|
-
* OpenAPI module — registers spec + docs routes.
|
|
1177
|
-
*
|
|
1178
|
-
* Routes are served as:
|
|
1179
|
-
* - `GET /api/openapi.json` — OpenAPI 3.1 JSON spec
|
|
1180
|
-
* - `GET /api/docs` — Scalar interactive API reference
|
|
1181
|
-
*
|
|
1182
|
-
* Configure via `config/openapi.ts`:
|
|
1183
|
-
* ```ts
|
|
1184
|
-
* import { openApiConfig } from "@questpie/openapi";
|
|
1185
|
-
* export default openApiConfig({ info: { title: "My API", version: "1.0.0" } });
|
|
1186
|
-
* ```
|
|
1187
|
-
*
|
|
1188
|
-
* @example Static (reads config from config/openapi.ts):
|
|
1189
|
-
* ```ts title="questpie/server/modules.ts"
|
|
1190
|
-
* import { openApiModule } from "@questpie/openapi";
|
|
1191
|
-
* export default [openApiModule] as const;
|
|
1192
|
-
* ```
|
|
1193
|
-
*/
|
|
1194
|
-
const openApiModule = module({
|
|
1195
|
-
name: "questpie-openapi",
|
|
1196
|
-
plugin: openApiPlugin(),
|
|
1197
|
-
routes: {
|
|
1198
|
-
"openapi.json": openApiRoute(),
|
|
1199
|
-
docs: docsRoute()
|
|
1200
|
-
}
|
|
1201
|
-
});
|
|
1202
|
-
var server_default = openApiModule;
|
|
1203
|
-
|
|
1204
|
-
//#endregion
|
|
1205
3
|
export { server_default as default, docsRoute, generateOpenApiSpec, openApiConfig, openApiModule, openApiRoute };
|