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/dist/index.mjs CHANGED
@@ -1,15 +1,15 @@
1
- import { H3, serve } from "h3";
2
1
  import { Box, factory } from "getbox";
2
+ import { H3, defineHandler, defineMiddleware, serve } from "h3";
3
3
 
4
4
  //#region src/index.ts
5
5
  /**
6
- * Creates an h3 application with dependency injection support.
6
+ * Creates an h3 application.
7
7
  *
8
- * @param fn - Function that configures the app. Receives a fresh H3 instance
9
- * and Box instance. Can add routes to the provided app, or create
10
- * and return a new H3 instance.
8
+ * @param setup - Function that configures the app. Receives a fresh H3 instance
9
+ * and Box instance. Can add routes to the provided app, or create
10
+ * and return a new H3 instance.
11
11
  * @param box - Optional Box instance. If not provided, creates a new one.
12
- * @returns Object with `app` (H3 instance) and `serve` method.
12
+ * @returns Object with `app` (H3 instance), `box` (Box instance), and `serve` method.
13
13
  *
14
14
  * @example
15
15
  * ```typescript
@@ -30,28 +30,33 @@ import { Box, factory } from "getbox";
30
30
  * await app.serve({ port: 3000 });
31
31
  * ```
32
32
  */
33
- function application(fn, box = new Box()) {
33
+ function application(setup, box = new Box()) {
34
34
  const defaultApp = new H3();
35
- const app = fn(defaultApp, box) || defaultApp;
35
+ const app = setup(defaultApp, box) || defaultApp;
36
36
  return {
37
37
  app,
38
+ box,
38
39
  serve: (options) => serve(app, options)
39
40
  };
40
41
  }
41
42
  /**
42
- * Creates a Constructor that produces an h3 app when resolved.
43
+ * Creates an h3 app constructor.
43
44
  *
44
- * Use `box.new(controller)` to create fresh controller instances.
45
- * Controllers can use `box.get()` to access shared dependencies.
46
- *
47
- * @param fn - Function that configures the controller.
48
- * @returns A Constructor that can be resolved via `box.new(controller)`.
45
+ * @param setup - Function that configures the app.
46
+ * @returns A Constructor that produces an h3 app.
49
47
  *
50
48
  * @example
51
49
  * ```typescript
50
+ * import { application, controller } from "serverstruct";
51
+ *
52
+ * class Database {
53
+ * getUsers() { return ["Alice", "Bob"]; }
54
+ * }
55
+ *
52
56
  * // Define a controller
53
57
  * const usersController = controller((app, box) => {
54
- * app.get("/", () => ["Alice", "Bob"]);
58
+ * const db = box.get(Database);
59
+ * app.get("/", () => db.getUsers());
55
60
  * });
56
61
  *
57
62
  * // Use it in your app
@@ -59,23 +64,182 @@ function application(fn, box = new Box()) {
59
64
  * app.mount("/users", box.new(usersController));
60
65
  * });
61
66
  * ```
67
+ */
68
+ function controller(setup) {
69
+ return factory((box) => application(setup, box).app);
70
+ }
71
+ /**
72
+ * Creates a handler constructor.
73
+ *
74
+ * @param setup - Handler function that receives the event and Box instance.
75
+ * @returns A Constructor that produces an h3 handler.
62
76
  *
63
77
  * @example
64
78
  * ```typescript
65
- * // Controller with shared dependencies
66
- * class Database {
67
- * getUsers() { return ["Alice", "Bob"]; }
79
+ * import { application, handler } from "serverstruct";
80
+ *
81
+ * class UserService {
82
+ * getUser(id: string) { return { id, name: "Alice" }; }
68
83
  * }
69
84
  *
70
- * const usersController = controller((app, box) => {
71
- * const db = box.get(Database);
72
- * app.get("/", () => db.getUsers());
85
+ * // Define a handler
86
+ * const getUserHandler = handler((event, box) => {
87
+ * const userService = box.get(UserService);
88
+ * const id = event.context.params?.id;
89
+ * return userService.getUser(id);
90
+ * });
91
+ *
92
+ * // Use it in your app
93
+ * const app = application((app, box) => {
94
+ * app.get("/users/:id", box.get(getUserHandler));
95
+ * });
96
+ * ```
97
+ */
98
+ function handler(setup) {
99
+ return factory((box) => defineHandler((event) => setup(event, box)));
100
+ }
101
+ /**
102
+ * Creates an event handler constructor from a setup function.
103
+ *
104
+ * @param setup - Function that receives Box instance and returns an event handler object.
105
+ * @returns A Constructor that produces an h3 event handler.
106
+ *
107
+ * @example
108
+ * ```typescript
109
+ * import { application, eventHandler } from "serverstruct";
110
+ *
111
+ * class UserService {
112
+ * getUser(id: string) { return { id, name: "Alice" }; }
113
+ * }
114
+ *
115
+ * // Define an event handler
116
+ * const getUserHandler = eventHandler((box) => ({
117
+ * handler(event) {
118
+ * const userService = box.get(UserService);
119
+ * const id = event.context.params?.id;
120
+ * return userService.getUser(id);
121
+ * },
122
+ * meta: { auth: true }
123
+ * }));
124
+ *
125
+ * // Use it in your app
126
+ * const app = application((app, box) => {
127
+ * app.get("/users/:id", box.get(getUserHandler));
128
+ * });
129
+ * ```
130
+ */
131
+ function eventHandler(setup) {
132
+ return factory((box) => defineHandler(setup(box)));
133
+ }
134
+ /**
135
+ * Creates a middleware constructor.
136
+ *
137
+ * @param setup - Middleware function that receives the event, next function, and Box instance.
138
+ * @returns A Constructor that produces an h3 middleware.
139
+ *
140
+ * @example
141
+ * ```typescript
142
+ * import { application, middleware } from "serverstruct";
143
+ *
144
+ * class AuthService {
145
+ * validateToken(token: string) { return token === "valid"; }
146
+ * }
147
+ *
148
+ * // Define a middleware
149
+ * const authMiddleware = middleware((event, next, box) => {
150
+ * const authService = box.get(AuthService);
151
+ * const token = event.headers.get("authorization");
152
+ * if (!token || !authService.validateToken(token)) {
153
+ * throw new Error("Unauthorized");
154
+ * }
155
+ * });
156
+ *
157
+ * // Use it in your app
158
+ * const app = application((app, box) => {
159
+ * app.use(box.get(authMiddleware));
160
+ * app.get("/", () => "Hello world!");
161
+ * });
162
+ * ```
163
+ */
164
+ function middleware(setup) {
165
+ return factory((box) => defineMiddleware((event, next) => setup(event, next, box)));
166
+ }
167
+ /**
168
+ * A request-scoped context store for associating values with H3 events.
169
+ *
170
+ * Each request gets its own isolated context that is automatically cleaned up
171
+ * when the request completes. Uses a WeakMap internally to ensure values are
172
+ * garbage collected with their events.
173
+ *
174
+ * @example
175
+ * ```typescript
176
+ * import { application, Context } from "serverstruct";
177
+ *
178
+ * const userContext = new Context<User>();
179
+ *
180
+ * const app = application((app) => {
181
+ * app.use((event) => {
182
+ * userContext.set(event, { id: "123", name: "Alice" });
183
+ * });
184
+ * app.get("/user", (event) => {
185
+ * const user = userContext.get(event);
186
+ * return user;
187
+ * });
188
+ * });
189
+ * ```
190
+ */
191
+ var Context = class {
192
+ #map = /* @__PURE__ */ new WeakMap();
193
+ /**
194
+ * Sets a value for the given event.
195
+ */
196
+ set(event, value) {
197
+ this.#map.set(event, value);
198
+ }
199
+ /**
200
+ * Gets the value for the given event.
201
+ * @throws Error if no value is set for the event.
202
+ */
203
+ get(event) {
204
+ if (this.#map.has(event)) return this.#map.get(event);
205
+ throw new Error("context not found");
206
+ }
207
+ /**
208
+ * Gets the value for the given event, or undefined if not set.
209
+ */
210
+ lookup(event) {
211
+ return this.#map.get(event);
212
+ }
213
+ };
214
+ /**
215
+ * Creates a request-scoped context store for associating values with H3 events.
216
+ *
217
+ * Each request gets its own isolated context that is automatically cleaned up
218
+ * when the request completes. Uses a WeakMap internally to ensure values are
219
+ * garbage collected with their events.
220
+ *
221
+ * @returns A Context instance.
222
+ *
223
+ * @example
224
+ * ```typescript
225
+ * import { application, context } from "serverstruct";
226
+ *
227
+ * const userContext = context<User>();
228
+ *
229
+ * const app = application((app) => {
230
+ * app.use((event) => {
231
+ * userContext.set(event, { id: "123", name: "Alice" });
232
+ * });
233
+ * app.get("/user", (event) => {
234
+ * const user = userContext.get(event);
235
+ * return user;
236
+ * });
73
237
  * });
74
238
  * ```
75
239
  */
76
- function controller(fn) {
77
- return factory((box) => application(fn, box).app);
240
+ function context() {
241
+ return new Context();
78
242
  }
79
243
 
80
244
  //#endregion
81
- export { application, controller };
245
+ export { Context, application, context, controller, eventHandler, handler, middleware };
@@ -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;