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.
@@ -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.0.0",
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
- "getbox": ">= 1.0.0 <2.0.0",
54
- "h3": ">=2.0.1-rc.6 <3.0.0"
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
  }
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from "tsdown";
2
+
3
+ export default defineConfig({
4
+ entry: [
5
+ "./src/index.ts",
6
+ "./src/openapi.ts",
7
+ "./src/openapi.scalar.ts",
8
+ "./src/otel.ts",
9
+ ],
10
+ format: ["esm", "cjs"],
11
+ });