peta-docs 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/elysia/index.d.mts +13 -0
- package/dist/elysia/index.mjs +35 -0
- package/dist/hono/index.d.mts +6 -10
- package/dist/hono/index.mjs +3 -303
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/route-Dd7Zp1HW.mjs +292 -0
- package/dist/{spec-Bobwv7gP.mjs → spec-CTxjSu78.mjs} +60 -37
- package/dist/{scanner-CU4MsJ2G.d.mts → types-ByYMmSfu.d.mts} +4 -9
- package/package.json +10 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 zfadhli
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { p as RouteScanner } from "../types-ByYMmSfu.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/elysia/scanner.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Scans an Elysia app instance and extracts route metadata.
|
|
6
|
+
*
|
|
7
|
+
* Relies on Elysia's internal `app.router.history` array. If that structure
|
|
8
|
+
* changes in a future Elysia version, this scanner will warn and
|
|
9
|
+
* return an empty array.
|
|
10
|
+
*/
|
|
11
|
+
declare const elysiaScanner: RouteScanner;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { elysiaScanner };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { n as getRouteMeta } from "../route-Dd7Zp1HW.mjs";
|
|
2
|
+
//#region src/elysia/scanner.ts
|
|
3
|
+
/**
|
|
4
|
+
* Scans an Elysia app instance and extracts route metadata.
|
|
5
|
+
*
|
|
6
|
+
* Relies on Elysia's internal `app.router.history` array. If that structure
|
|
7
|
+
* changes in a future Elysia version, this scanner will warn and
|
|
8
|
+
* return an empty array.
|
|
9
|
+
*/
|
|
10
|
+
const elysiaScanner = { scan(app) {
|
|
11
|
+
const entries = [];
|
|
12
|
+
if (app == null) return entries;
|
|
13
|
+
const rawRoutes = app.router?.history;
|
|
14
|
+
if (!Array.isArray(rawRoutes)) {
|
|
15
|
+
console.warn("[peta-docs] elysiaScanner: expected app.router.history to be an array, got " + typeof rawRoutes + ". Is this an Elysia app? Provide a custom RouteScanner for other frameworks.");
|
|
16
|
+
return entries;
|
|
17
|
+
}
|
|
18
|
+
for (const r of rawRoutes) {
|
|
19
|
+
const rec = r;
|
|
20
|
+
const handler = rec.handler;
|
|
21
|
+
const meta = typeof handler === "function" ? getRouteMeta(handler) : void 0;
|
|
22
|
+
if (meta) {
|
|
23
|
+
const path = typeof rec.path === "string" ? String(rec.path) : "";
|
|
24
|
+
const method = typeof rec.method === "string" ? String(rec.method) : "";
|
|
25
|
+
entries.push({
|
|
26
|
+
path,
|
|
27
|
+
method,
|
|
28
|
+
config: meta
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return entries;
|
|
33
|
+
} };
|
|
34
|
+
//#endregion
|
|
35
|
+
export { elysiaScanner };
|
package/dist/hono/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as FilterOperator, c as Pagination, d as RouteConfig, g as TypedContext, i as FilterFields, n as FieldsetParams, p as RouteScanner, t as ArkTypeSchema } from "../types-ByYMmSfu.mjs";
|
|
2
2
|
import { Context, Hono, MiddlewareHandler } from "hono";
|
|
3
3
|
|
|
4
4
|
//#region src/hono/loader.d.ts
|
|
@@ -45,13 +45,7 @@ declare function loadRoutes(app: AnyHono, dir: string, options?: {
|
|
|
45
45
|
}): Promise<void>;
|
|
46
46
|
//#endregion
|
|
47
47
|
//#region src/hono/route.d.ts
|
|
48
|
-
interface PaginationOptions {
|
|
49
|
-
maxLimit?: number;
|
|
50
|
-
defaultLimit?: number;
|
|
51
|
-
}
|
|
52
48
|
type ValidationErrorHandler = (issues: unknown[], c: Context) => Response | Promise<Response>;
|
|
53
|
-
/** @deprecated Use {@link RouteBuilder.onValidationError} on the route chain instead. Per-route handlers take precedence over the global handler. */
|
|
54
|
-
declare function setOnValidationError(handler: ValidationErrorHandler): () => void;
|
|
55
49
|
declare class RouteBuilder<B = undefined, Q = undefined, P = undefined, Hd = undefined, Pg extends Pagination | undefined = undefined, F = Record<string, unknown>, Sr = Record<string, unknown>, Ir = Record<string, unknown>, Fs = Record<string, unknown>> {
|
|
56
50
|
private _config;
|
|
57
51
|
private static readonly VALIDATOR_MAP;
|
|
@@ -77,8 +71,10 @@ declare class RouteBuilder<B = undefined, Q = undefined, P = undefined, Hd = und
|
|
|
77
71
|
}, Fs>;
|
|
78
72
|
fieldsets<R extends string[]>(resources: R): RouteBuilder<B, Q, P, Hd, Pg, F, Sr, Ir, FieldsetParams<R>>;
|
|
79
73
|
onValidationError(handler: ValidationErrorHandler): this;
|
|
80
|
-
|
|
81
|
-
|
|
74
|
+
paginated(options?: {
|
|
75
|
+
maxLimit?: number;
|
|
76
|
+
defaultLimit?: number;
|
|
77
|
+
}): RouteBuilder<B, Q, P, Hd, Pagination, F, Sr, Ir, Fs>;
|
|
82
78
|
handle(handler: (c: TypedContext<B, Q, P, Hd, Pg, F, Sr, Ir, Fs>) => Response | Promise<Response>): MiddlewareHandler;
|
|
83
79
|
private buildValidators;
|
|
84
80
|
private buildRouteConfig;
|
|
@@ -99,4 +95,4 @@ declare function getRouteMeta(handler: unknown): RouteConfig | undefined;
|
|
|
99
95
|
*/
|
|
100
96
|
declare const honoScanner: RouteScanner;
|
|
101
97
|
//#endregion
|
|
102
|
-
export {
|
|
98
|
+
export { RouteBuilder, type ValidationErrorHandler, getRouteMeta, honoScanner, loadRoutes, route };
|
package/dist/hono/index.mjs
CHANGED
|
@@ -1,308 +1,8 @@
|
|
|
1
|
-
import { n as setDefaultScanner } from "../spec-
|
|
2
|
-
import {
|
|
1
|
+
import { n as setDefaultScanner } from "../spec-CTxjSu78.mjs";
|
|
2
|
+
import { n as getRouteMeta, r as route, t as RouteBuilder } from "../route-Dd7Zp1HW.mjs";
|
|
3
3
|
import { readdirSync, statSync } from "node:fs";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { Hono } from "hono";
|
|
6
|
-
//#region src/hono/route.ts
|
|
7
|
-
const OPENAPI_META = Symbol("openapi-meta");
|
|
8
|
-
/** Parse a comma-separated string into a trimmed, non-empty array. */
|
|
9
|
-
function parseCommaSeparated(value) {
|
|
10
|
-
return value ? value.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Validate a value against a Standard Schema.
|
|
14
|
-
* Returns the validated value, or calls `onError` and returns a Response.
|
|
15
|
-
*/
|
|
16
|
-
async function validateOrError(schema, value, c, onError) {
|
|
17
|
-
const result = await schema["~standard"].validate(value);
|
|
18
|
-
if (Array.isArray(result)) return onError(result, c);
|
|
19
|
-
const r = result;
|
|
20
|
-
if (r.issues) return onError([...r.issues], c);
|
|
21
|
-
if ("value" in r && r.value !== void 0) return r.value;
|
|
22
|
-
return value;
|
|
23
|
-
}
|
|
24
|
-
let onValidationError = (issues, c) => {
|
|
25
|
-
return c.json({
|
|
26
|
-
error: "Validation failed",
|
|
27
|
-
issues
|
|
28
|
-
}, 400);
|
|
29
|
-
};
|
|
30
|
-
/** @deprecated Use {@link RouteBuilder.onValidationError} on the route chain instead. Per-route handlers take precedence over the global handler. */
|
|
31
|
-
function setOnValidationError(handler) {
|
|
32
|
-
const prev = onValidationError;
|
|
33
|
-
onValidationError = handler;
|
|
34
|
-
return () => {
|
|
35
|
-
onValidationError = prev;
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
const onResponseValidationError = (issues, c) => {
|
|
39
|
-
return c.json({
|
|
40
|
-
error: "Response validation failed",
|
|
41
|
-
issues
|
|
42
|
-
}, 500);
|
|
43
|
-
};
|
|
44
|
-
/** @internal */
|
|
45
|
-
function createValidator(target, schema, onError) {
|
|
46
|
-
return validator(target, async (value, c) => {
|
|
47
|
-
const validated = await validateOrError(schema, value, c, onError);
|
|
48
|
-
if (validated instanceof Response) return validated;
|
|
49
|
-
return validated;
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Creates a middleware that checks for the presence of auth credentials.
|
|
54
|
-
*
|
|
55
|
-
* This is a mechanism-level guard — it verifies that the expected auth
|
|
56
|
-
* header/cookie exists, but does NOT verify the credential itself
|
|
57
|
-
* (e.g., JWT validity, session data). The app's own middleware or
|
|
58
|
-
* handler is responsible for identity verification.
|
|
59
|
-
*/
|
|
60
|
-
function authGuard(schemes) {
|
|
61
|
-
return async (c, next) => {
|
|
62
|
-
for (const scheme of schemes) {
|
|
63
|
-
if (scheme === "bearerAuth") {
|
|
64
|
-
if (!c.req.header("Authorization")?.startsWith("Bearer ")) return c.json({ error: "unauthorized" }, 401);
|
|
65
|
-
}
|
|
66
|
-
if (scheme === "sessionAuth" || scheme === "cookieAuth") {
|
|
67
|
-
if (!c.req.header("Cookie")?.includes("session=")) return c.json({ error: "unauthorized" }, 401);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return await next();
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
var RouteBuilder = class RouteBuilder {
|
|
74
|
-
_config = { responses: {} };
|
|
75
|
-
static VALIDATOR_MAP = [
|
|
76
|
-
["requestBody", "json"],
|
|
77
|
-
["params", "param"],
|
|
78
|
-
["headers", "header"]
|
|
79
|
-
];
|
|
80
|
-
summary(s) {
|
|
81
|
-
this._config.summary = s;
|
|
82
|
-
return this;
|
|
83
|
-
}
|
|
84
|
-
description(s) {
|
|
85
|
-
this._config.description = s;
|
|
86
|
-
return this;
|
|
87
|
-
}
|
|
88
|
-
operationId(s) {
|
|
89
|
-
this._config.operationId = s;
|
|
90
|
-
return this;
|
|
91
|
-
}
|
|
92
|
-
tags(...t) {
|
|
93
|
-
this._config.tags = t;
|
|
94
|
-
return this;
|
|
95
|
-
}
|
|
96
|
-
deprecated(d = true) {
|
|
97
|
-
this._config.deprecated = d;
|
|
98
|
-
return this;
|
|
99
|
-
}
|
|
100
|
-
requestBody(schema) {
|
|
101
|
-
this._config.requestBody = schema;
|
|
102
|
-
return this;
|
|
103
|
-
}
|
|
104
|
-
query(schema) {
|
|
105
|
-
this._config.query = schema;
|
|
106
|
-
return this;
|
|
107
|
-
}
|
|
108
|
-
params(schema) {
|
|
109
|
-
this._config.params = schema;
|
|
110
|
-
return this;
|
|
111
|
-
}
|
|
112
|
-
headers(schema) {
|
|
113
|
-
this._config.headers = schema;
|
|
114
|
-
return this;
|
|
115
|
-
}
|
|
116
|
-
response(status, value) {
|
|
117
|
-
this._config.responses[String(status)] = value;
|
|
118
|
-
return this;
|
|
119
|
-
}
|
|
120
|
-
auth(scheme = "bearerAuth") {
|
|
121
|
-
const acc = this._config.security ?? [];
|
|
122
|
-
acc.push(scheme);
|
|
123
|
-
this._config.security = acc;
|
|
124
|
-
return this;
|
|
125
|
-
}
|
|
126
|
-
filter(name, schema, options) {
|
|
127
|
-
const acc = this._config.filters ?? [];
|
|
128
|
-
acc.push({
|
|
129
|
-
name,
|
|
130
|
-
schema,
|
|
131
|
-
operators: options?.operators ?? ["eq"]
|
|
132
|
-
});
|
|
133
|
-
this._config.filters = acc;
|
|
134
|
-
return this;
|
|
135
|
-
}
|
|
136
|
-
sort(fields) {
|
|
137
|
-
this._config.sort = fields;
|
|
138
|
-
return this;
|
|
139
|
-
}
|
|
140
|
-
include(relations) {
|
|
141
|
-
this._config.include = relations;
|
|
142
|
-
return this;
|
|
143
|
-
}
|
|
144
|
-
fieldsets(resources) {
|
|
145
|
-
this._config.fieldsets = resources;
|
|
146
|
-
return this;
|
|
147
|
-
}
|
|
148
|
-
onValidationError(handler) {
|
|
149
|
-
this._config.onValidationError = handler;
|
|
150
|
-
return this;
|
|
151
|
-
}
|
|
152
|
-
onResponseValidationError(handler) {
|
|
153
|
-
this._config.onResponseValidationError = handler;
|
|
154
|
-
return this;
|
|
155
|
-
}
|
|
156
|
-
paginated(options) {
|
|
157
|
-
this._config.pagination = {
|
|
158
|
-
maxLimit: options?.maxLimit ?? 100,
|
|
159
|
-
defaultLimit: options?.defaultLimit ?? 20
|
|
160
|
-
};
|
|
161
|
-
return this;
|
|
162
|
-
}
|
|
163
|
-
handle(handler) {
|
|
164
|
-
const onError = this._config.onValidationError ?? onValidationError;
|
|
165
|
-
const validators = this.buildValidators(onError);
|
|
166
|
-
if (this._config.security?.length) validators.unshift(authGuard(this._config.security));
|
|
167
|
-
const routeConfig = this.buildRouteConfig(handler);
|
|
168
|
-
const wrapped = this.composeHandler(validators, handler);
|
|
169
|
-
return this.attachRouteMeta(wrapped, routeConfig);
|
|
170
|
-
}
|
|
171
|
-
buildValidators(onError) {
|
|
172
|
-
const validators = [];
|
|
173
|
-
const querySchema = this._config.query;
|
|
174
|
-
const filters = this._config.filters;
|
|
175
|
-
const sortFields = this._config.sort;
|
|
176
|
-
const includeFields = this._config.include;
|
|
177
|
-
const fieldsetResources = this._config.fieldsets;
|
|
178
|
-
const pag = this._config.pagination;
|
|
179
|
-
if (querySchema || filters?.length || sortFields || includeFields || fieldsetResources || pag) validators.push(validator("query", async (value, c) => {
|
|
180
|
-
let merged = {};
|
|
181
|
-
const raw = value;
|
|
182
|
-
if (querySchema) {
|
|
183
|
-
const validated = await validateOrError(querySchema, value, c, onError);
|
|
184
|
-
if (validated instanceof Response) return validated;
|
|
185
|
-
merged = {
|
|
186
|
-
...merged,
|
|
187
|
-
...validated
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
if (filters) for (const filter of filters) for (const op of filter.operators) {
|
|
191
|
-
const paramName = op === "eq" ? filter.name : `${filter.name}__${op}`;
|
|
192
|
-
const val = raw[paramName];
|
|
193
|
-
if (val === void 0) continue;
|
|
194
|
-
if (op === "in") {
|
|
195
|
-
const items = parseCommaSeparated(val);
|
|
196
|
-
const validatedItems = [];
|
|
197
|
-
for (const item of items) {
|
|
198
|
-
const validated = await validateOrError(filter.schema, item, c, onError);
|
|
199
|
-
if (validated instanceof Response) return validated;
|
|
200
|
-
validatedItems.push(validated);
|
|
201
|
-
}
|
|
202
|
-
merged[paramName] = validatedItems;
|
|
203
|
-
} else {
|
|
204
|
-
const validated = await validateOrError(filter.schema, val, c, onError);
|
|
205
|
-
if (validated instanceof Response) return validated;
|
|
206
|
-
merged[paramName] = validated;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
if (sortFields) {
|
|
210
|
-
const sortVal = raw.sort;
|
|
211
|
-
if (sortVal !== void 0) {
|
|
212
|
-
const parts = parseCommaSeparated(sortVal);
|
|
213
|
-
const allowed = new Set(sortFields.flatMap((f) => [f, `-${f}`]));
|
|
214
|
-
for (const part of parts) if (!allowed.has(part)) return onError([{ message: `Invalid sort field "${part}". Allowed: ${[...allowed].join(", ")}` }], c);
|
|
215
|
-
merged.sort = parts;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
const includeFields = this._config.include;
|
|
219
|
-
if (includeFields) {
|
|
220
|
-
const rawInclude = raw.include;
|
|
221
|
-
if (rawInclude !== void 0) {
|
|
222
|
-
const parts = parseCommaSeparated(rawInclude);
|
|
223
|
-
const allowed = new Set(includeFields);
|
|
224
|
-
for (const part of parts) if (!allowed.has(part)) return onError([{ message: `Invalid include "${part}". Allowed: ${[...allowed].join(", ")}` }], c);
|
|
225
|
-
merged.include = parts;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
const fieldsetResources = this._config.fieldsets;
|
|
229
|
-
if (fieldsetResources) for (const resource of fieldsetResources) {
|
|
230
|
-
const paramName = `fields[${resource}]`;
|
|
231
|
-
const val = raw[paramName];
|
|
232
|
-
if (val !== void 0 && typeof val !== "string") return onError([{ message: `"${paramName}" must be a string` }], c);
|
|
233
|
-
if (val !== void 0) merged[paramName] = val;
|
|
234
|
-
}
|
|
235
|
-
if (pag) {
|
|
236
|
-
const page = Math.floor(Number(raw.page ?? "1"));
|
|
237
|
-
const limit = Math.min(pag.maxLimit, Math.max(1, Math.floor(Number(raw.limit ?? String(pag.defaultLimit)))));
|
|
238
|
-
merged.page = Number.isFinite(page) && page >= 1 ? page : 1;
|
|
239
|
-
merged.limit = Number.isFinite(limit) ? limit : pag.defaultLimit;
|
|
240
|
-
merged.offset = (merged.page - 1) * merged.limit;
|
|
241
|
-
}
|
|
242
|
-
return merged;
|
|
243
|
-
}));
|
|
244
|
-
for (const [key, target] of RouteBuilder.VALIDATOR_MAP) {
|
|
245
|
-
const schema = this._config[key];
|
|
246
|
-
if (schema != null) validators.push(createValidator(target, schema, onError));
|
|
247
|
-
}
|
|
248
|
-
return validators;
|
|
249
|
-
}
|
|
250
|
-
buildRouteConfig(handler) {
|
|
251
|
-
return {
|
|
252
|
-
...this._config,
|
|
253
|
-
responses: this._config.responses,
|
|
254
|
-
pagination: this._config.pagination,
|
|
255
|
-
filters: this._config.filters,
|
|
256
|
-
sort: this._config.sort,
|
|
257
|
-
include: this._config.include,
|
|
258
|
-
fieldsets: this._config.fieldsets,
|
|
259
|
-
security: this._config.security,
|
|
260
|
-
handler
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
composeHandler(validators, handler) {
|
|
264
|
-
return async (c, _next) => {
|
|
265
|
-
const run = async (i) => {
|
|
266
|
-
if (i < validators.length) return await validators[i](c, run.bind(null, i + 1)) ?? void 0;
|
|
267
|
-
const response = await handler(c) ?? void 0;
|
|
268
|
-
if (response) {
|
|
269
|
-
const onError = this._config.onResponseValidationError ?? onResponseValidationError;
|
|
270
|
-
return await this.validateResponse(response, c, onError) ?? void 0;
|
|
271
|
-
}
|
|
272
|
-
return response;
|
|
273
|
-
};
|
|
274
|
-
return run(0);
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
async validateResponse(response, c, onError) {
|
|
278
|
-
if (!(response.headers.get("content-type") ?? "").includes("application/json")) return response;
|
|
279
|
-
if (response.status === 204) return response;
|
|
280
|
-
const schema = this._config.responses[String(response.status)];
|
|
281
|
-
if (!schema || typeof schema !== "object" && typeof schema !== "function" || !("~standard" in schema)) return response;
|
|
282
|
-
try {
|
|
283
|
-
const validated = await validateOrError(schema, await response.clone().json(), c, onError);
|
|
284
|
-
if (validated instanceof Response) return validated;
|
|
285
|
-
return response;
|
|
286
|
-
} catch {
|
|
287
|
-
return response;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
attachRouteMeta(handler, config) {
|
|
291
|
-
Object.defineProperty(handler, OPENAPI_META, {
|
|
292
|
-
value: config,
|
|
293
|
-
writable: false
|
|
294
|
-
});
|
|
295
|
-
return handler;
|
|
296
|
-
}
|
|
297
|
-
};
|
|
298
|
-
function route() {
|
|
299
|
-
return new RouteBuilder();
|
|
300
|
-
}
|
|
301
|
-
function getRouteMeta(handler) {
|
|
302
|
-
if (typeof handler !== "function") return void 0;
|
|
303
|
-
return handler[OPENAPI_META];
|
|
304
|
-
}
|
|
305
|
-
//#endregion
|
|
306
6
|
//#region src/hono/scanner.ts
|
|
307
7
|
function routeProp(obj, key) {
|
|
308
8
|
if (obj == null) return void 0;
|
|
@@ -432,4 +132,4 @@ async function loadRoutes(app, dir, options) {
|
|
|
432
132
|
*/
|
|
433
133
|
setDefaultScanner(honoScanner);
|
|
434
134
|
//#endregion
|
|
435
|
-
export { RouteBuilder, getRouteMeta, honoScanner, loadRoutes, route
|
|
135
|
+
export { RouteBuilder, getRouteMeta, honoScanner, loadRoutes, route };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as FilterOperator, c as Pagination, d as RouteConfig, f as RouteEntry, g as TypedContext, h as StatusCode, i as FilterFields, l as PathItemObject, m as SchemaObject, n as FieldsetParams, o as InfoObject, p as RouteScanner, r as FilterDef, s as OpenAPIObject, t as ArkTypeSchema, u as ResponseValue } from "./types-ByYMmSfu.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/scalar.d.ts
|
|
4
4
|
interface ScalarUIOptions {
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as toOpenAPISchema, r as buildOpenAPISpec, t as getOpenAPISpec } from "./spec-
|
|
1
|
+
import { i as toOpenAPISchema, r as buildOpenAPISpec, t as getOpenAPISpec } from "./spec-CTxjSu78.mjs";
|
|
2
2
|
//#region src/scalar.ts
|
|
3
3
|
function escapeHtml(str) {
|
|
4
4
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { validator } from "hono/validator";
|
|
2
|
+
//#region src/hono/route.ts
|
|
3
|
+
const OPENAPI_META = Symbol("openapi-meta");
|
|
4
|
+
/** Parse a comma-separated string into a trimmed, non-empty array. */
|
|
5
|
+
function parseCommaSeparated(value) {
|
|
6
|
+
return value ? value.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Validate a value against a Standard Schema.
|
|
10
|
+
* Returns the validated value, or calls `onError` and returns a Response.
|
|
11
|
+
*/
|
|
12
|
+
async function validateOrError(schema, value, c, onError) {
|
|
13
|
+
const result = await schema["~standard"].validate(value);
|
|
14
|
+
if (Array.isArray(result)) return onError(result, c);
|
|
15
|
+
const r = result;
|
|
16
|
+
if (r.issues) return onError([...r.issues], c);
|
|
17
|
+
if ("value" in r && r.value !== void 0) return r.value;
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
const onValidationError = (issues, c) => {
|
|
21
|
+
return c.json({
|
|
22
|
+
error: "Validation failed",
|
|
23
|
+
issues
|
|
24
|
+
}, 400);
|
|
25
|
+
};
|
|
26
|
+
const onResponseValidationError = (issues, c) => {
|
|
27
|
+
return c.json({
|
|
28
|
+
error: "Response validation failed",
|
|
29
|
+
issues
|
|
30
|
+
}, 500);
|
|
31
|
+
};
|
|
32
|
+
/** @internal */
|
|
33
|
+
function createValidator(target, schema, onError) {
|
|
34
|
+
return validator(target, async (value, c) => {
|
|
35
|
+
const validated = await validateOrError(schema, value, c, onError);
|
|
36
|
+
if (validated instanceof Response) return validated;
|
|
37
|
+
return validated;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Creates a middleware that checks for the presence of auth credentials.
|
|
42
|
+
*
|
|
43
|
+
* This is a mechanism-level guard — it verifies that the expected auth
|
|
44
|
+
* header/cookie exists, but does NOT verify the credential itself
|
|
45
|
+
* (e.g., JWT validity, session data). The app's own middleware or
|
|
46
|
+
* handler is responsible for identity verification.
|
|
47
|
+
*/
|
|
48
|
+
function authGuard(schemes) {
|
|
49
|
+
return async (c, next) => {
|
|
50
|
+
for (const scheme of schemes) {
|
|
51
|
+
if (scheme === "bearerAuth") {
|
|
52
|
+
if (!c.req.header("Authorization")?.startsWith("Bearer ")) return c.json({ error: "unauthorized" }, 401);
|
|
53
|
+
}
|
|
54
|
+
if (scheme === "sessionAuth" || scheme === "cookieAuth") {
|
|
55
|
+
const cookie = c.req.header("Cookie");
|
|
56
|
+
if (!cookie) return c.json({ error: "unauthorized" }, 401);
|
|
57
|
+
if (!cookie.split(";").some((pair) => pair.trim().split("=")[0] === "session")) return c.json({ error: "unauthorized" }, 401);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return await next();
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
var RouteBuilder = class RouteBuilder {
|
|
64
|
+
_config = { responses: {} };
|
|
65
|
+
static VALIDATOR_MAP = [
|
|
66
|
+
["requestBody", "json"],
|
|
67
|
+
["params", "param"],
|
|
68
|
+
["headers", "header"]
|
|
69
|
+
];
|
|
70
|
+
summary(s) {
|
|
71
|
+
this._config.summary = s;
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
description(s) {
|
|
75
|
+
this._config.description = s;
|
|
76
|
+
return this;
|
|
77
|
+
}
|
|
78
|
+
operationId(s) {
|
|
79
|
+
this._config.operationId = s;
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
tags(...t) {
|
|
83
|
+
this._config.tags = t;
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
deprecated(d = true) {
|
|
87
|
+
this._config.deprecated = d;
|
|
88
|
+
return this;
|
|
89
|
+
}
|
|
90
|
+
requestBody(schema) {
|
|
91
|
+
this._config.requestBody = schema;
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
query(schema) {
|
|
95
|
+
this._config.query = schema;
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
params(schema) {
|
|
99
|
+
this._config.params = schema;
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
headers(schema) {
|
|
103
|
+
this._config.headers = schema;
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
106
|
+
response(status, value) {
|
|
107
|
+
this._config.responses[String(status)] = value;
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
auth(scheme = "bearerAuth") {
|
|
111
|
+
const acc = this._config.security ?? [];
|
|
112
|
+
acc.push(scheme);
|
|
113
|
+
this._config.security = acc;
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
filter(name, schema, options) {
|
|
117
|
+
const acc = this._config.filters ?? [];
|
|
118
|
+
acc.push({
|
|
119
|
+
name,
|
|
120
|
+
schema,
|
|
121
|
+
operators: options?.operators ?? ["eq"]
|
|
122
|
+
});
|
|
123
|
+
this._config.filters = acc;
|
|
124
|
+
return this;
|
|
125
|
+
}
|
|
126
|
+
sort(fields) {
|
|
127
|
+
this._config.sort = fields;
|
|
128
|
+
return this;
|
|
129
|
+
}
|
|
130
|
+
include(relations) {
|
|
131
|
+
this._config.include = relations;
|
|
132
|
+
return this;
|
|
133
|
+
}
|
|
134
|
+
fieldsets(resources) {
|
|
135
|
+
this._config.fieldsets = resources;
|
|
136
|
+
return this;
|
|
137
|
+
}
|
|
138
|
+
onValidationError(handler) {
|
|
139
|
+
this._config.onValidationError = handler;
|
|
140
|
+
return this;
|
|
141
|
+
}
|
|
142
|
+
paginated(options) {
|
|
143
|
+
this._config.pagination = {
|
|
144
|
+
maxLimit: options?.maxLimit ?? 100,
|
|
145
|
+
defaultLimit: options?.defaultLimit ?? 20
|
|
146
|
+
};
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
handle(handler) {
|
|
150
|
+
const onError = this._config.onValidationError ?? onValidationError;
|
|
151
|
+
const validators = this.buildValidators(onError);
|
|
152
|
+
if (this._config.security?.length) validators.unshift(authGuard(this._config.security));
|
|
153
|
+
const routeConfig = this.buildRouteConfig(handler);
|
|
154
|
+
const wrapped = this.composeHandler(validators, handler);
|
|
155
|
+
return this.attachRouteMeta(wrapped, routeConfig);
|
|
156
|
+
}
|
|
157
|
+
buildValidators(onError) {
|
|
158
|
+
const validators = [];
|
|
159
|
+
const querySchema = this._config.query;
|
|
160
|
+
const filters = this._config.filters;
|
|
161
|
+
const sortFields = this._config.sort;
|
|
162
|
+
const includeFields = this._config.include;
|
|
163
|
+
const fieldsetResources = this._config.fieldsets;
|
|
164
|
+
const pag = this._config.pagination;
|
|
165
|
+
if (querySchema || filters?.length || sortFields || includeFields || fieldsetResources || pag) validators.push(validator("query", async (value, c) => {
|
|
166
|
+
let merged = {};
|
|
167
|
+
const raw = value;
|
|
168
|
+
if (querySchema) {
|
|
169
|
+
const validated = await validateOrError(querySchema, value, c, onError);
|
|
170
|
+
if (validated instanceof Response) return validated;
|
|
171
|
+
merged = {
|
|
172
|
+
...merged,
|
|
173
|
+
...validated
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (filters) for (const filter of filters) for (const op of filter.operators) {
|
|
177
|
+
const paramName = op === "eq" ? filter.name : `${filter.name}__${op}`;
|
|
178
|
+
const val = raw[paramName];
|
|
179
|
+
if (val === void 0) continue;
|
|
180
|
+
if (op === "in") {
|
|
181
|
+
const items = parseCommaSeparated(val);
|
|
182
|
+
const validatedItems = [];
|
|
183
|
+
for (const item of items) {
|
|
184
|
+
const validated = await validateOrError(filter.schema, item, c, onError);
|
|
185
|
+
if (validated instanceof Response) return validated;
|
|
186
|
+
validatedItems.push(validated);
|
|
187
|
+
}
|
|
188
|
+
merged[paramName] = validatedItems;
|
|
189
|
+
} else {
|
|
190
|
+
const validated = await validateOrError(filter.schema, val, c, onError);
|
|
191
|
+
if (validated instanceof Response) return validated;
|
|
192
|
+
merged[paramName] = validated;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (sortFields) {
|
|
196
|
+
const sortVal = raw.sort;
|
|
197
|
+
if (sortVal !== void 0) {
|
|
198
|
+
const parts = parseCommaSeparated(sortVal);
|
|
199
|
+
const allowed = new Set(sortFields.flatMap((f) => [f, `-${f}`]));
|
|
200
|
+
for (const part of parts) if (!allowed.has(part)) return onError([{ message: `Invalid sort field "${part}". Allowed: ${[...allowed].join(", ")}` }], c);
|
|
201
|
+
merged.sort = parts;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const includeFields = this._config.include;
|
|
205
|
+
if (includeFields) {
|
|
206
|
+
const rawInclude = raw.include;
|
|
207
|
+
if (rawInclude !== void 0) {
|
|
208
|
+
const parts = parseCommaSeparated(rawInclude);
|
|
209
|
+
const allowed = new Set(includeFields);
|
|
210
|
+
for (const part of parts) if (!allowed.has(part)) return onError([{ message: `Invalid include "${part}". Allowed: ${[...allowed].join(", ")}` }], c);
|
|
211
|
+
merged.include = parts;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const fieldsetResources = this._config.fieldsets;
|
|
215
|
+
if (fieldsetResources) for (const resource of fieldsetResources) {
|
|
216
|
+
const paramName = `fields[${resource}]`;
|
|
217
|
+
const val = raw[paramName];
|
|
218
|
+
if (val !== void 0 && typeof val !== "string") return onError([{ message: `"${paramName}" must be a string` }], c);
|
|
219
|
+
if (val !== void 0) merged[paramName] = val;
|
|
220
|
+
}
|
|
221
|
+
if (pag) {
|
|
222
|
+
const page = Math.floor(Number(raw.page ?? "1"));
|
|
223
|
+
const limit = Math.min(pag.maxLimit, Math.max(1, Math.floor(Number(raw.limit ?? String(pag.defaultLimit)))));
|
|
224
|
+
merged.page = Number.isFinite(page) && page >= 1 ? page : 1;
|
|
225
|
+
merged.limit = Number.isFinite(limit) ? limit : pag.defaultLimit;
|
|
226
|
+
merged.offset = (merged.page - 1) * merged.limit;
|
|
227
|
+
}
|
|
228
|
+
return merged;
|
|
229
|
+
}));
|
|
230
|
+
for (const [key, target] of RouteBuilder.VALIDATOR_MAP) {
|
|
231
|
+
const schema = this._config[key];
|
|
232
|
+
if (schema != null) validators.push(createValidator(target, schema, onError));
|
|
233
|
+
}
|
|
234
|
+
return validators;
|
|
235
|
+
}
|
|
236
|
+
buildRouteConfig(handler) {
|
|
237
|
+
return {
|
|
238
|
+
...this._config,
|
|
239
|
+
responses: this._config.responses,
|
|
240
|
+
pagination: this._config.pagination,
|
|
241
|
+
filters: this._config.filters,
|
|
242
|
+
sort: this._config.sort,
|
|
243
|
+
include: this._config.include,
|
|
244
|
+
fieldsets: this._config.fieldsets,
|
|
245
|
+
security: this._config.security,
|
|
246
|
+
handler
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
composeHandler(validators, handler) {
|
|
250
|
+
return async (c, _next) => {
|
|
251
|
+
const run = async (i) => {
|
|
252
|
+
if (i < validators.length) return await validators[i](c, run.bind(null, i + 1)) ?? void 0;
|
|
253
|
+
const response = await handler(c) ?? void 0;
|
|
254
|
+
if (response) {
|
|
255
|
+
const onError = onResponseValidationError;
|
|
256
|
+
return await this.validateResponse(response, c, onError) ?? void 0;
|
|
257
|
+
}
|
|
258
|
+
return response;
|
|
259
|
+
};
|
|
260
|
+
return run(0);
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
async validateResponse(response, c, onError) {
|
|
264
|
+
if (!(response.headers.get("content-type") ?? "").includes("application/json")) return response;
|
|
265
|
+
if (response.status === 204) return response;
|
|
266
|
+
const schema = this._config.responses[String(response.status)];
|
|
267
|
+
if (!schema || typeof schema !== "object" && typeof schema !== "function" || !("~standard" in schema)) return response;
|
|
268
|
+
try {
|
|
269
|
+
const validated = await validateOrError(schema, await response.clone().json(), c, onError);
|
|
270
|
+
if (validated instanceof Response) return validated;
|
|
271
|
+
return response;
|
|
272
|
+
} catch {
|
|
273
|
+
return response;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
attachRouteMeta(handler, config) {
|
|
277
|
+
Object.defineProperty(handler, OPENAPI_META, {
|
|
278
|
+
value: config,
|
|
279
|
+
writable: false
|
|
280
|
+
});
|
|
281
|
+
return handler;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
function route() {
|
|
285
|
+
return new RouteBuilder();
|
|
286
|
+
}
|
|
287
|
+
function getRouteMeta(handler) {
|
|
288
|
+
if (typeof handler !== "function") return void 0;
|
|
289
|
+
return handler[OPENAPI_META];
|
|
290
|
+
}
|
|
291
|
+
//#endregion
|
|
292
|
+
export { getRouteMeta as n, route as r, RouteBuilder as t };
|
|
@@ -24,9 +24,7 @@ function toOpenAPISchema(schema) {
|
|
|
24
24
|
return {};
|
|
25
25
|
}
|
|
26
26
|
function mapContentSchemas(content, convert) {
|
|
27
|
-
|
|
28
|
-
for (const [mediaType, { schema }] of Object.entries(content)) result[mediaType] = { schema: convert(schema) };
|
|
29
|
-
return result;
|
|
27
|
+
return Object.fromEntries(Object.entries(content).map(([mt, v]) => [mt, { schema: convert(v.schema) }]));
|
|
30
28
|
}
|
|
31
29
|
const STATUS_DESCRIPTIONS = {
|
|
32
30
|
"200": "OK",
|
|
@@ -73,14 +71,7 @@ function honoPathToOpenAPI(path) {
|
|
|
73
71
|
return path.replace(/:(\w+)/g, "{$1}");
|
|
74
72
|
}
|
|
75
73
|
function parsePathParams(path) {
|
|
76
|
-
|
|
77
|
-
const regex = /{(\w+)}/g;
|
|
78
|
-
let match = regex.exec(path);
|
|
79
|
-
while (match !== null) {
|
|
80
|
-
params.push(match[1]);
|
|
81
|
-
match = regex.exec(path);
|
|
82
|
-
}
|
|
83
|
-
return params;
|
|
74
|
+
return [...path.matchAll(/{(\w+)}/g)].map((m) => m[1]);
|
|
84
75
|
}
|
|
85
76
|
function extractProperties(schema) {
|
|
86
77
|
const props = schema.properties;
|
|
@@ -93,7 +84,7 @@ function extractProperties(schema) {
|
|
|
93
84
|
}));
|
|
94
85
|
}
|
|
95
86
|
function autoOperationId(method, path) {
|
|
96
|
-
const segments = path.replace(/{(\w+)}/g, (_m, name) => `By${name[0].toUpperCase() + name.slice(1)}`).replace(
|
|
87
|
+
const segments = path.replace(/{(\w+)}/g, (_m, name) => `By${name[0].toUpperCase() + name.slice(1)}`).replace(/\/+/g, "/").replace(/^\//, "").split("/").filter(Boolean);
|
|
97
88
|
return method.toLowerCase() + segments.map((s) => s[0].toUpperCase() + s.slice(1)).join("");
|
|
98
89
|
}
|
|
99
90
|
function autoTags(path, basePath) {
|
|
@@ -108,10 +99,37 @@ const METHOD_ORDER = {
|
|
|
108
99
|
delete: 3,
|
|
109
100
|
patch: 4
|
|
110
101
|
};
|
|
102
|
+
const VALID_METHODS = new Set(Object.keys(METHOD_ORDER));
|
|
111
103
|
function buildOpenAPISpec(routes, info, options) {
|
|
112
104
|
const basePath = options?.basePath ?? "/api";
|
|
113
105
|
const s = (schema) => toOpenAPISchema(schema);
|
|
114
106
|
const paths = {};
|
|
107
|
+
const schemaCount = /* @__PURE__ */ new Map();
|
|
108
|
+
for (const { config } of routes) {
|
|
109
|
+
if (config.requestBody) {
|
|
110
|
+
const normalized = normalizeRequestBody(config.requestBody);
|
|
111
|
+
if (normalized?.content) {
|
|
112
|
+
for (const { schema } of Object.values(normalized.content)) if (typeof schema === "function" && "toJsonSchema" in schema) schemaCount.set(schema, (schemaCount.get(schema) ?? 0) + 1);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const [status, rawResponse] of Object.entries(config.responses)) {
|
|
116
|
+
const normalized = normalizeResponse(status, rawResponse);
|
|
117
|
+
if (normalized?.content) {
|
|
118
|
+
for (const { schema } of Object.values(normalized.content)) if (typeof schema === "function" && "toJsonSchema" in schema) schemaCount.set(schema, (schemaCount.get(schema) ?? 0) + 1);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const schemaRegistry = /* @__PURE__ */ new Map();
|
|
123
|
+
const componentSchemas = {};
|
|
124
|
+
function refOrInline(schema) {
|
|
125
|
+
if (!(typeof schema === "function" && "toJsonSchema" in schema)) return toOpenAPISchema(schema);
|
|
126
|
+
if ((schemaCount.get(schema) ?? 0) <= 1) return toOpenAPISchema(schema);
|
|
127
|
+
if (schemaRegistry.has(schema)) return { $ref: `#/components/schemas/${schemaRegistry.get(schema)}` };
|
|
128
|
+
const name = `Schema${schemaRegistry.size + 1}`;
|
|
129
|
+
schemaRegistry.set(schema, name);
|
|
130
|
+
componentSchemas[name] = toOpenAPISchema(schema);
|
|
131
|
+
return { $ref: `#/components/schemas/${name}` };
|
|
132
|
+
}
|
|
115
133
|
const tagged = routes.map((r) => ({
|
|
116
134
|
...r,
|
|
117
135
|
tag: (r.config.tags ?? autoTags(r.path, basePath))[0] ?? "__untagged"
|
|
@@ -181,27 +199,37 @@ function buildOpenAPISpec(routes, info, options) {
|
|
|
181
199
|
});
|
|
182
200
|
}
|
|
183
201
|
if (config.sort) {
|
|
184
|
-
const
|
|
202
|
+
const itemEnum = config.sort.flatMap((f) => [f, `-${f}`]);
|
|
185
203
|
parameters.push({
|
|
186
204
|
name: "sort",
|
|
187
205
|
in: "query",
|
|
188
206
|
required: false,
|
|
207
|
+
style: "form",
|
|
208
|
+
explode: false,
|
|
189
209
|
schema: {
|
|
190
|
-
type: "
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
210
|
+
type: "array",
|
|
211
|
+
items: {
|
|
212
|
+
type: "string",
|
|
213
|
+
enum: itemEnum
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
description: "Comma-separated sort fields. Prefix with - for descending."
|
|
194
217
|
});
|
|
195
218
|
}
|
|
196
219
|
if (config.include) parameters.push({
|
|
197
220
|
name: "include",
|
|
198
221
|
in: "query",
|
|
199
222
|
required: false,
|
|
223
|
+
style: "form",
|
|
224
|
+
explode: false,
|
|
200
225
|
schema: {
|
|
201
|
-
type: "
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
226
|
+
type: "array",
|
|
227
|
+
items: {
|
|
228
|
+
type: "string",
|
|
229
|
+
enum: config.include
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
description: "Comma-separated related resources to sideload."
|
|
205
233
|
});
|
|
206
234
|
if (config.fieldsets) for (const resource of config.fieldsets) parameters.push({
|
|
207
235
|
name: `fields[${resource}]`,
|
|
@@ -224,6 +252,7 @@ function buildOpenAPISpec(routes, info, options) {
|
|
|
224
252
|
const operation = { operationId: config.operationId ?? autoOperationId(method, path) };
|
|
225
253
|
if (config.summary) operation.summary = config.summary;
|
|
226
254
|
if (config.description) operation.description = config.description;
|
|
255
|
+
if (config.deprecated) operation.deprecated = config.deprecated;
|
|
227
256
|
if (config.tags) operation.tags = config.tags;
|
|
228
257
|
else operation.tags = autoTags(path, basePath);
|
|
229
258
|
if (config.security) operation.security = [{ ...Object.fromEntries(config.security.map((s) => [s, []])) }];
|
|
@@ -233,48 +262,42 @@ function buildOpenAPISpec(routes, info, options) {
|
|
|
233
262
|
if (normalized) operation.requestBody = {
|
|
234
263
|
description: normalized.description,
|
|
235
264
|
required: normalized.required,
|
|
236
|
-
content: mapContentSchemas(normalized.content,
|
|
265
|
+
content: mapContentSchemas(normalized.content, refOrInline)
|
|
237
266
|
};
|
|
238
267
|
}
|
|
239
268
|
const responses = {};
|
|
240
269
|
for (const [status, rawResponse] of Object.entries(config.responses)) {
|
|
241
270
|
const normalized = normalizeResponse(status, rawResponse);
|
|
242
271
|
const resp = { description: normalized.description };
|
|
243
|
-
if (normalized.content) resp.content = mapContentSchemas(normalized.content,
|
|
272
|
+
if (normalized.content) resp.content = mapContentSchemas(normalized.content, refOrInline);
|
|
244
273
|
responses[status] = resp;
|
|
245
274
|
}
|
|
246
275
|
operation.responses = responses;
|
|
247
276
|
const methodLower = method.toLowerCase();
|
|
248
|
-
|
|
249
|
-
"get",
|
|
250
|
-
"post",
|
|
251
|
-
"put",
|
|
252
|
-
"delete",
|
|
253
|
-
"patch"
|
|
254
|
-
].includes(methodLower) ? methodLower : void 0;
|
|
255
|
-
if (key) pathItem[key] = operation;
|
|
277
|
+
if (VALID_METHODS.has(methodLower)) pathItem[methodLower] = operation;
|
|
256
278
|
}
|
|
257
279
|
const spec = {
|
|
258
280
|
openapi: "3.1.0",
|
|
259
281
|
info,
|
|
260
282
|
paths
|
|
261
283
|
};
|
|
262
|
-
if (
|
|
284
|
+
if (Object.keys(componentSchemas).length > 0) spec.components = {
|
|
285
|
+
...options?.components ?? {},
|
|
286
|
+
schemas: componentSchemas
|
|
287
|
+
};
|
|
288
|
+
else if (options?.components) spec.components = options.components;
|
|
263
289
|
return spec;
|
|
264
290
|
}
|
|
265
291
|
//#endregion
|
|
266
292
|
//#region src/spec.ts
|
|
267
293
|
let _defaultScanner = null;
|
|
268
|
-
/**
|
|
269
|
-
* Register a default scanner for `getOpenAPISpec`.
|
|
270
|
-
* Called automatically by framework adapter modules (e.g., `peta-docs/hono`).
|
|
271
|
-
*/
|
|
294
|
+
/** @deprecated Import 'peta-docs/hono' which auto-registers the Hono scanner. Pass the scanner as the 3rd argument to getOpenAPISpec instead. */
|
|
272
295
|
function setDefaultScanner(scanner) {
|
|
273
296
|
_defaultScanner = scanner;
|
|
274
297
|
}
|
|
275
298
|
function getOpenAPISpec(app, info, scanner, options) {
|
|
276
299
|
const active = scanner ?? _defaultScanner;
|
|
277
|
-
if (!active) throw new Error("No RouteScanner provided.
|
|
300
|
+
if (!active) throw new Error("No RouteScanner provided. Import 'peta-docs/hono' or pass a scanner explicitly.");
|
|
278
301
|
return buildOpenAPISpec(active.scan(app), info, options);
|
|
279
302
|
}
|
|
280
303
|
//#endregion
|
|
@@ -51,6 +51,8 @@ interface ParameterObject {
|
|
|
51
51
|
description?: string;
|
|
52
52
|
required?: boolean;
|
|
53
53
|
deprecated?: boolean;
|
|
54
|
+
style?: string;
|
|
55
|
+
explode?: boolean;
|
|
54
56
|
schema: SchemaObject;
|
|
55
57
|
}
|
|
56
58
|
interface MediaTypeObject {
|
|
@@ -113,6 +115,7 @@ interface RouteConfig {
|
|
|
113
115
|
description?: string;
|
|
114
116
|
operationId?: string;
|
|
115
117
|
tags?: string[];
|
|
118
|
+
deprecated?: boolean;
|
|
116
119
|
query?: unknown;
|
|
117
120
|
params?: unknown;
|
|
118
121
|
headers?: unknown;
|
|
@@ -140,16 +143,8 @@ interface RouteEntry {
|
|
|
140
143
|
method: string;
|
|
141
144
|
config: RouteConfig;
|
|
142
145
|
}
|
|
143
|
-
//#endregion
|
|
144
|
-
//#region src/scanner.d.ts
|
|
145
|
-
/**
|
|
146
|
-
* Scans a framework app instance and extracts registered routes.
|
|
147
|
-
*
|
|
148
|
-
* Each framework adapter implements this interface to bridge the
|
|
149
|
-
* framework-specific route registry to the generic OpenAPI pipeline.
|
|
150
|
-
*/
|
|
151
146
|
interface RouteScanner {
|
|
152
147
|
scan(app: unknown): RouteEntry[];
|
|
153
148
|
}
|
|
154
149
|
//#endregion
|
|
155
|
-
export {
|
|
150
|
+
export { FilterOperator as a, Pagination as c, RouteConfig as d, RouteEntry as f, TypedContext as g, StatusCode as h, FilterFields as i, PathItemObject as l, SchemaObject as m, FieldsetParams as n, InfoObject as o, RouteScanner as p, FilterDef as r, OpenAPIObject as s, ArkTypeSchema as t, ResponseValue as u };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peta-docs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "OpenAPI + Scalar docs for Hono (and more), powered by Standard Schema",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"dist/",
|
|
14
|
-
"README.md"
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
15
16
|
],
|
|
16
17
|
"main": "./dist/index.mjs",
|
|
17
18
|
"module": "./dist/index.mjs",
|
|
@@ -26,14 +27,16 @@
|
|
|
26
27
|
"types": "./dist/hono/index.d.mts",
|
|
27
28
|
"import": "./dist/hono/index.mjs",
|
|
28
29
|
"default": "./dist/hono/index.mjs"
|
|
30
|
+
},
|
|
31
|
+
"./elysia": {
|
|
32
|
+
"types": "./dist/elysia/index.d.mts",
|
|
33
|
+
"import": "./dist/elysia/index.mjs",
|
|
34
|
+
"default": "./dist/elysia/index.mjs"
|
|
29
35
|
}
|
|
30
36
|
},
|
|
31
37
|
"dependencies": {
|
|
32
38
|
"hono": "^4"
|
|
33
39
|
},
|
|
34
|
-
"optionalDependencies": {
|
|
35
|
-
"arktype": "^2"
|
|
36
|
-
},
|
|
37
40
|
"peerDependencies": {
|
|
38
41
|
"typescript": "^6.0.3"
|
|
39
42
|
},
|
|
@@ -48,6 +51,8 @@
|
|
|
48
51
|
"devDependencies": {
|
|
49
52
|
"@biomejs/biome": "^2.5.0",
|
|
50
53
|
"@types/bun": "^1.3.14",
|
|
54
|
+
"elysia": "^1.4.29",
|
|
55
|
+
"arktype": "^2",
|
|
51
56
|
"tsdown": "^0.22.1"
|
|
52
57
|
}
|
|
53
58
|
}
|