serverstruct 1.1.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 +175 -121
- 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 +40 -4
- package/tsdown.config.mts +11 -0
package/README.md
CHANGED
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
⚡️ Typesafe and modular servers with [H3](https://github.com/unjs/h3).
|
|
4
4
|
|
|
5
|
-
Serverstruct provides simple helpers for building modular
|
|
5
|
+
Serverstruct provides simple helpers for building modular H3 applications with dependency injection using [getbox](https://github.com/eriicafes/getbox).
|
|
6
|
+
|
|
7
|
+
## Integrations
|
|
8
|
+
|
|
9
|
+
- [OpenAPI](./OPENAPI.md) - Typesafe OpenAPI operations with Zod schema validation.
|
|
10
|
+
- [OpenTelemetry](./OTEL.md) - Distributed tracing middleware for HTTP requests.
|
|
6
11
|
|
|
7
12
|
## Installation
|
|
8
13
|
|
|
@@ -13,170 +18,151 @@ npm i serverstruct h3 getbox
|
|
|
13
18
|
## Quick Start
|
|
14
19
|
|
|
15
20
|
```typescript
|
|
16
|
-
import { application } from "serverstruct";
|
|
21
|
+
import { application, serve } from "serverstruct";
|
|
17
22
|
|
|
18
23
|
const app = application((app) => {
|
|
19
24
|
app.get("/", () => "Hello world!");
|
|
20
25
|
});
|
|
21
26
|
|
|
22
|
-
|
|
27
|
+
serve(app, { port: 3000 });
|
|
23
28
|
```
|
|
24
29
|
|
|
25
|
-
##
|
|
30
|
+
## Application
|
|
26
31
|
|
|
27
|
-
|
|
32
|
+
`application()` creates an H3 instance and a Box instance for dependency injection.
|
|
33
|
+
You can pass a Box instance to share dependencies across applications (see [Box Instance](#box-instance)).
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
import { application } from "serverstruct";
|
|
35
|
+
You can mount other apps using `app.mount()`:
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
```typescript
|
|
38
|
+
import { H3 } from "h3";
|
|
39
|
+
import { application, serve } from "serverstruct";
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
users.push(body);
|
|
40
|
-
return body;
|
|
41
|
-
});
|
|
41
|
+
// Create a sub application
|
|
42
|
+
const usersApp = application((app) => {
|
|
43
|
+
app.get("/", () => ["Alice", "Bob"]);
|
|
42
44
|
});
|
|
43
45
|
|
|
44
|
-
//
|
|
46
|
+
// Create a regular H3 instance
|
|
47
|
+
const docsApp = new H3().get("/", () => "API Documentation");
|
|
48
|
+
|
|
49
|
+
// Mount in main app
|
|
45
50
|
const app = application((app) => {
|
|
46
|
-
app.get("/
|
|
51
|
+
app.get("/", () => "Hello world!");
|
|
47
52
|
app.mount("/users", usersApp);
|
|
53
|
+
app.mount("/docs", docsApp);
|
|
48
54
|
});
|
|
49
55
|
|
|
50
|
-
|
|
56
|
+
serve(app, { port: 3000 });
|
|
51
57
|
```
|
|
52
58
|
|
|
53
|
-
|
|
59
|
+
When an app is mounted, its middlewares and routes are copied to the main app in place with middlewares scoped to the base path.
|
|
60
|
+
|
|
61
|
+
Both `application()` and `controller()` can return a custom H3 instance:
|
|
54
62
|
|
|
55
63
|
```typescript
|
|
56
64
|
import { H3 } from "h3";
|
|
57
65
|
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
onError: (error) => {
|
|
61
|
-
console.error(error);
|
|
66
|
+
const customApp = application(() => {
|
|
67
|
+
const app = new H3({
|
|
68
|
+
onError: (error, event) => {
|
|
69
|
+
console.error("Error:", error);
|
|
62
70
|
},
|
|
63
71
|
});
|
|
64
|
-
|
|
65
|
-
return
|
|
66
|
-
});
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
### Accessing the Box Instance
|
|
70
|
-
|
|
71
|
-
The `application()` function creates a new Box instance by default and returns it along with `app` and `serve`. You can access the box instance to retrieve dependencies:
|
|
72
|
-
|
|
73
|
-
```typescript
|
|
74
|
-
import { constant } from "getbox";
|
|
75
|
-
|
|
76
|
-
const Port = constant(5000);
|
|
77
|
-
|
|
78
|
-
const { box, serve } = application((app, box) => {
|
|
79
|
-
app.get("/", () => "Hello world!");
|
|
72
|
+
app.get("/", () => "Hello from custom app!");
|
|
73
|
+
return app;
|
|
80
74
|
});
|
|
81
|
-
|
|
82
|
-
const port = box.get(Port);
|
|
83
|
-
serve({ port });
|
|
84
75
|
```
|
|
85
76
|
|
|
86
77
|
## Controllers
|
|
87
78
|
|
|
88
|
-
Use `controller()` to create
|
|
79
|
+
Controllers are apps that are initialized with a parent Box instance, sharing the same dependency container. Use `controller()` to create H3 app constructors:
|
|
89
80
|
|
|
90
81
|
```typescript
|
|
91
82
|
import { application, controller } from "serverstruct";
|
|
92
83
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
84
|
+
class UserStore {
|
|
85
|
+
public users: User[] = [];
|
|
86
|
+
|
|
87
|
+
add(user: User) {
|
|
88
|
+
this.users.push(user);
|
|
89
|
+
return user;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Create a controller
|
|
94
|
+
const usersController = controller((app, box) => {
|
|
95
|
+
const store = box.get(UserStore);
|
|
96
96
|
|
|
97
|
-
app.get("/", () => users);
|
|
97
|
+
app.get("/", () => store.users);
|
|
98
98
|
app.post("/", async (event) => {
|
|
99
|
-
const body = await
|
|
100
|
-
|
|
101
|
-
return body;
|
|
99
|
+
const body = await readBody(event);
|
|
100
|
+
return store.add(body);
|
|
102
101
|
});
|
|
103
102
|
});
|
|
104
103
|
|
|
105
104
|
// Use it in your main app
|
|
106
105
|
const app = application((app, box) => {
|
|
107
|
-
|
|
106
|
+
const store = box.get(UserStore);
|
|
107
|
+
|
|
108
|
+
app.get("/count", () => ({
|
|
109
|
+
users: store.users.length,
|
|
110
|
+
}));
|
|
108
111
|
app.mount("/users", box.new(usersController));
|
|
109
112
|
});
|
|
110
113
|
|
|
111
|
-
|
|
114
|
+
serve(app, { port: 3000 });
|
|
112
115
|
```
|
|
113
116
|
|
|
114
117
|
## Handlers
|
|
115
118
|
|
|
116
|
-
Use `handler()` to create
|
|
119
|
+
Use `handler()` to create H3 handler constructors:
|
|
117
120
|
|
|
118
121
|
```typescript
|
|
119
122
|
import { application, handler } from "serverstruct";
|
|
120
123
|
|
|
121
|
-
class UserService {
|
|
122
|
-
getUser(id: string) {
|
|
123
|
-
return { id, name: "Alice" };
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
124
|
// Define a handler
|
|
128
125
|
const getUserHandler = handler((event, box) => {
|
|
129
|
-
const
|
|
126
|
+
const store = box.get(UserStore);
|
|
127
|
+
|
|
130
128
|
const id = event.context.params?.id;
|
|
131
|
-
return
|
|
129
|
+
return store.users.find((user) => user.id === id);
|
|
132
130
|
});
|
|
133
131
|
|
|
134
132
|
// Use it in your app
|
|
135
133
|
const app = application((app, box) => {
|
|
136
|
-
app.get("/users/:id", box.
|
|
134
|
+
app.get("/users/:id", box.new(getUserHandler));
|
|
137
135
|
});
|
|
138
136
|
```
|
|
139
137
|
|
|
140
138
|
### Event Handlers
|
|
141
139
|
|
|
142
|
-
Use `eventHandler()` to create
|
|
140
|
+
Use `eventHandler()` to create H3 handler constructors with additional options like meta and middleware:
|
|
143
141
|
|
|
144
142
|
```typescript
|
|
145
143
|
import { application, eventHandler } from "serverstruct";
|
|
146
144
|
|
|
147
|
-
|
|
148
|
-
getUser(id: string) {
|
|
149
|
-
return { id, name: "Alice" };
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Define an event handler with middleware and metadata
|
|
145
|
+
// Define an event handler with additional options
|
|
154
146
|
const getUserHandler = eventHandler((box) => ({
|
|
155
147
|
handler(event) {
|
|
156
|
-
const
|
|
148
|
+
const store = box.get(UserStore);
|
|
149
|
+
|
|
157
150
|
const id = event.context.params?.id;
|
|
158
|
-
return
|
|
151
|
+
return store.users.find((user) => user.id === id);
|
|
159
152
|
},
|
|
160
153
|
meta: { auth: true },
|
|
161
|
-
middleware: [
|
|
162
|
-
(event) => {
|
|
163
|
-
const token = event.headers.get("authorization");
|
|
164
|
-
if (!token || token !== "secret-token") {
|
|
165
|
-
throw new Error("Unauthorized");
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
],
|
|
154
|
+
middleware: [],
|
|
169
155
|
}));
|
|
170
156
|
|
|
171
157
|
// Use it in your app
|
|
172
158
|
const app = application((app, box) => {
|
|
173
|
-
app.get("/users/:id", box.
|
|
159
|
+
app.get("/users/:id", box.new(getUserHandler));
|
|
174
160
|
});
|
|
175
161
|
```
|
|
176
162
|
|
|
177
163
|
## Middleware
|
|
178
164
|
|
|
179
|
-
Use `middleware()` to create
|
|
165
|
+
Use `middleware()` to create H3 middleware constructors:
|
|
180
166
|
|
|
181
167
|
```typescript
|
|
182
168
|
import { application, middleware } from "serverstruct";
|
|
@@ -200,75 +186,143 @@ const app = application((app, box) => {
|
|
|
200
186
|
});
|
|
201
187
|
```
|
|
202
188
|
|
|
203
|
-
|
|
189
|
+
All middlewares defined with `app.use()` are global and execute before the matched handler in the exact order they are defined.
|
|
204
190
|
|
|
205
|
-
|
|
191
|
+
## Error Handling
|
|
206
192
|
|
|
207
|
-
|
|
208
|
-
import { application, context } from "serverstruct";
|
|
193
|
+
Error handlers are middleware that catch errors after calling `await next()`.
|
|
209
194
|
|
|
210
|
-
|
|
211
|
-
id: string;
|
|
212
|
-
name: string;
|
|
213
|
-
}
|
|
195
|
+
The last error handler defined executes before earlier ones. The error bubbles through each error handler until a response is returned or the default error response is sent.
|
|
214
196
|
|
|
215
|
-
|
|
216
|
-
|
|
197
|
+
Use H3's `onError` helper to define error handlers:
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
import { onError } from "h3";
|
|
201
|
+
import { application } from "serverstruct";
|
|
217
202
|
|
|
218
203
|
const app = application((app) => {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
204
|
+
app.use(
|
|
205
|
+
onError((error) => {
|
|
206
|
+
console.log("Error:", error);
|
|
207
|
+
}),
|
|
208
|
+
);
|
|
209
|
+
app.get("/", () => {
|
|
210
|
+
throw new Error("Oops");
|
|
223
211
|
});
|
|
212
|
+
});
|
|
213
|
+
```
|
|
224
214
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
215
|
+
When the error handler needs access to the Box, wrap it with `middleware()`:
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
import { onError } from "h3";
|
|
219
|
+
import { application, middleware } from "serverstruct";
|
|
220
|
+
|
|
221
|
+
const errorHandler = middleware((event, next, box) => {
|
|
222
|
+
return onError((error) => {
|
|
223
|
+
console.log("Error:", error);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const app = application((app, box) => {
|
|
228
|
+
app.use(box.get(errorHandler));
|
|
229
|
+
app.get("/", () => {
|
|
230
|
+
throw new Error("Oops");
|
|
229
231
|
});
|
|
230
232
|
});
|
|
231
233
|
```
|
|
232
234
|
|
|
233
|
-
###
|
|
235
|
+
### Not Found Routes
|
|
234
236
|
|
|
235
|
-
|
|
236
|
-
- `get(event)` - Retrieve the value for the current request (throws if not found)
|
|
237
|
-
- `lookup(event)` - Retrieve the value or `undefined` if not found
|
|
237
|
+
To catch not found routes, define a catch-all handler and return the desired error:
|
|
238
238
|
|
|
239
239
|
```typescript
|
|
240
|
-
|
|
240
|
+
import { H3Error } from "h3";
|
|
241
241
|
|
|
242
242
|
const app = application((app) => {
|
|
243
|
-
app.
|
|
244
|
-
|
|
245
|
-
|
|
243
|
+
app.get("/", () => "Hello world!");
|
|
244
|
+
app.all("**", () => new H3Error({ status: 404, message: "Not found" }));
|
|
245
|
+
});
|
|
246
|
+
```
|
|
246
247
|
|
|
247
|
-
|
|
248
|
-
// Safe access - throws if not set
|
|
249
|
-
const id = requestIdContext.get(event);
|
|
248
|
+
Mounted apps can define their own not found handlers:
|
|
250
249
|
|
|
251
|
-
|
|
252
|
-
|
|
250
|
+
```typescript
|
|
251
|
+
const usersApp = application((app) => {
|
|
252
|
+
app.get("/", () => ["Alice", "Bob"]);
|
|
253
|
+
app.all(
|
|
254
|
+
"**",
|
|
255
|
+
() => new H3Error({ status: 404, message: "User route not found" }),
|
|
256
|
+
);
|
|
257
|
+
});
|
|
253
258
|
|
|
254
|
-
|
|
255
|
-
|
|
259
|
+
const app = application((app) => {
|
|
260
|
+
app.mount("/users", usersApp);
|
|
261
|
+
app.all("**", () => new H3Error({ status: 404, message: "Not found" }));
|
|
256
262
|
});
|
|
257
263
|
```
|
|
258
264
|
|
|
259
|
-
##
|
|
265
|
+
## Box Instance
|
|
260
266
|
|
|
261
|
-
|
|
267
|
+
By default, `application()` creates a new Box instance. Pass a Box instance to reuse it:
|
|
262
268
|
|
|
263
269
|
```typescript
|
|
264
270
|
import { Box } from "getbox";
|
|
271
|
+
import { application, serve } from "serverstruct";
|
|
265
272
|
|
|
266
273
|
const box = new Box();
|
|
267
274
|
|
|
268
|
-
|
|
269
|
-
|
|
275
|
+
const usersApp = application((app, box) => {
|
|
276
|
+
const store = box.get(UserStore);
|
|
277
|
+
app.get("/", () => store.users);
|
|
278
|
+
}, box);
|
|
270
279
|
|
|
271
280
|
const app = application((app, box) => {
|
|
272
|
-
|
|
281
|
+
const store = box.get(UserStore);
|
|
282
|
+
app.get("/count", () => store.users.length);
|
|
283
|
+
app.mount("/users", usersApp);
|
|
273
284
|
}, box);
|
|
285
|
+
|
|
286
|
+
serve(app, { port: 3000 });
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Context
|
|
290
|
+
|
|
291
|
+
`context()` creates a request-scoped, type-safe store for per-request values.
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
import { application, context } from "serverstruct";
|
|
295
|
+
|
|
296
|
+
interface User {
|
|
297
|
+
id: string;
|
|
298
|
+
name: string;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Create a context store
|
|
302
|
+
const userContext = context<User>();
|
|
303
|
+
|
|
304
|
+
const app = application((app) => {
|
|
305
|
+
// Set context in middleware
|
|
306
|
+
app.use((event) => {
|
|
307
|
+
const user = { id: "123", name: "Alice" };
|
|
308
|
+
userContext.set(event, user);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Access context in handlers
|
|
312
|
+
app.get("/profile", (event) => {
|
|
313
|
+
// Optional access - returns undefined if not set
|
|
314
|
+
const maybeUser = userContext.lookup(event);
|
|
315
|
+
|
|
316
|
+
// Safe access - throws if not set
|
|
317
|
+
const user = userContext.get(event);
|
|
318
|
+
|
|
319
|
+
return { profile: user };
|
|
320
|
+
});
|
|
321
|
+
});
|
|
274
322
|
```
|
|
323
|
+
|
|
324
|
+
### Context Methods
|
|
325
|
+
|
|
326
|
+
- `set(event, value)` - Store a value for the current request
|
|
327
|
+
- `get(event)` - Retrieve the value for the current request (throws if not found)
|
|
328
|
+
- `lookup(event)` - Retrieve the value or `undefined` if not found
|
package/dist/openapi.cjs
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
let h3 = require("h3");
|
|
2
|
+
let zod_openapi_api = require("zod-openapi/api");
|
|
3
|
+
let zod_openapi = require("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 = (0, zod_openapi_api.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 ? (0, h3.getValidatedRouterParams)(event, paramsSchema) : Promise.resolve((0, h3.getRouterParams)(event)),
|
|
20
|
+
query: (event) => querySchema ? (0, h3.getValidatedQuery)(event, querySchema) : Promise.resolve((0, h3.getQuery)(event)),
|
|
21
|
+
body: (event) => bodySchema ? (0, h3.readValidatedBody)(event, bodySchema) : (0, h3.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
|
+
exports.OpenApiPaths = OpenApiPaths;
|
|
232
|
+
exports.OpenApiRouter = OpenApiRouter;
|
|
233
|
+
Object.defineProperty(exports, 'createDocument', {
|
|
234
|
+
enumerable: true,
|
|
235
|
+
get: function () {
|
|
236
|
+
return zod_openapi.createDocument;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
exports.createRouter = createRouter;
|
|
240
|
+
exports.jsonRequest = jsonRequest;
|
|
241
|
+
exports.jsonResponse = jsonResponse;
|
|
242
|
+
exports.metadata = metadata;
|