@danceroutine/tango-adapters-express 0.1.0 → 1.0.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Pedro Del Moral Lopez
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.
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # @danceroutine/tango-adapters-express
2
+
3
+ `@danceroutine/tango-adapters-express` runs Tango views and viewsets inside Express.
4
+
5
+ Express still owns routing, middleware, request lifecycle, and application bootstrapping. This package connects those Express responsibilities to Tango's framework-agnostic resource layer, filtering, pagination, serializer-backed request and response contracts, and response conventions.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @danceroutine/tango-adapters-express express
11
+ ```
12
+
13
+ In a real application you will usually install this alongside the Tango packages that define your models, serializers, and resource classes.
14
+
15
+ ## How it fits into an Express application
16
+
17
+ A typical Tango + Express stack looks like this:
18
+
19
+ 1. define models with `@danceroutine/tango-schema`
20
+ 2. query and mutate data through `Model.objects` from `@danceroutine/tango-orm`
21
+ 3. define serializer-backed resource behavior with `@danceroutine/tango-resources`
22
+ 4. use `ExpressAdapter` to register those handlers with Express routes
23
+
24
+ The adapter's responsibility is restricted to translating between Express request handlers and Tango's framework-agnostic view and viewset interfaces, then handing the resulting response back to Express.
25
+
26
+ ## Quick start
27
+
28
+ ```ts
29
+ import express from 'express';
30
+ import { z } from 'zod';
31
+ import '@danceroutine/tango-orm/runtime';
32
+ import { ExpressAdapter } from '@danceroutine/tango-adapters-express/adapter';
33
+ import { Model, t } from '@danceroutine/tango-schema';
34
+ import { ModelSerializer, ModelViewSet } from '@danceroutine/tango-resources';
35
+
36
+ const TodoReadSchema = z.object({
37
+ id: z.number(),
38
+ title: z.string(),
39
+ completed: z.boolean(),
40
+ });
41
+
42
+ const TodoCreateSchema = TodoReadSchema.omit({ id: true });
43
+ const TodoUpdateSchema = TodoCreateSchema.partial();
44
+
45
+ type Todo = z.infer<typeof TodoReadSchema>;
46
+
47
+ const TodoModel = Model({
48
+ namespace: 'app',
49
+ name: 'Todo',
50
+ schema: TodoReadSchema.extend({
51
+ id: t.primaryKey(z.number().int()),
52
+ title: z.string(),
53
+ completed: t.default(z.boolean(), 'false'),
54
+ }),
55
+ });
56
+
57
+ class TodoSerializer extends ModelSerializer<
58
+ Todo,
59
+ typeof TodoCreateSchema,
60
+ typeof TodoUpdateSchema,
61
+ typeof TodoReadSchema
62
+ > {
63
+ static readonly model = TodoModel;
64
+ static readonly createSchema = TodoCreateSchema;
65
+ static readonly updateSchema = TodoUpdateSchema;
66
+ static readonly outputSchema = TodoReadSchema;
67
+ }
68
+
69
+ class TodoViewSet extends ModelViewSet<Todo, typeof TodoSerializer> {
70
+ constructor() {
71
+ super({
72
+ serializer: TodoSerializer,
73
+ orderingFields: ['id', 'title'],
74
+ });
75
+ }
76
+ }
77
+
78
+ async function main(): Promise<void> {
79
+ const app = express();
80
+ app.use(express.json());
81
+
82
+ const viewset = new TodoViewSet();
83
+ const adapter = new ExpressAdapter();
84
+
85
+ adapter.registerViewSet(app, '/api/todos', viewset);
86
+ }
87
+ ```
88
+
89
+ `registerViewSet(...)` is the application-facing CRUD helper. It registers collection routes at `/api/todos` and detail routes at `/api/todos/:id`, then calls `list`, `create`, `retrieve`, `update`, and `destroy` on the viewset instance as requests come in.
90
+
91
+ `ExpressAdapter` also exposes `toQueryParams(req)` for application code that wants the same normalized query contract resources use internally. That helper returns `TangoQueryParams` from `@danceroutine/tango-core` and stays focused on Express-to-Tango normalization.
92
+
93
+ ## Public API
94
+
95
+ The root export includes:
96
+
97
+ - `ExpressAdapter`, the main integration class
98
+ - `AdaptExpressOptions`, for adapter configuration
99
+ - route-facing helper types such as `ExpressAPIView`, `ExpressCrudViewSet`, and `ExpressRouteRegistrar`
100
+
101
+ You can import from the package root or from the `adapter` subpath:
102
+
103
+ ```ts
104
+ import { ExpressAdapter } from '@danceroutine/tango-adapters-express';
105
+ import { adapter } from '@danceroutine/tango-adapters-express';
106
+ ```
107
+
108
+ ## Documentation
109
+
110
+ The official documentation walks through Tango from first principles and gives the clearest starting point when you are evaluating the framework or building with it.
111
+
112
+ - Official documentation: <https://tangowebframework.dev>
113
+ - Blog API tutorial: <https://tangowebframework.dev/tutorials/blog-api>
114
+ - Resources topic: <https://tangowebframework.dev/topics/resources-and-viewsets>
115
+ - Serializers topic: <https://tangowebframework.dev/topics/serializers>
116
+
117
+ ## Development
118
+
119
+ ```bash
120
+ pnpm --filter @danceroutine/tango-adapters-express build
121
+ pnpm --filter @danceroutine/tango-adapters-express typecheck
122
+ pnpm --filter @danceroutine/tango-adapters-express test
123
+ ```
124
+
125
+ For the wider contributor workflow, use:
126
+
127
+ - <https://tangowebframework.dev/contributing>
128
+
129
+ ## License
130
+
131
+ MIT
@@ -1,13 +1,81 @@
1
1
  import type { Request as ExpressRequest, RequestHandler } from 'express';
2
+ import { Router } from 'express';
2
3
  import { RequestContext } from '@danceroutine/tango-resources';
3
- import type { FrameworkAdapter, FrameworkAdapterOptions } from '@danceroutine/tango-adapters-core/adapter';
4
- export interface AdaptExpressOptions extends FrameworkAdapterOptions<ExpressRequest> {
4
+ import { TangoQueryParams, TangoResponse } from '@danceroutine/tango-core';
5
+ import { FRAMEWORK_ADAPTER_BRAND, type FrameworkAdapter, type FrameworkAdapterOptions } from '@danceroutine/tango-adapters-core/adapter';
6
+ /**
7
+ * Adapter options for Express integration.
8
+ */
9
+ export type AdaptExpressOptions = FrameworkAdapterOptions<ExpressRequest>;
10
+ /**
11
+ * Minimal CRUD viewset contract used by adapter route registration helpers.
12
+ */
13
+ export interface ExpressCrudViewSet {
14
+ list(ctx: RequestContext): Promise<TangoResponse>;
15
+ create(ctx: RequestContext): Promise<TangoResponse>;
16
+ retrieve(ctx: RequestContext, id: string): Promise<TangoResponse>;
17
+ update(ctx: RequestContext, id: string): Promise<TangoResponse>;
18
+ destroy(ctx: RequestContext, id: string): Promise<TangoResponse>;
5
19
  }
20
+ export interface ExpressAPIView {
21
+ dispatch(ctx: RequestContext): Promise<TangoResponse>;
22
+ }
23
+ /**
24
+ * Minimal route registrar interface implemented by Express apps and routers.
25
+ */
26
+ export interface ExpressRouteRegistrar {
27
+ get(path: string, handler: RequestHandler): unknown;
28
+ post(path: string, handler: RequestHandler): unknown;
29
+ patch(path: string, handler: RequestHandler): unknown;
30
+ put(path: string, handler: RequestHandler): unknown;
31
+ delete(path: string, handler: RequestHandler): unknown;
32
+ }
33
+ /**
34
+ * Express adapter that translates Express handlers to Tango `RequestContext`.
35
+ */
6
36
  export declare class ExpressAdapter implements FrameworkAdapter<Response, RequestHandler, ExpressRequest> {
7
- adapt(handler: (ctx: RequestContext, ...args: unknown[]) => Promise<Response>, options?: AdaptExpressOptions): RequestHandler;
37
+ readonly __tangoBrand: typeof FRAMEWORK_ADAPTER_BRAND;
38
+ /**
39
+ * Normalize an Express request into Tango query params.
40
+ */
41
+ toQueryParams(req: ExpressRequest): TangoQueryParams;
42
+ /**
43
+ * Adapt a Tango-style handler into an Express request handler.
44
+ */
45
+ adapt(handler: (ctx: RequestContext, ...args: unknown[]) => Promise<TangoResponse>, options?: AdaptExpressOptions): RequestHandler;
46
+ /**
47
+ * Build an Express router that wires all CRUD routes for a viewset.
48
+ */
49
+ createViewSetRouter(viewset: ExpressCrudViewSet, options?: AdaptExpressOptions): Router;
50
+ /**
51
+ * Build an Express router for a single-path APIView.
52
+ */
53
+ createAPIViewRouter(apiView: ExpressAPIView, options?: AdaptExpressOptions): Router;
54
+ /**
55
+ * Register all CRUD routes for a viewset under a base path.
56
+ */
57
+ registerViewSet(registrar: ExpressRouteRegistrar, basePath: string, viewset: ExpressCrudViewSet, options?: AdaptExpressOptions): void;
58
+ /**
59
+ * Register one APIView on a single path and dispatch by HTTP method.
60
+ */
61
+ registerAPIView(registrar: ExpressRouteRegistrar, path: string, apiView: ExpressAPIView, options?: AdaptExpressOptions): void;
62
+ /**
63
+ * Register one GenericAPIView with collection and detail routes.
64
+ */
65
+ registerGenericAPIView(registrar: ExpressRouteRegistrar, collectionPath: string, detailPath: string | undefined, apiView: ExpressAPIView, options?: AdaptExpressOptions): void;
66
+ private invokeDetailAction;
67
+ private invokeCollectionAction;
8
68
  private createHandler;
69
+ private normalizeParams;
70
+ private callHandler;
9
71
  private toRequestFromExpress;
10
72
  private normalizeBody;
11
73
  private isJsonLike;
12
74
  private hasTag;
75
+ private registerMethod;
76
+ private normalizeBasePath;
77
+ private joinPath;
78
+ private registerActionMethods;
79
+ private registerAllMethods;
80
+ private getViewSetActions;
13
81
  }
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * Domain boundary barrel: centralizes this subdomain's public contract.
3
3
  */
4
- export { ExpressAdapter, type AdaptExpressOptions } from './ExpressAdapter';
4
+ export { ExpressAdapter, type AdaptExpressOptions, type ExpressAPIView, type ExpressCrudViewSet, type ExpressRouteRegistrar, } from './ExpressAdapter';
@@ -1,4 +1,3 @@
1
- /**
2
- * Domain boundary barrel: centralizes this subdomain's public contract.
3
- */
4
- export { ExpressAdapter } from './ExpressAdapter';
1
+ import { ExpressAdapter } from "../adapter-C6JD05QW.js";
2
+
3
+ export { ExpressAdapter };
@@ -0,0 +1,209 @@
1
+ import { Router } from "express";
2
+ import { RequestContext } from "@danceroutine/tango-resources";
3
+ import { TangoQueryParams } from "@danceroutine/tango-core";
4
+ import { FRAMEWORK_ADAPTER_BRAND } from "@danceroutine/tango-adapters-core/adapter";
5
+
6
+ //#region rolldown:runtime
7
+ var __defProp = Object.defineProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all) __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true
12
+ });
13
+ };
14
+
15
+ //#endregion
16
+ //#region src/adapter/ExpressAdapter.ts
17
+ var ExpressAdapter = class {
18
+ __tangoBrand = FRAMEWORK_ADAPTER_BRAND;
19
+ /**
20
+ * Normalize an Express request into Tango query params.
21
+ */
22
+ toQueryParams(req) {
23
+ const request = this.toRequestFromExpress(req);
24
+ return TangoQueryParams.fromRequest(request);
25
+ }
26
+ /**
27
+ * Adapt a Tango-style handler into an Express request handler.
28
+ */
29
+ adapt(handler, options = {}) {
30
+ return this.createHandler(handler, options);
31
+ }
32
+ /**
33
+ * Build an Express router that wires all CRUD routes for a viewset.
34
+ */
35
+ createViewSetRouter(viewset, options = {}) {
36
+ const router = Router();
37
+ this.registerViewSet(router, "", viewset, options);
38
+ return router;
39
+ }
40
+ /**
41
+ * Build an Express router for a single-path APIView.
42
+ */
43
+ createAPIViewRouter(apiView, options = {}) {
44
+ const router = Router();
45
+ this.registerAPIView(router, "", apiView, options);
46
+ return router;
47
+ }
48
+ /**
49
+ * Register all CRUD routes for a viewset under a base path.
50
+ */
51
+ registerViewSet(registrar, basePath, viewset, options = {}) {
52
+ const collectionPath = this.normalizeBasePath(basePath);
53
+ const detailPath = collectionPath === "/" ? "/:id" : `${collectionPath}/:id`;
54
+ registrar.get(collectionPath, this.adapt((ctx) => viewset.list(ctx), options));
55
+ registrar.post(collectionPath, this.adapt((ctx) => viewset.create(ctx), options));
56
+ registrar.get(detailPath, this.adapt((ctx, id) => viewset.retrieve(ctx, String(id)), options));
57
+ registrar.patch(detailPath, this.adapt((ctx, id) => viewset.update(ctx, String(id)), options));
58
+ registrar.put(detailPath, this.adapt((ctx, id) => viewset.update(ctx, String(id)), options));
59
+ registrar.delete(detailPath, this.adapt((ctx, id) => viewset.destroy(ctx, String(id)), options));
60
+ const actions = this.getViewSetActions(viewset);
61
+ actions.forEach((action) => {
62
+ const actionPath = action.scope === "detail" ? this.joinPath(detailPath, action.path) : this.joinPath(collectionPath, action.path);
63
+ const handler = action.scope === "detail" ? this.adapt((ctx, id) => this.invokeDetailAction(viewset, action, ctx, String(id)), options) : this.adapt((ctx) => this.invokeCollectionAction(viewset, action, ctx), options);
64
+ this.registerActionMethods(registrar, action.methods, actionPath, handler);
65
+ });
66
+ }
67
+ /**
68
+ * Register one APIView on a single path and dispatch by HTTP method.
69
+ */
70
+ registerAPIView(registrar, path, apiView, options = {}) {
71
+ const normalizedPath = this.normalizeBasePath(path);
72
+ this.registerAllMethods(registrar, normalizedPath, this.adapt((ctx) => apiView.dispatch(ctx), options));
73
+ }
74
+ /**
75
+ * Register one GenericAPIView with collection and detail routes.
76
+ */
77
+ registerGenericAPIView(registrar, collectionPath, detailPath, apiView, options = {}) {
78
+ const normalizedCollectionPath = this.normalizeBasePath(collectionPath);
79
+ const normalizedDetailPath = detailPath?.trim().length ? this.normalizeBasePath(detailPath) : this.joinPath(normalizedCollectionPath, ":id");
80
+ registrar.get(normalizedCollectionPath, this.adapt((ctx) => apiView.dispatch(ctx), options));
81
+ registrar.post(normalizedCollectionPath, this.adapt((ctx) => apiView.dispatch(ctx), options));
82
+ registrar.get(normalizedDetailPath, this.adapt((ctx) => apiView.dispatch(ctx), options));
83
+ registrar.put(normalizedDetailPath, this.adapt((ctx) => apiView.dispatch(ctx), options));
84
+ registrar.patch(normalizedDetailPath, this.adapt((ctx) => apiView.dispatch(ctx), options));
85
+ registrar.delete(normalizedDetailPath, this.adapt((ctx) => apiView.dispatch(ctx), options));
86
+ }
87
+ invokeDetailAction(viewset, action, ctx, id) {
88
+ const candidate = viewset[action.name];
89
+ if (typeof candidate !== "function") throw new TypeError(`Missing detail action method '${action.name}' on viewset.`);
90
+ return candidate.call(viewset, ctx, id);
91
+ }
92
+ invokeCollectionAction(viewset, action, ctx) {
93
+ const candidate = viewset[action.name];
94
+ if (typeof candidate !== "function") throw new TypeError(`Missing collection action method '${action.name}' on viewset.`);
95
+ return candidate.call(viewset, ctx);
96
+ }
97
+ createHandler(handler, options) {
98
+ return async (req, res, next) => {
99
+ try {
100
+ const user = options.getUser ? await options.getUser(req) : null;
101
+ const request = this.toRequestFromExpress(req);
102
+ const ctx = RequestContext.create(request, user);
103
+ ctx.params = this.normalizeParams(req.params);
104
+ const rawId = req.params.id;
105
+ const id = Array.isArray(rawId) ? rawId[0] : rawId;
106
+ const response = (await this.callHandler(handler, ctx, id)).toWebResponse();
107
+ res.status(response.status);
108
+ response.headers.forEach((value, key) => {
109
+ res.setHeader(key, value);
110
+ });
111
+ if (!response.body) {
112
+ res.end();
113
+ return;
114
+ }
115
+ const text = await response.text();
116
+ res.send(text);
117
+ } catch (error) {
118
+ next(error);
119
+ }
120
+ };
121
+ }
122
+ normalizeParams(params) {
123
+ return Object.fromEntries(Object.entries(params).map(([key, value]) => [key, Array.isArray(value) ? value[0] ?? "" : value]));
124
+ }
125
+ async callHandler(handler, ctx, id) {
126
+ if (id && handler.length > 1) return handler(ctx, id);
127
+ return handler(ctx);
128
+ }
129
+ toRequestFromExpress(req) {
130
+ const protocol = req.protocol || "http";
131
+ const host = req.get("host") || "localhost";
132
+ const url = `${protocol}://${host}${req.originalUrl || req.url}`;
133
+ const headers = new Headers(req.headers);
134
+ const body = this.normalizeBody(req);
135
+ if (body !== undefined && !headers.has("content-type") && this.isJsonLike(req.body)) headers.set("content-type", "application/json; charset=utf-8");
136
+ return new Request(url, {
137
+ method: req.method,
138
+ headers,
139
+ body
140
+ });
141
+ }
142
+ normalizeBody(req) {
143
+ if (["GET", "HEAD"].includes(req.method)) return undefined;
144
+ if (req.body === null || req.body === undefined) return undefined;
145
+ if (typeof req.body === "string" || this.hasTag(req.body, "Uint8Array") || this.hasTag(req.body, "ArrayBuffer")) return req.body;
146
+ if (this.isJsonLike(req.body)) return JSON.stringify(req.body);
147
+ return undefined;
148
+ }
149
+ isJsonLike(value) {
150
+ if (value === null) return true;
151
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return true;
152
+ if (Array.isArray(value)) return value.every((item) => this.isJsonLike(item));
153
+ if (typeof value === "object") return Object.values(value).every((item) => this.isJsonLike(item));
154
+ return false;
155
+ }
156
+ hasTag(value, tag) {
157
+ return value !== null && value !== undefined && Object.prototype.toString.call(value) === `[object ${tag}]`;
158
+ }
159
+ registerMethod(registrar, method, path, handler) {
160
+ switch (method) {
161
+ case "GET":
162
+ registrar.get(path, handler);
163
+ return;
164
+ case "POST":
165
+ registrar.post(path, handler);
166
+ return;
167
+ case "PATCH":
168
+ registrar.patch(path, handler);
169
+ return;
170
+ case "PUT":
171
+ registrar.put(path, handler);
172
+ return;
173
+ default: registrar.delete(path, handler);
174
+ }
175
+ }
176
+ normalizeBasePath(basePath) {
177
+ const trimmed = basePath.trim();
178
+ if (!trimmed || trimmed === "/") return "/";
179
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
180
+ }
181
+ joinPath(base, subPath) {
182
+ const normalizedSubPath = subPath.replace(/^\/+|\/+$/g, "");
183
+ return base === "/" ? `/${normalizedSubPath}` : `${base}/${normalizedSubPath}`;
184
+ }
185
+ registerActionMethods(registrar, methods, path, handler) {
186
+ for (const method of methods) this.registerMethod(registrar, method, path, handler);
187
+ }
188
+ registerAllMethods(registrar, path, handler) {
189
+ this.registerMethod(registrar, "GET", path, handler);
190
+ this.registerMethod(registrar, "POST", path, handler);
191
+ this.registerMethod(registrar, "PUT", path, handler);
192
+ this.registerMethod(registrar, "PATCH", path, handler);
193
+ this.registerMethod(registrar, "DELETE", path, handler);
194
+ }
195
+ getViewSetActions(viewset) {
196
+ const constructorValue = viewset.constructor;
197
+ if (typeof constructorValue.getActions !== "function") return [];
198
+ return constructorValue.getActions(viewset);
199
+ }
200
+ };
201
+
202
+ //#endregion
203
+ //#region src/adapter/index.ts
204
+ var adapter_exports = {};
205
+ __export(adapter_exports, { ExpressAdapter: () => ExpressAdapter });
206
+
207
+ //#endregion
208
+ export { ExpressAdapter, adapter_exports };
209
+ //# sourceMappingURL=adapter-C6JD05QW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter-C6JD05QW.js","names":["req: ExpressRequest","handler: (ctx: RequestContext, ...args: unknown[]) => Promise<TangoResponse>","options: AdaptExpressOptions","viewset: ExpressCrudViewSet","apiView: ExpressAPIView","registrar: ExpressRouteRegistrar","basePath: string","path: string","collectionPath: string","detailPath: string | undefined","action: ResolvedViewSetActionDescriptor","ctx: RequestContext","id: string","res: ExpressResponse","next: NextFunction","params: Record<string, string | string[]>","id?: string","value: unknown","tag: string","method: ViewSetActionMethod","handler: RequestHandler","base: string","subPath: string","methods: readonly ViewSetActionMethod[]"],"sources":["../src/adapter/ExpressAdapter.ts","../src/adapter/index.ts"],"sourcesContent":["import type { Request as ExpressRequest, Response as ExpressResponse, NextFunction, RequestHandler } from 'express';\nimport { Router } from 'express';\nimport { RequestContext } from '@danceroutine/tango-resources';\nimport { TangoQueryParams, TangoResponse } from '@danceroutine/tango-core';\nimport {\n FRAMEWORK_ADAPTER_BRAND,\n type FrameworkAdapter,\n type FrameworkAdapterOptions,\n} from '@danceroutine/tango-adapters-core/adapter';\n\ntype ViewSetActionMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n\ntype ResolvedViewSetActionDescriptor = {\n name: string;\n scope: 'detail' | 'collection';\n methods: readonly ViewSetActionMethod[];\n path: string;\n};\n\n/**\n * Adapter options for Express integration.\n */\nexport type AdaptExpressOptions = FrameworkAdapterOptions<ExpressRequest>;\n\n/**\n * Minimal CRUD viewset contract used by adapter route registration helpers.\n */\nexport interface ExpressCrudViewSet {\n list(ctx: RequestContext): Promise<TangoResponse>;\n create(ctx: RequestContext): Promise<TangoResponse>;\n retrieve(ctx: RequestContext, id: string): Promise<TangoResponse>;\n update(ctx: RequestContext, id: string): Promise<TangoResponse>;\n destroy(ctx: RequestContext, id: string): Promise<TangoResponse>;\n}\n\nexport interface ExpressAPIView {\n dispatch(ctx: RequestContext): Promise<TangoResponse>;\n}\n\n/**\n * Minimal route registrar interface implemented by Express apps and routers.\n */\nexport interface ExpressRouteRegistrar {\n get(path: string, handler: RequestHandler): unknown;\n post(path: string, handler: RequestHandler): unknown;\n patch(path: string, handler: RequestHandler): unknown;\n put(path: string, handler: RequestHandler): unknown;\n delete(path: string, handler: RequestHandler): unknown;\n}\n\n/**\n * Express adapter that translates Express handlers to Tango `RequestContext`.\n */\nexport class ExpressAdapter implements FrameworkAdapter<Response, RequestHandler, ExpressRequest> {\n readonly __tangoBrand: typeof FRAMEWORK_ADAPTER_BRAND = FRAMEWORK_ADAPTER_BRAND;\n\n /**\n * Normalize an Express request into Tango query params.\n */\n toQueryParams(req: ExpressRequest): TangoQueryParams {\n const request = this.toRequestFromExpress(req);\n return TangoQueryParams.fromRequest(request);\n }\n\n /**\n * Adapt a Tango-style handler into an Express request handler.\n */\n adapt(\n handler: (ctx: RequestContext, ...args: unknown[]) => Promise<TangoResponse>,\n options: AdaptExpressOptions = {}\n ): RequestHandler {\n return this.createHandler(handler, options);\n }\n\n /**\n * Build an Express router that wires all CRUD routes for a viewset.\n */\n createViewSetRouter(viewset: ExpressCrudViewSet, options: AdaptExpressOptions = {}): Router {\n const router = Router();\n this.registerViewSet(router, '', viewset, options);\n return router;\n }\n\n /**\n * Build an Express router for a single-path APIView.\n */\n createAPIViewRouter(apiView: ExpressAPIView, options: AdaptExpressOptions = {}): Router {\n const router = Router();\n this.registerAPIView(router, '', apiView, options);\n return router;\n }\n\n /**\n * Register all CRUD routes for a viewset under a base path.\n */\n registerViewSet(\n registrar: ExpressRouteRegistrar,\n basePath: string,\n viewset: ExpressCrudViewSet,\n options: AdaptExpressOptions = {}\n ): void {\n const collectionPath = this.normalizeBasePath(basePath);\n const detailPath = collectionPath === '/' ? '/:id' : `${collectionPath}/:id`;\n\n registrar.get(\n collectionPath,\n this.adapt((ctx) => viewset.list(ctx), options)\n );\n registrar.post(\n collectionPath,\n this.adapt((ctx) => viewset.create(ctx), options)\n );\n registrar.get(\n detailPath,\n this.adapt((ctx, id) => viewset.retrieve(ctx, String(id)), options)\n );\n registrar.patch(\n detailPath,\n this.adapt((ctx, id) => viewset.update(ctx, String(id)), options)\n );\n registrar.put(\n detailPath,\n this.adapt((ctx, id) => viewset.update(ctx, String(id)), options)\n );\n registrar.delete(\n detailPath,\n this.adapt((ctx, id) => viewset.destroy(ctx, String(id)), options)\n );\n\n const actions = this.getViewSetActions(viewset);\n actions.forEach((action) => {\n const actionPath =\n action.scope === 'detail'\n ? this.joinPath(detailPath, action.path)\n : this.joinPath(collectionPath, action.path);\n const handler =\n action.scope === 'detail'\n ? this.adapt((ctx, id) => this.invokeDetailAction(viewset, action, ctx, String(id)), options)\n : this.adapt((ctx) => this.invokeCollectionAction(viewset, action, ctx), options);\n\n this.registerActionMethods(registrar, action.methods, actionPath, handler);\n });\n }\n\n /**\n * Register one APIView on a single path and dispatch by HTTP method.\n */\n registerAPIView(\n registrar: ExpressRouteRegistrar,\n path: string,\n apiView: ExpressAPIView,\n options: AdaptExpressOptions = {}\n ): void {\n const normalizedPath = this.normalizeBasePath(path);\n this.registerAllMethods(\n registrar,\n normalizedPath,\n this.adapt((ctx) => apiView.dispatch(ctx), options)\n );\n }\n\n /**\n * Register one GenericAPIView with collection and detail routes.\n */\n registerGenericAPIView(\n registrar: ExpressRouteRegistrar,\n collectionPath: string,\n detailPath: string | undefined,\n apiView: ExpressAPIView,\n options: AdaptExpressOptions = {}\n ): void {\n const normalizedCollectionPath = this.normalizeBasePath(collectionPath);\n const normalizedDetailPath = detailPath?.trim().length\n ? this.normalizeBasePath(detailPath)\n : this.joinPath(normalizedCollectionPath, ':id');\n\n registrar.get(\n normalizedCollectionPath,\n this.adapt((ctx) => apiView.dispatch(ctx), options)\n );\n registrar.post(\n normalizedCollectionPath,\n this.adapt((ctx) => apiView.dispatch(ctx), options)\n );\n registrar.get(\n normalizedDetailPath,\n this.adapt((ctx) => apiView.dispatch(ctx), options)\n );\n registrar.put(\n normalizedDetailPath,\n this.adapt((ctx) => apiView.dispatch(ctx), options)\n );\n registrar.patch(\n normalizedDetailPath,\n this.adapt((ctx) => apiView.dispatch(ctx), options)\n );\n registrar.delete(\n normalizedDetailPath,\n this.adapt((ctx) => apiView.dispatch(ctx), options)\n );\n }\n\n private invokeDetailAction(\n viewset: ExpressCrudViewSet,\n action: ResolvedViewSetActionDescriptor,\n ctx: RequestContext,\n id: string\n ): Promise<TangoResponse> {\n const candidate = (viewset as unknown as Record<string, unknown>)[action.name];\n if (typeof candidate !== 'function') {\n throw new TypeError(`Missing detail action method '${action.name}' on viewset.`);\n }\n return (\n candidate as (this: ExpressCrudViewSet, ctx: RequestContext, id: string) => Promise<TangoResponse>\n ).call(viewset, ctx, id);\n }\n\n private invokeCollectionAction(\n viewset: ExpressCrudViewSet,\n action: ResolvedViewSetActionDescriptor,\n ctx: RequestContext\n ): Promise<TangoResponse> {\n const candidate = (viewset as unknown as Record<string, unknown>)[action.name];\n if (typeof candidate !== 'function') {\n throw new TypeError(`Missing collection action method '${action.name}' on viewset.`);\n }\n return (candidate as (this: ExpressCrudViewSet, ctx: RequestContext) => Promise<TangoResponse>).call(\n viewset,\n ctx\n );\n }\n\n private createHandler(\n handler: (ctx: RequestContext, ...args: unknown[]) => Promise<TangoResponse>,\n options: AdaptExpressOptions\n ): RequestHandler {\n return async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {\n try {\n const user = options.getUser ? await options.getUser(req) : null;\n\n const request = this.toRequestFromExpress(req);\n const ctx = RequestContext.create(request, user);\n ctx.params = this.normalizeParams(req.params);\n\n const rawId = req.params.id;\n const id = Array.isArray(rawId) ? rawId[0] : rawId;\n const response = (await this.callHandler(handler, ctx, id)).toWebResponse();\n\n res.status(response.status);\n\n response.headers.forEach((value, key) => {\n res.setHeader(key, value);\n });\n\n if (!response.body) {\n res.end();\n return;\n }\n\n const text = await response.text();\n res.send(text);\n } catch (error) {\n next(error);\n }\n };\n }\n\n private normalizeParams(params: Record<string, string | string[]>): Record<string, string> {\n return Object.fromEntries(\n Object.entries(params).map(([key, value]) => [key, Array.isArray(value) ? (value[0] ?? '') : value])\n );\n }\n\n private async callHandler(\n handler: (ctx: RequestContext, ...args: unknown[]) => Promise<TangoResponse>,\n ctx: RequestContext,\n id?: string\n ): Promise<TangoResponse> {\n if (id && handler.length > 1) {\n return handler(ctx, id);\n }\n return handler(ctx);\n }\n\n private toRequestFromExpress(req: ExpressRequest): Request {\n const protocol = req.protocol || 'http';\n const host = req.get('host') || 'localhost';\n const url = `${protocol}://${host}${req.originalUrl || req.url}`;\n const headers = new Headers(req.headers as HeadersInit);\n const body = this.normalizeBody(req);\n\n if (body !== undefined && !headers.has('content-type') && this.isJsonLike(req.body)) {\n headers.set('content-type', 'application/json; charset=utf-8');\n }\n\n return new Request(url, {\n method: req.method,\n headers,\n body,\n });\n }\n\n private normalizeBody(req: ExpressRequest): BodyInit | null | undefined {\n if (['GET', 'HEAD'].includes(req.method)) {\n return undefined;\n }\n\n if (req.body === null || req.body === undefined) {\n return undefined;\n }\n\n if (\n typeof req.body === 'string' ||\n this.hasTag(req.body, 'Uint8Array') ||\n this.hasTag(req.body, 'ArrayBuffer')\n ) {\n return req.body;\n }\n\n if (this.isJsonLike(req.body)) {\n return JSON.stringify(req.body);\n }\n\n return undefined;\n }\n\n private isJsonLike(value: unknown): boolean {\n if (value === null) return true;\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return true;\n if (Array.isArray(value)) return value.every((item) => this.isJsonLike(item));\n if (typeof value === 'object') {\n return Object.values(value as Record<string, unknown>).every((item) => this.isJsonLike(item));\n }\n return false;\n }\n\n private hasTag(value: unknown, tag: string): boolean {\n return value !== null && value !== undefined && Object.prototype.toString.call(value) === `[object ${tag}]`;\n }\n\n private registerMethod(\n registrar: ExpressRouteRegistrar,\n method: ViewSetActionMethod,\n path: string,\n handler: RequestHandler\n ): void {\n switch (method) {\n case 'GET':\n registrar.get(path, handler);\n return;\n case 'POST':\n registrar.post(path, handler);\n return;\n case 'PATCH':\n registrar.patch(path, handler);\n return;\n case 'PUT':\n registrar.put(path, handler);\n return;\n default:\n registrar.delete(path, handler);\n }\n }\n\n private normalizeBasePath(basePath: string): string {\n const trimmed = basePath.trim();\n if (!trimmed || trimmed === '/') {\n return '/';\n }\n return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;\n }\n\n private joinPath(base: string, subPath: string): string {\n const normalizedSubPath = subPath.replace(/^\\/+|\\/+$/g, '');\n return base === '/' ? `/${normalizedSubPath}` : `${base}/${normalizedSubPath}`;\n }\n\n private registerActionMethods(\n registrar: ExpressRouteRegistrar,\n methods: readonly ViewSetActionMethod[],\n path: string,\n handler: RequestHandler\n ): void {\n for (const method of methods) {\n this.registerMethod(registrar, method, path, handler);\n }\n }\n\n private registerAllMethods(registrar: ExpressRouteRegistrar, path: string, handler: RequestHandler): void {\n this.registerMethod(registrar, 'GET', path, handler);\n this.registerMethod(registrar, 'POST', path, handler);\n this.registerMethod(registrar, 'PUT', path, handler);\n this.registerMethod(registrar, 'PATCH', path, handler);\n this.registerMethod(registrar, 'DELETE', path, handler);\n }\n\n private getViewSetActions(viewset: ExpressCrudViewSet): readonly ResolvedViewSetActionDescriptor[] {\n const constructorValue = viewset.constructor as {\n getActions?: (input: ExpressCrudViewSet) => readonly ResolvedViewSetActionDescriptor[];\n };\n\n if (typeof constructorValue.getActions !== 'function') {\n return [];\n }\n\n return constructorValue.getActions(viewset);\n }\n}\n","/**\n * Domain boundary barrel: centralizes this subdomain's public contract.\n */\n\nexport {\n ExpressAdapter,\n type AdaptExpressOptions,\n type ExpressAPIView,\n type ExpressCrudViewSet,\n type ExpressRouteRegistrar,\n} from './ExpressAdapter';\n"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -3,4 +3,4 @@
3
3
  * top-level symbols for TS-native ergonomic imports.
4
4
  */
5
5
  export * as adapter from './adapter/index';
6
- export { ExpressAdapter, type AdaptExpressOptions } from './adapter/index';
6
+ export { ExpressAdapter, type AdaptExpressOptions, type ExpressAPIView, type ExpressCrudViewSet, type ExpressRouteRegistrar, } from './adapter/index';
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { ExpressAdapter, adapter_exports } from "./adapter-BllmY5Er.js";
1
+ import { ExpressAdapter, adapter_exports } from "./adapter-C6JD05QW.js";
2
2
 
3
3
  export { ExpressAdapter, adapter_exports as adapter };
package/package.json CHANGED
@@ -1,55 +1,58 @@
1
1
  {
2
- "name": "@danceroutine/tango-adapters-express",
3
- "version": "0.1.0",
4
- "description": "Express adapter for Tango",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./dist/index.d.ts",
11
- "import": "./dist/index.js"
12
- },
13
- "./adapter": {
14
- "types": "./dist/adapter/index.d.ts",
15
- "import": "./dist/adapter/index.js"
16
- }
2
+ "name": "@danceroutine/tango-adapters-express",
3
+ "version": "1.0.0",
4
+ "description": "Express adapter for Tango",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
17
12
  },
18
- "files": [
19
- "dist"
20
- ],
21
- "scripts": {
22
- "build": "tsdown",
23
- "test": "vitest run --coverage",
24
- "test:watch": "vitest",
25
- "typecheck": "tsc --noEmit"
26
- },
27
- "keywords": [
28
- "tango",
29
- "express",
30
- "adapter"
31
- ],
32
- "author": "Pedro Del Moral Lopez",
33
- "license": "MIT",
34
- "repository": {
35
- "type": "git",
36
- "url": "https://github.com/danceroutine/tango.git",
37
- "directory": "packages/adapters/express"
38
- },
39
- "dependencies": {
40
- "@danceroutine/tango-resources": "workspace:*",
41
- "@danceroutine/tango-adapters-core": "workspace:*",
42
- "@danceroutine/tango-core": "workspace:*"
43
- },
44
- "peerDependencies": {
45
- "express": "^4.21.1 || ^5.0.0"
46
- },
47
- "devDependencies": {
48
- "@types/express": "^5.0.0",
49
- "@types/node": "^22.9.0",
50
- "express": "^4.21.1",
51
- "tsdown": "^0.4.0",
52
- "typescript": "^5.6.3",
53
- "vitest": "^4.0.6"
13
+ "./adapter": {
14
+ "types": "./dist/adapter/index.d.ts",
15
+ "import": "./dist/adapter/index.js"
54
16
  }
55
- }
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "keywords": [
22
+ "tango",
23
+ "express",
24
+ "adapter"
25
+ ],
26
+ "author": "Pedro Del Moral Lopez",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/danceroutine/tango.git",
31
+ "directory": "packages/adapters/express"
32
+ },
33
+ "dependencies": {
34
+ "@danceroutine/tango-core": "1.0.0",
35
+ "@danceroutine/tango-adapters-core": "1.0.0",
36
+ "@danceroutine/tango-resources": "1.0.0"
37
+ },
38
+ "peerDependencies": {
39
+ "express": "^4.21.1 || ^5.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/express": "^5.0.0",
43
+ "@types/node": "^22.9.0",
44
+ "express": "^4.21.1",
45
+ "tsdown": "^0.4.0",
46
+ "typescript": "^5.6.3",
47
+ "vitest": "^4.0.6",
48
+ "@danceroutine/tango-testing": "1.0.0"
49
+ },
50
+ "scripts": {
51
+ "build": "tsdown",
52
+ "test": "vitest run --coverage",
53
+ "test:watch": "vitest",
54
+ "typecheck": "pnpm run typecheck:prod && pnpm run typecheck:test",
55
+ "typecheck:prod": "tsc --noEmit -p tsconfig.json",
56
+ "typecheck:test": "tsc --noEmit -p tsconfig.tests.json"
57
+ }
58
+ }
@@ -1,88 +0,0 @@
1
- import { RequestContext } from '@danceroutine/tango-resources';
2
- export class ExpressAdapter {
3
- adapt(handler, options = {}) {
4
- return this.createHandler(handler, options);
5
- }
6
- createHandler(handler, options) {
7
- return async (req, res, next) => {
8
- try {
9
- const user = options.getUser ? await options.getUser(req) : null;
10
- const request = this.toRequestFromExpress(req);
11
- const ctx = RequestContext.create(request, user);
12
- ctx.params = Object.fromEntries(Object.entries(req.params).map(([key, value]) => [
13
- key,
14
- Array.isArray(value) ? (value[0] ?? '') : value,
15
- ]));
16
- const id = req.params.id;
17
- let response;
18
- if (id && handler.length > 1) {
19
- response = await handler(ctx, id);
20
- }
21
- else {
22
- response = await handler(ctx);
23
- }
24
- res.status(response.status);
25
- response.headers.forEach((value, key) => {
26
- res.setHeader(key, value);
27
- });
28
- if (response.body) {
29
- const text = await response.text();
30
- res.send(text);
31
- }
32
- else {
33
- res.end();
34
- }
35
- }
36
- catch (error) {
37
- next(error);
38
- }
39
- };
40
- }
41
- toRequestFromExpress(req) {
42
- const protocol = req.protocol || 'http';
43
- const host = req.get('host') || 'localhost';
44
- const url = `${protocol}://${host}${req.originalUrl || req.url}`;
45
- const headers = new Headers(req.headers);
46
- const body = this.normalizeBody(req);
47
- if (body !== undefined && !headers.has('content-type') && this.isJsonLike(req.body)) {
48
- headers.set('content-type', 'application/json; charset=utf-8');
49
- }
50
- return new Request(url, {
51
- method: req.method,
52
- headers,
53
- body,
54
- });
55
- }
56
- normalizeBody(req) {
57
- if (['GET', 'HEAD'].includes(req.method)) {
58
- return undefined;
59
- }
60
- if (req.body == null) {
61
- return undefined;
62
- }
63
- if (typeof req.body === 'string' ||
64
- this.hasTag(req.body, 'Uint8Array') ||
65
- this.hasTag(req.body, 'ArrayBuffer')) {
66
- return req.body;
67
- }
68
- if (this.isJsonLike(req.body)) {
69
- return JSON.stringify(req.body);
70
- }
71
- return undefined;
72
- }
73
- isJsonLike(value) {
74
- if (value === null)
75
- return true;
76
- if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
77
- return true;
78
- if (Array.isArray(value))
79
- return value.every((item) => this.isJsonLike(item));
80
- if (typeof value === 'object') {
81
- return Object.values(value).every((item) => this.isJsonLike(item));
82
- }
83
- return false;
84
- }
85
- hasTag(value, tag) {
86
- return value != null && Object.prototype.toString.call(value) === `[object ${tag}]`;
87
- }
88
- }
@@ -1,81 +0,0 @@
1
- import { RequestContext } from "@danceroutine/tango-resources";
2
-
3
- //#region rolldown:runtime
4
- var __defProp = Object.defineProperty;
5
- var __export = (target, all) => {
6
- for (var name in all) __defProp(target, name, {
7
- get: all[name],
8
- enumerable: true
9
- });
10
- };
11
-
12
- //#endregion
13
- //#region src/adapter/ExpressAdapter.ts
14
- var ExpressAdapter = class {
15
- adapt(handler, options = {}) {
16
- return this.createHandler(handler, options);
17
- }
18
- createHandler(handler, options) {
19
- return async (req, res, next) => {
20
- try {
21
- const user = options.getUser ? await options.getUser(req) : null;
22
- const request = this.toRequestFromExpress(req);
23
- const ctx = RequestContext.create(request, user);
24
- ctx.params = Object.fromEntries(Object.entries(req.params).map(([key, value]) => [key, Array.isArray(value) ? value[0] ?? "" : value]));
25
- const id = req.params.id;
26
- let response;
27
- if (id && handler.length > 1) response = await handler(ctx, id);
28
- else response = await handler(ctx);
29
- res.status(response.status);
30
- response.headers.forEach((value, key) => {
31
- res.setHeader(key, value);
32
- });
33
- if (response.body) {
34
- const text = await response.text();
35
- res.send(text);
36
- } else res.end();
37
- } catch (error) {
38
- next(error);
39
- }
40
- };
41
- }
42
- toRequestFromExpress(req) {
43
- const protocol = req.protocol || "http";
44
- const host = req.get("host") || "localhost";
45
- const url = `${protocol}://${host}${req.originalUrl || req.url}`;
46
- const headers = new Headers(req.headers);
47
- const body = this.normalizeBody(req);
48
- if (body !== undefined && !headers.has("content-type") && this.isJsonLike(req.body)) headers.set("content-type", "application/json; charset=utf-8");
49
- return new Request(url, {
50
- method: req.method,
51
- headers,
52
- body
53
- });
54
- }
55
- normalizeBody(req) {
56
- if (["GET", "HEAD"].includes(req.method)) return undefined;
57
- if (req.body == null) return undefined;
58
- if (typeof req.body === "string" || this.hasTag(req.body, "Uint8Array") || this.hasTag(req.body, "ArrayBuffer")) return req.body;
59
- if (this.isJsonLike(req.body)) return JSON.stringify(req.body);
60
- return undefined;
61
- }
62
- isJsonLike(value) {
63
- if (value === null) return true;
64
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return true;
65
- if (Array.isArray(value)) return value.every((item) => this.isJsonLike(item));
66
- if (typeof value === "object") return Object.values(value).every((item) => this.isJsonLike(item));
67
- return false;
68
- }
69
- hasTag(value, tag) {
70
- return value != null && Object.prototype.toString.call(value) === `[object ${tag}]`;
71
- }
72
- };
73
-
74
- //#endregion
75
- //#region src/adapter/index.ts
76
- var adapter_exports = {};
77
- __export(adapter_exports, { ExpressAdapter: () => ExpressAdapter });
78
-
79
- //#endregion
80
- export { ExpressAdapter, adapter_exports };
81
- //# sourceMappingURL=adapter-BllmY5Er.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"adapter-BllmY5Er.js","names":["handler: (ctx: RequestContext, ...args: unknown[]) => Promise<Response>","options: AdaptExpressOptions","req: ExpressRequest","res: ExpressResponse","next: NextFunction","response: Response","value: unknown","tag: string"],"sources":["../src/adapter/ExpressAdapter.ts","../src/adapter/index.ts"],"sourcesContent":["import type { Request as ExpressRequest, Response as ExpressResponse, NextFunction, RequestHandler } from 'express';\nimport { RequestContext } from '@danceroutine/tango-resources';\nimport type { FrameworkAdapter, FrameworkAdapterOptions } from '@danceroutine/tango-adapters-core/adapter';\n\nexport interface AdaptExpressOptions extends FrameworkAdapterOptions<ExpressRequest> {}\n\nexport class ExpressAdapter implements FrameworkAdapter<Response, RequestHandler, ExpressRequest> {\n adapt(\n handler: (ctx: RequestContext, ...args: unknown[]) => Promise<Response>,\n options: AdaptExpressOptions = {}\n ): RequestHandler {\n return this.createHandler(handler, options);\n }\n\n private createHandler(\n handler: (ctx: RequestContext, ...args: unknown[]) => Promise<Response>,\n options: AdaptExpressOptions\n ): RequestHandler {\n return async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {\n try {\n const user = options.getUser ? await options.getUser(req) : null;\n\n const request = this.toRequestFromExpress(req);\n const ctx = RequestContext.create(request, user);\n ctx.params = Object.fromEntries(\n Object.entries(req.params).map(([key, value]) => [\n key,\n Array.isArray(value) ? (value[0] ?? '') : value,\n ])\n );\n\n const id = req.params.id;\n let response: Response;\n\n if (id && handler.length > 1) {\n response = await handler(ctx, id);\n } else {\n response = await handler(ctx);\n }\n\n res.status(response.status);\n\n response.headers.forEach((value, key) => {\n res.setHeader(key, value);\n });\n\n if (response.body) {\n const text = await response.text();\n res.send(text);\n } else {\n res.end();\n }\n } catch (error) {\n next(error);\n }\n };\n }\n\n private toRequestFromExpress(req: ExpressRequest): Request {\n const protocol = req.protocol || 'http';\n const host = req.get('host') || 'localhost';\n const url = `${protocol}://${host}${req.originalUrl || req.url}`;\n const headers = new Headers(req.headers as HeadersInit);\n const body = this.normalizeBody(req);\n\n if (body !== undefined && !headers.has('content-type') && this.isJsonLike(req.body)) {\n headers.set('content-type', 'application/json; charset=utf-8');\n }\n\n return new Request(url, {\n method: req.method,\n headers,\n body,\n });\n }\n\n private normalizeBody(req: ExpressRequest): BodyInit | null | undefined {\n if (['GET', 'HEAD'].includes(req.method)) {\n return undefined;\n }\n\n if (req.body == null) {\n return undefined;\n }\n\n if (\n typeof req.body === 'string' ||\n this.hasTag(req.body, 'Uint8Array') ||\n this.hasTag(req.body, 'ArrayBuffer')\n ) {\n return req.body;\n }\n\n if (this.isJsonLike(req.body)) {\n return JSON.stringify(req.body);\n }\n\n return undefined;\n }\n\n private isJsonLike(value: unknown): boolean {\n if (value === null) return true;\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return true;\n if (Array.isArray(value)) return value.every((item) => this.isJsonLike(item));\n if (typeof value === 'object') {\n return Object.values(value as Record<string, unknown>).every((item) => this.isJsonLike(item));\n }\n return false;\n }\n\n private hasTag(value: unknown, tag: string): boolean {\n return value != null && Object.prototype.toString.call(value) === `[object ${tag}]`;\n }\n}\n","/**\n * Domain boundary barrel: centralizes this subdomain's public contract.\n */\n\nexport { ExpressAdapter, type AdaptExpressOptions } from './ExpressAdapter';\n"],"mappings":";;;;;;;;;;;;;IAMa,iBAAN,MAA2F;CAC9F,MACIA,SACAC,UAA+B,CAAE,GACnB;AACd,SAAO,KAAK,cAAc,SAAS,QAAQ;CAC9C;CAED,cACID,SACAC,SACc;AACd,SAAO,OAAOC,KAAqBC,KAAsBC,SAAuB;AAC5E,OAAI;IACA,MAAM,OAAO,QAAQ,UAAU,MAAM,QAAQ,QAAQ,IAAI,GAAG;IAE5D,MAAM,UAAU,KAAK,qBAAqB,IAAI;IAC9C,MAAM,MAAM,eAAe,OAAO,SAAS,KAAK;AAChD,QAAI,SAAS,OAAO,YAChB,OAAO,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,MAAM,KAAK,CAC7C,KACA,MAAM,QAAQ,MAAM,GAAI,MAAM,MAAM,KAAM,KAC7C,EAAC,CACL;IAED,MAAM,KAAK,IAAI,OAAO;IACtB,IAAIC;AAEJ,QAAI,MAAM,QAAQ,SAAS,EACvB,YAAW,MAAM,QAAQ,KAAK,GAAG;IAEjC,YAAW,MAAM,QAAQ,IAAI;AAGjC,QAAI,OAAO,SAAS,OAAO;AAE3B,aAAS,QAAQ,QAAQ,CAAC,OAAO,QAAQ;AACrC,SAAI,UAAU,KAAK,MAAM;IAC5B,EAAC;AAEF,QAAI,SAAS,MAAM;KACf,MAAM,OAAO,MAAM,SAAS,MAAM;AAClC,SAAI,KAAK,KAAK;IACjB,MACG,KAAI,KAAK;GAEhB,SAAQ,OAAO;AACZ,SAAK,MAAM;GACd;EACJ;CACJ;CAED,qBAA6BH,KAA8B;EACvD,MAAM,WAAW,IAAI,YAAY;EACjC,MAAM,OAAO,IAAI,IAAI,OAAO,IAAI;EAChC,MAAM,OAAO,EAAE,SAAS,KAAK,KAAK,EAAE,IAAI,eAAe,IAAI,IAAI;EAC/D,MAAM,UAAU,IAAI,QAAQ,IAAI;EAChC,MAAM,OAAO,KAAK,cAAc,IAAI;AAEpC,MAAI,SAAS,cAAc,QAAQ,IAAI,eAAe,IAAI,KAAK,WAAW,IAAI,KAAK,CAC/E,SAAQ,IAAI,gBAAgB,kCAAkC;AAGlE,SAAO,IAAI,QAAQ,KAAK;GACpB,QAAQ,IAAI;GACZ;GACA;EACH;CACJ;CAED,cAAsBA,KAAkD;AACpE,MAAI,CAAC,OAAO,MAAO,EAAC,SAAS,IAAI,OAAO,CACpC,QAAO;AAGX,MAAI,IAAI,QAAQ,KACZ,QAAO;AAGX,aACW,IAAI,SAAS,YACpB,KAAK,OAAO,IAAI,MAAM,aAAa,IACnC,KAAK,OAAO,IAAI,MAAM,cAAc,CAEpC,QAAO,IAAI;AAGf,MAAI,KAAK,WAAW,IAAI,KAAK,CACzB,QAAO,KAAK,UAAU,IAAI,KAAK;AAGnC,SAAO;CACV;CAED,WAAmBI,OAAyB;AACxC,MAAI,UAAU,KAAM,QAAO;AAC3B,aAAW,UAAU,mBAAmB,UAAU,mBAAmB,UAAU,UAAW,QAAO;AACjG,MAAI,MAAM,QAAQ,MAAM,CAAE,QAAO,MAAM,MAAM,CAAC,SAAS,KAAK,WAAW,KAAK,CAAC;AAC7E,aAAW,UAAU,SACjB,QAAO,OAAO,OAAO,MAAiC,CAAC,MAAM,CAAC,SAAS,KAAK,WAAW,KAAK,CAAC;AAEjG,SAAO;CACV;CAED,OAAeA,OAAgBC,KAAsB;AACjD,SAAO,SAAS,QAAQ,OAAO,UAAU,SAAS,KAAK,MAAM,MAAM,UAAU,IAAI;CACpF;AACJ"}