serverstruct 1.0.0 → 1.2.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/.claude/settings.local.json +8 -0
- package/OPENAPI.md +286 -0
- package/OTEL.md +208 -0
- package/README.md +224 -81
- package/dist/index.cjs +193 -24
- package/dist/index.d.cts +175 -23
- package/dist/index.d.mts +175 -23
- package/dist/index.mjs +188 -24
- package/dist/openapi.cjs +242 -0
- package/dist/openapi.d.cts +241 -0
- package/dist/openapi.d.mts +241 -0
- package/dist/openapi.mjs +231 -0
- package/dist/openapi.scalar.cjs +13 -0
- package/dist/openapi.scalar.d.cts +8 -0
- package/dist/openapi.scalar.d.mts +8 -0
- package/dist/openapi.scalar.mjs +13 -0
- package/package.json +46 -10
- package/tsdown.config.mts +11 -0
package/dist/openapi.mjs
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { getQuery, getRouterParams, getValidatedQuery, getValidatedRouterParams, readBody, readValidatedBody } from "h3";
|
|
2
|
+
import { isAnyZodType } from "zod-openapi/api";
|
|
3
|
+
import { createDocument } from "zod-openapi";
|
|
4
|
+
|
|
5
|
+
//#region src/openapi.ts
|
|
6
|
+
function createContext(operation) {
|
|
7
|
+
const paramsSchema = operation.requestParams?.path;
|
|
8
|
+
const querySchema = operation.requestParams?.query;
|
|
9
|
+
const headersSchema = operation.requestParams?.header;
|
|
10
|
+
const bodyRawSchema = operation.requestBody?.content?.["application/json"]?.schema;
|
|
11
|
+
const bodySchema = isAnyZodType(bodyRawSchema) ? bodyRawSchema : void 0;
|
|
12
|
+
return {
|
|
13
|
+
schemas: {
|
|
14
|
+
params: paramsSchema,
|
|
15
|
+
query: querySchema,
|
|
16
|
+
headers: headersSchema,
|
|
17
|
+
body: bodySchema
|
|
18
|
+
},
|
|
19
|
+
params: (event) => paramsSchema ? getValidatedRouterParams(event, paramsSchema) : Promise.resolve(getRouterParams(event)),
|
|
20
|
+
query: (event) => querySchema ? getValidatedQuery(event, querySchema) : Promise.resolve(getQuery(event)),
|
|
21
|
+
body: (event) => bodySchema ? readValidatedBody(event, bodySchema) : readBody(event),
|
|
22
|
+
reply: (event, status, data, headers) => {
|
|
23
|
+
event.res.status = status;
|
|
24
|
+
if (headers) for (const [key, value] of Object.entries(headers)) event.res.headers.set(key, String(value));
|
|
25
|
+
return data;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const HTTP_METHODS = [
|
|
30
|
+
"get",
|
|
31
|
+
"post",
|
|
32
|
+
"put",
|
|
33
|
+
"delete",
|
|
34
|
+
"patch"
|
|
35
|
+
];
|
|
36
|
+
/**
|
|
37
|
+
* Collects OpenAPI operation definitions for document generation.
|
|
38
|
+
*
|
|
39
|
+
* Register operations by HTTP method and path. The accumulated `paths`
|
|
40
|
+
* object can be passed to `createDocument()` to generate the OpenAPI spec.
|
|
41
|
+
*
|
|
42
|
+
* Each registration returns a typed {@link RouterContext} for use in route handlers.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* // Singleton via getbox DI
|
|
47
|
+
* const paths = box.get(OpenApiPaths);
|
|
48
|
+
*
|
|
49
|
+
* const getPost = paths.get("/posts/{id}", { ... });
|
|
50
|
+
*
|
|
51
|
+
* // Generate OpenAPI document
|
|
52
|
+
* createDocument({ openapi: "3.1.0", info: { ... }, paths: paths.paths });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
var OpenApiPaths = class {
|
|
56
|
+
paths = {};
|
|
57
|
+
get(path, operation) {
|
|
58
|
+
return this.register(path, "get", operation);
|
|
59
|
+
}
|
|
60
|
+
post(path, operation) {
|
|
61
|
+
return this.register(path, "post", operation);
|
|
62
|
+
}
|
|
63
|
+
put(path, operation) {
|
|
64
|
+
return this.register(path, "put", operation);
|
|
65
|
+
}
|
|
66
|
+
delete(path, operation) {
|
|
67
|
+
return this.register(path, "delete", operation);
|
|
68
|
+
}
|
|
69
|
+
patch(path, operation) {
|
|
70
|
+
return this.register(path, "patch", operation);
|
|
71
|
+
}
|
|
72
|
+
/** Register an operation for all standard HTTP methods (get, post, put, delete, patch). */
|
|
73
|
+
all(path, operation) {
|
|
74
|
+
return this.on(HTTP_METHODS, path, operation);
|
|
75
|
+
}
|
|
76
|
+
/** Register an operation for specific HTTP methods. */
|
|
77
|
+
on(methods, path, operation) {
|
|
78
|
+
const item = {};
|
|
79
|
+
for (const method of methods) item[method] = operation;
|
|
80
|
+
this.paths[path] = {
|
|
81
|
+
...this.paths[path],
|
|
82
|
+
...item
|
|
83
|
+
};
|
|
84
|
+
return createContext(operation);
|
|
85
|
+
}
|
|
86
|
+
register(path, method, operation) {
|
|
87
|
+
this.paths[path] = {
|
|
88
|
+
...this.paths[path],
|
|
89
|
+
[method]: operation
|
|
90
|
+
};
|
|
91
|
+
return createContext(operation);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Combines OpenAPI path registration with H3 route registration.
|
|
96
|
+
*
|
|
97
|
+
* Each method registers the operation in {@link OpenApiPaths} (converting the
|
|
98
|
+
* H3 path syntax to OpenAPI format) and simultaneously registers the route
|
|
99
|
+
* handler on the H3 app. The handler receives the typed {@link RouterContext}.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```ts
|
|
103
|
+
* const router = createRouter(app, box.get(OpenApiPaths));
|
|
104
|
+
*
|
|
105
|
+
* router.get("/posts/:id", {
|
|
106
|
+
* operationId: "getPost",
|
|
107
|
+
* requestBody: jsonRequest(inputSchema),
|
|
108
|
+
* responses: {
|
|
109
|
+
* 200: jsonResponse(outputSchema, { description: "Success" }),
|
|
110
|
+
* },
|
|
111
|
+
* }, async (event, ctx) => {
|
|
112
|
+
* const body = await ctx.body(event);
|
|
113
|
+
* return ctx.reply(event, 200, { message: "ok" });
|
|
114
|
+
* });
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
var OpenApiRouter = class {
|
|
118
|
+
constructor(app, paths) {
|
|
119
|
+
this.app = app;
|
|
120
|
+
this.paths = paths;
|
|
121
|
+
}
|
|
122
|
+
get(path, operation, handler, opts) {
|
|
123
|
+
const ctx = this.paths.get(toOpenApiPath(path), operation);
|
|
124
|
+
this.app.get(path, (event) => handler(event, ctx), opts);
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
post(path, operation, handler, opts) {
|
|
128
|
+
const ctx = this.paths.post(toOpenApiPath(path), operation);
|
|
129
|
+
this.app.post(path, (event) => handler(event, ctx), opts);
|
|
130
|
+
return this;
|
|
131
|
+
}
|
|
132
|
+
put(path, operation, handler, opts) {
|
|
133
|
+
const ctx = this.paths.put(toOpenApiPath(path), operation);
|
|
134
|
+
this.app.put(path, (event) => handler(event, ctx), opts);
|
|
135
|
+
return this;
|
|
136
|
+
}
|
|
137
|
+
delete(path, operation, handler, opts) {
|
|
138
|
+
const ctx = this.paths.delete(toOpenApiPath(path), operation);
|
|
139
|
+
this.app.delete(path, (event) => handler(event, ctx), opts);
|
|
140
|
+
return this;
|
|
141
|
+
}
|
|
142
|
+
patch(path, operation, handler, opts) {
|
|
143
|
+
const ctx = this.paths.patch(toOpenApiPath(path), operation);
|
|
144
|
+
this.app.patch(path, (event) => handler(event, ctx), opts);
|
|
145
|
+
return this;
|
|
146
|
+
}
|
|
147
|
+
/** Register a route and operation for all standard HTTP methods. */
|
|
148
|
+
all(path, operation, handler, opts) {
|
|
149
|
+
const ctx = this.paths.all(toOpenApiPath(path), operation);
|
|
150
|
+
this.app.all(path, (event) => handler(event, ctx), opts);
|
|
151
|
+
return this;
|
|
152
|
+
}
|
|
153
|
+
/** Register a route and operation for specific HTTP methods. */
|
|
154
|
+
on(methods, path, operation, handler, opts) {
|
|
155
|
+
const ctx = this.paths.on(methods, toOpenApiPath(path), operation);
|
|
156
|
+
for (const method of methods) this.app.on(method, path, (event) => handler(event, ctx), opts);
|
|
157
|
+
return this;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
/** Create an {@link OpenApiRouter} that combines H3 route registration with OpenAPI path collection. */
|
|
161
|
+
function createRouter(app, paths) {
|
|
162
|
+
return new OpenApiRouter(app, paths);
|
|
163
|
+
}
|
|
164
|
+
/** Builder for OpenAPI metadata passed to `.meta()` on Zod schemas. */
|
|
165
|
+
const metadata = (meta) => meta;
|
|
166
|
+
/**
|
|
167
|
+
* Build a typed `requestBody` object with `application/json` content.
|
|
168
|
+
*
|
|
169
|
+
* Additional media type options (e.g. `example`) can be passed via `opts.content`.
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```ts
|
|
173
|
+
* jsonRequest(inputSchema)
|
|
174
|
+
* jsonRequest(inputSchema, { description: "Create a post", content: { example: { title: "Hello" } } })
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
function jsonRequest(schema, opts) {
|
|
178
|
+
const { content, ...rest } = opts || {};
|
|
179
|
+
return {
|
|
180
|
+
required: true,
|
|
181
|
+
...rest,
|
|
182
|
+
content: { "application/json": {
|
|
183
|
+
schema,
|
|
184
|
+
...content
|
|
185
|
+
} }
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Build a typed response object with `application/json` content.
|
|
190
|
+
*
|
|
191
|
+
* Additional media type options (e.g. `example`) can be passed via `opts.content`.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```ts
|
|
195
|
+
* jsonResponse(outputSchema, { description: "Success" })
|
|
196
|
+
* jsonResponse(outputSchema, {
|
|
197
|
+
* description: "Success",
|
|
198
|
+
* headers: z.object({ "x-request-id": z.string() }).meta({}),
|
|
199
|
+
* })
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
function jsonResponse(schema, opts) {
|
|
203
|
+
const { content, headers, ...rest } = opts;
|
|
204
|
+
return {
|
|
205
|
+
...rest,
|
|
206
|
+
headers,
|
|
207
|
+
content: { "application/json": {
|
|
208
|
+
schema,
|
|
209
|
+
...content
|
|
210
|
+
} }
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Convert H3 path syntax to OpenAPI path syntax.
|
|
215
|
+
*
|
|
216
|
+
* - `/:name` → `/{name}`
|
|
217
|
+
* - `/*` → `/{param}`
|
|
218
|
+
* - `/**` → `/{path}`
|
|
219
|
+
*/
|
|
220
|
+
function toOpenApiPath(route) {
|
|
221
|
+
if (!route.startsWith("/")) route = "/" + route;
|
|
222
|
+
return route.split("/").map((segment) => {
|
|
223
|
+
if (segment.startsWith(":")) return `{${segment.slice(1)}}`;
|
|
224
|
+
else if (segment === "*") return "{param}";
|
|
225
|
+
else if (segment === "**") return "{path}";
|
|
226
|
+
else return segment;
|
|
227
|
+
}).join("/");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
//#endregion
|
|
231
|
+
export { OpenApiPaths, OpenApiRouter, createDocument, createRouter, jsonRequest, jsonResponse, metadata };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
let h3 = require("h3");
|
|
2
|
+
let _scalar_core_libs_html_rendering = require("@scalar/core/libs/html-rendering");
|
|
3
|
+
|
|
4
|
+
//#region src/openapi.scalar.ts
|
|
5
|
+
function apiReference(configuration, customTheme) {
|
|
6
|
+
return (0, h3.html)((0, _scalar_core_libs_html_rendering.getHtmlDocument)({
|
|
7
|
+
_integration: "serverstruct",
|
|
8
|
+
...configuration
|
|
9
|
+
}, customTheme));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
13
|
+
exports.apiReference = apiReference;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as h30 from "h3";
|
|
2
|
+
import { HtmlRenderingConfiguration } from "@scalar/core/libs/html-rendering";
|
|
3
|
+
|
|
4
|
+
//#region src/openapi.scalar.d.ts
|
|
5
|
+
type ApiReferenceConfiguration = Partial<HtmlRenderingConfiguration>;
|
|
6
|
+
declare function apiReference(configuration: ApiReferenceConfiguration, customTheme?: string): h30.HTTPResponse;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { ApiReferenceConfiguration, apiReference };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as h31 from "h3";
|
|
2
|
+
import { HtmlRenderingConfiguration } from "@scalar/core/libs/html-rendering";
|
|
3
|
+
|
|
4
|
+
//#region src/openapi.scalar.d.ts
|
|
5
|
+
type ApiReferenceConfiguration = Partial<HtmlRenderingConfiguration>;
|
|
6
|
+
declare function apiReference(configuration: ApiReferenceConfiguration, customTheme?: string): h31.HTTPResponse;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { ApiReferenceConfiguration, apiReference };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { html } from "h3";
|
|
2
|
+
import { getHtmlDocument } from "@scalar/core/libs/html-rendering";
|
|
3
|
+
|
|
4
|
+
//#region src/openapi.scalar.ts
|
|
5
|
+
function apiReference(configuration, customTheme) {
|
|
6
|
+
return html(getHtmlDocument({
|
|
7
|
+
_integration: "serverstruct",
|
|
8
|
+
...configuration
|
|
9
|
+
}, customTheme));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
13
|
+
export { apiReference };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "serverstruct",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Type safe and modular servers with H3",
|
|
5
5
|
"private": false,
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -16,15 +16,38 @@
|
|
|
16
16
|
"types": "./dist/index.d.cts",
|
|
17
17
|
"default": "./dist/index.cjs"
|
|
18
18
|
}
|
|
19
|
+
},
|
|
20
|
+
"./openapi": {
|
|
21
|
+
"import": {
|
|
22
|
+
"types": "./dist/openapi.d.mts",
|
|
23
|
+
"default": "./dist/openapi.mjs"
|
|
24
|
+
},
|
|
25
|
+
"require": {
|
|
26
|
+
"types": "./dist/openapi.d.cts",
|
|
27
|
+
"default": "./dist/openapi.cjs"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"./openapi/scalar": {
|
|
31
|
+
"import": {
|
|
32
|
+
"types": "./dist/openapi.scalar.d.mts",
|
|
33
|
+
"default": "./dist/openapi.scalar.mjs"
|
|
34
|
+
},
|
|
35
|
+
"require": {
|
|
36
|
+
"types": "./dist/openapi.scalar.d.cts",
|
|
37
|
+
"default": "./dist/openapi.scalar.cjs"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"./otel": {
|
|
41
|
+
"import": {
|
|
42
|
+
"types": "./dist/otel.d.mts",
|
|
43
|
+
"default": "./dist/otel.mjs"
|
|
44
|
+
},
|
|
45
|
+
"require": {
|
|
46
|
+
"types": "./dist/otel.d.cts",
|
|
47
|
+
"default": "./dist/otel.cjs"
|
|
48
|
+
}
|
|
19
49
|
}
|
|
20
50
|
},
|
|
21
|
-
"scripts": {
|
|
22
|
-
"build": "tsc && tsdown src/index.ts --format esm,cjs",
|
|
23
|
-
"release": "pnpm run build && changeset publish",
|
|
24
|
-
"watch": "vitest",
|
|
25
|
-
"test": "vitest run",
|
|
26
|
-
"test:coverage": "vitest run --coverage"
|
|
27
|
-
},
|
|
28
51
|
"keywords": [
|
|
29
52
|
"h3",
|
|
30
53
|
"server",
|
|
@@ -43,6 +66,7 @@
|
|
|
43
66
|
"homepage": "https://github.com/eriicafes/serverstruct#readme",
|
|
44
67
|
"devDependencies": {
|
|
45
68
|
"@changesets/cli": "^2.29.8",
|
|
69
|
+
"@opentelemetry/sdk-trace-node": "^2.5.1",
|
|
46
70
|
"@types/node": "^25.0.3",
|
|
47
71
|
"@vitest/coverage-v8": "^4.0.16",
|
|
48
72
|
"tsdown": "^0.18.3",
|
|
@@ -50,7 +74,19 @@
|
|
|
50
74
|
"vitest": "^4.0.16"
|
|
51
75
|
},
|
|
52
76
|
"peerDependencies": {
|
|
53
|
-
"
|
|
54
|
-
"
|
|
77
|
+
"@opentelemetry/api": ">=1.9.0 <2.0.0",
|
|
78
|
+
"@opentelemetry/semantic-conventions": ">=1.39.0 <2.0.0",
|
|
79
|
+
"@scalar/core": ">=0.3.37",
|
|
80
|
+
"getbox": ">=1.0.0 <2.0.0",
|
|
81
|
+
"h3": ">=2.0.1-rc.6 <3.0.0",
|
|
82
|
+
"zod": ">=4.0.0 <5.0.0",
|
|
83
|
+
"zod-openapi": ">=5.4.6 <6.0.0"
|
|
84
|
+
},
|
|
85
|
+
"scripts": {
|
|
86
|
+
"build": "tsc && tsdown",
|
|
87
|
+
"release": "pnpm run build && changeset publish",
|
|
88
|
+
"watch": "vitest",
|
|
89
|
+
"test": "vitest run",
|
|
90
|
+
"test:coverage": "vitest run --coverage"
|
|
55
91
|
}
|
|
56
92
|
}
|