@danceroutine/tango-adapters-express 1.10.3 → 1.11.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/README.md +10 -0
- package/dist/adapter/ExpressAdapter.d.ts +3 -1
- package/dist/adapter/index.js +1 -1
- package/dist/{adapter-C6JD05QW.js → adapter-CGeztuym.js} +31 -10
- package/dist/adapter-CGeztuym.js.map +1 -0
- package/dist/index.js +1 -1
- package/package.json +6 -5
- package/dist/adapter-C6JD05QW.js.map +0 -1
package/README.md
CHANGED
|
@@ -90,6 +90,16 @@ async function main(): Promise<void> {
|
|
|
90
90
|
|
|
91
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
92
|
|
|
93
|
+
If one resource should treat each write request as a single database unit of work, enable the adapter's writes-only transaction mode:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
adapter.registerViewSet(app, '/api/todos', viewset, {
|
|
97
|
+
transaction: 'writes',
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
That option wraps `POST`, `PUT`, `PATCH`, and `DELETE` requests in one `transaction.atomic(...)` boundary. `GET`, `HEAD`, and `OPTIONS` stay outside the wrapper. The current adapter transaction mode uses the Tango runtime your application installs as its default runtime.
|
|
102
|
+
|
|
93
103
|
## Public API
|
|
94
104
|
|
|
95
105
|
The root export includes:
|
|
@@ -35,6 +35,7 @@ export interface ExpressRouteRegistrar {
|
|
|
35
35
|
*/
|
|
36
36
|
export declare class ExpressAdapter implements FrameworkAdapter<Response, RequestHandler, ExpressRequest> {
|
|
37
37
|
readonly __tangoBrand: typeof FRAMEWORK_ADAPTER_BRAND;
|
|
38
|
+
private readonly requestExecutor;
|
|
38
39
|
/**
|
|
39
40
|
* Normalize an Express request into Tango query params.
|
|
40
41
|
*/
|
|
@@ -66,8 +67,9 @@ export declare class ExpressAdapter implements FrameworkAdapter<Response, Reques
|
|
|
66
67
|
private invokeDetailAction;
|
|
67
68
|
private invokeCollectionAction;
|
|
68
69
|
private createHandler;
|
|
70
|
+
private pipeReadableStream;
|
|
71
|
+
private normalizeResponseBody;
|
|
69
72
|
private normalizeParams;
|
|
70
|
-
private callHandler;
|
|
71
73
|
private toRequestFromExpress;
|
|
72
74
|
private normalizeBody;
|
|
73
75
|
private isJsonLike;
|
package/dist/adapter/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { Readable } from "node:stream";
|
|
1
2
|
import { Router } from "express";
|
|
2
3
|
import { RequestContext } from "@danceroutine/tango-resources";
|
|
3
4
|
import { TangoQueryParams } from "@danceroutine/tango-core";
|
|
4
|
-
import { FRAMEWORK_ADAPTER_BRAND } from "@danceroutine/tango-adapters-core/adapter";
|
|
5
|
+
import { FRAMEWORK_ADAPTER_BRAND, FrameworkAdapterRequestExecutor } from "@danceroutine/tango-adapters-core/adapter";
|
|
5
6
|
|
|
6
7
|
//#region rolldown:runtime
|
|
7
8
|
var __defProp = Object.defineProperty;
|
|
@@ -16,6 +17,7 @@ var __export = (target, all) => {
|
|
|
16
17
|
//#region src/adapter/ExpressAdapter.ts
|
|
17
18
|
var ExpressAdapter = class {
|
|
18
19
|
__tangoBrand = FRAMEWORK_ADAPTER_BRAND;
|
|
20
|
+
requestExecutor = new FrameworkAdapterRequestExecutor();
|
|
19
21
|
/**
|
|
20
22
|
* Normalize an Express request into Tango query params.
|
|
21
23
|
*/
|
|
@@ -103,29 +105,48 @@ var ExpressAdapter = class {
|
|
|
103
105
|
ctx.params = this.normalizeParams(req.params);
|
|
104
106
|
const rawId = req.params.id;
|
|
105
107
|
const id = Array.isArray(rawId) ? rawId[0] : rawId;
|
|
106
|
-
const
|
|
108
|
+
const tangoResponse = await this.requestExecutor.forHandler({
|
|
109
|
+
handler,
|
|
110
|
+
ctx,
|
|
111
|
+
id
|
|
112
|
+
}).runResponse(req.method, options.transaction);
|
|
113
|
+
const response = tangoResponse.toWebResponse();
|
|
107
114
|
res.status(response.status);
|
|
108
115
|
response.headers.forEach((value, key) => {
|
|
109
116
|
res.setHeader(key, value);
|
|
110
117
|
});
|
|
111
|
-
if (
|
|
118
|
+
if (response.body === null) {
|
|
112
119
|
res.end();
|
|
113
120
|
return;
|
|
114
121
|
}
|
|
115
|
-
|
|
116
|
-
|
|
122
|
+
if (tangoResponse.body !== null) {
|
|
123
|
+
await this.pipeReadableStream(response.body, res);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const body = new Uint8Array(await response.arrayBuffer());
|
|
127
|
+
res.send(this.normalizeResponseBody(body, response.headers));
|
|
117
128
|
} catch (error) {
|
|
118
129
|
next(error);
|
|
119
130
|
}
|
|
120
131
|
};
|
|
121
132
|
}
|
|
133
|
+
async pipeReadableStream(body, res) {
|
|
134
|
+
await new Promise((resolve, reject) => {
|
|
135
|
+
const stream = Readable.fromWeb(body);
|
|
136
|
+
stream.on("error", reject);
|
|
137
|
+
res.on("error", reject);
|
|
138
|
+
res.on("finish", () => resolve());
|
|
139
|
+
stream.pipe(res);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
normalizeResponseBody(body, headers) {
|
|
143
|
+
const contentType = headers.get("content-type")?.toLowerCase() ?? "";
|
|
144
|
+
if (contentType.startsWith("text/") || contentType.includes("json")) return new TextDecoder().decode(body);
|
|
145
|
+
return Buffer.from(body);
|
|
146
|
+
}
|
|
122
147
|
normalizeParams(params) {
|
|
123
148
|
return Object.fromEntries(Object.entries(params).map(([key, value]) => [key, Array.isArray(value) ? value[0] ?? "" : value]));
|
|
124
149
|
}
|
|
125
|
-
async callHandler(handler, ctx, id) {
|
|
126
|
-
if (id && handler.length > 1) return handler(ctx, id);
|
|
127
|
-
return handler(ctx);
|
|
128
|
-
}
|
|
129
150
|
toRequestFromExpress(req) {
|
|
130
151
|
const protocol = req.protocol || "http";
|
|
131
152
|
const host = req.get("host") || "localhost";
|
|
@@ -206,4 +227,4 @@ __export(adapter_exports, { ExpressAdapter: () => ExpressAdapter });
|
|
|
206
227
|
|
|
207
228
|
//#endregion
|
|
208
229
|
export { ExpressAdapter, adapter_exports };
|
|
209
|
-
//# sourceMappingURL=adapter-
|
|
230
|
+
//# sourceMappingURL=adapter-CGeztuym.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter-CGeztuym.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","body: ReadableStream<Uint8Array<ArrayBuffer>>","body: Uint8Array<ArrayBuffer>","headers: Headers","params: Record<string, string | 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 { Readable } from 'node:stream';\nimport type { ReadableStream as NodeReadableStream } from 'node:stream/web';\nimport { Router } from 'express';\nimport { RequestContext } from '@danceroutine/tango-resources';\nimport { TangoQueryParams, TangoResponse } from '@danceroutine/tango-core';\nimport {\n FRAMEWORK_ADAPTER_BRAND,\n FrameworkAdapterRequestExecutor,\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 private readonly requestExecutor = new FrameworkAdapterRequestExecutor();\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 tangoResponse = await this.requestExecutor.forHandler({ handler, ctx, id }).runResponse(\n req.method,\n options.transaction\n );\n const response = tangoResponse.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 === null) {\n res.end();\n return;\n }\n\n if (tangoResponse.body !== null) {\n await this.pipeReadableStream(response.body, res);\n return;\n }\n\n const body = new Uint8Array<ArrayBuffer>(await response.arrayBuffer());\n res.send(this.normalizeResponseBody(body, response.headers));\n } catch (error) {\n next(error);\n }\n };\n }\n\n private async pipeReadableStream(\n body: ReadableStream<Uint8Array<ArrayBuffer>>,\n res: ExpressResponse\n ): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n const stream = Readable.fromWeb(body as unknown as NodeReadableStream);\n stream.on('error', reject);\n res.on('error', reject);\n res.on('finish', () => resolve());\n stream.pipe(res);\n });\n }\n\n private normalizeResponseBody(body: Uint8Array<ArrayBuffer>, headers: Headers): string | Buffer {\n const contentType = headers.get('content-type')?.toLowerCase() ?? '';\n if (contentType.startsWith('text/') || contentType.includes('json')) {\n return new TextDecoder().decode(body);\n }\n\n return Buffer.from(body);\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 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.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danceroutine/tango-adapters-express",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "Express adapter for Tango",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"directory": "packages/adapters/express"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@danceroutine/tango-core": "1.
|
|
35
|
-
"@danceroutine/tango-
|
|
36
|
-
"@danceroutine/tango-
|
|
34
|
+
"@danceroutine/tango-core": "1.11.0",
|
|
35
|
+
"@danceroutine/tango-resources": "1.11.0",
|
|
36
|
+
"@danceroutine/tango-adapters-core": "1.11.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"express": "^4.21.1 || ^5.0.0"
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
"tsdown": "^0.4.0",
|
|
46
46
|
"typescript": "^5.6.3",
|
|
47
47
|
"vitest": "^4.0.6",
|
|
48
|
-
"@danceroutine/tango-
|
|
48
|
+
"@danceroutine/tango-orm": "1.11.0",
|
|
49
|
+
"@danceroutine/tango-testing": "1.11.0"
|
|
49
50
|
},
|
|
50
51
|
"scripts": {
|
|
51
52
|
"build": "tsdown",
|
|
@@ -1 +0,0 @@
|
|
|
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":";;;;;;;;;;;;;;;;IAqDa,iBAAN,MAA2F;CAC9F,eAAwD;;;;CAKxD,cAAcA,KAAuC;EACjD,MAAM,UAAU,KAAK,qBAAqB,IAAI;AAC9C,SAAO,iBAAiB,YAAY,QAAQ;CAC/C;;;;CAKD,MACIC,SACAC,UAA+B,CAAE,GACnB;AACd,SAAO,KAAK,cAAc,SAAS,QAAQ;CAC9C;;;;CAKD,oBAAoBC,SAA6BD,UAA+B,CAAE,GAAU;EACxF,MAAM,SAAS,QAAQ;AACvB,OAAK,gBAAgB,QAAQ,IAAI,SAAS,QAAQ;AAClD,SAAO;CACV;;;;CAKD,oBAAoBE,SAAyBF,UAA+B,CAAE,GAAU;EACpF,MAAM,SAAS,QAAQ;AACvB,OAAK,gBAAgB,QAAQ,IAAI,SAAS,QAAQ;AAClD,SAAO;CACV;;;;CAKD,gBACIG,WACAC,UACAH,SACAD,UAA+B,CAAE,GAC7B;EACJ,MAAM,iBAAiB,KAAK,kBAAkB,SAAS;EACvD,MAAM,aAAa,mBAAmB,MAAM,UAAU,EAAE,eAAe;AAEvE,YAAU,IACN,gBACA,KAAK,MAAM,CAAC,QAAQ,QAAQ,KAAK,IAAI,EAAE,QAAQ,CAClD;AACD,YAAU,KACN,gBACA,KAAK,MAAM,CAAC,QAAQ,QAAQ,OAAO,IAAI,EAAE,QAAQ,CACpD;AACD,YAAU,IACN,YACA,KAAK,MAAM,CAAC,KAAK,OAAO,QAAQ,SAAS,KAAK,OAAO,GAAG,CAAC,EAAE,QAAQ,CACtE;AACD,YAAU,MACN,YACA,KAAK,MAAM,CAAC,KAAK,OAAO,QAAQ,OAAO,KAAK,OAAO,GAAG,CAAC,EAAE,QAAQ,CACpE;AACD,YAAU,IACN,YACA,KAAK,MAAM,CAAC,KAAK,OAAO,QAAQ,OAAO,KAAK,OAAO,GAAG,CAAC,EAAE,QAAQ,CACpE;AACD,YAAU,OACN,YACA,KAAK,MAAM,CAAC,KAAK,OAAO,QAAQ,QAAQ,KAAK,OAAO,GAAG,CAAC,EAAE,QAAQ,CACrE;EAED,MAAM,UAAU,KAAK,kBAAkB,QAAQ;AAC/C,UAAQ,QAAQ,CAAC,WAAW;GACxB,MAAM,aACF,OAAO,UAAU,WACX,KAAK,SAAS,YAAY,OAAO,KAAK,GACtC,KAAK,SAAS,gBAAgB,OAAO,KAAK;GACpD,MAAM,UACF,OAAO,UAAU,WACX,KAAK,MAAM,CAAC,KAAK,OAAO,KAAK,mBAAmB,SAAS,QAAQ,KAAK,OAAO,GAAG,CAAC,EAAE,QAAQ,GAC3F,KAAK,MAAM,CAAC,QAAQ,KAAK,uBAAuB,SAAS,QAAQ,IAAI,EAAE,QAAQ;AAEzF,QAAK,sBAAsB,WAAW,OAAO,SAAS,YAAY,QAAQ;EAC7E,EAAC;CACL;;;;CAKD,gBACIG,WACAE,MACAH,SACAF,UAA+B,CAAE,GAC7B;EACJ,MAAM,iBAAiB,KAAK,kBAAkB,KAAK;AACnD,OAAK,mBACD,WACA,gBACA,KAAK,MAAM,CAAC,QAAQ,QAAQ,SAAS,IAAI,EAAE,QAAQ,CACtD;CACJ;;;;CAKD,uBACIG,WACAG,gBACAC,YACAL,SACAF,UAA+B,CAAE,GAC7B;EACJ,MAAM,2BAA2B,KAAK,kBAAkB,eAAe;EACvE,MAAM,uBAAuB,YAAY,MAAM,CAAC,SAC1C,KAAK,kBAAkB,WAAW,GAClC,KAAK,SAAS,0BAA0B,MAAM;AAEpD,YAAU,IACN,0BACA,KAAK,MAAM,CAAC,QAAQ,QAAQ,SAAS,IAAI,EAAE,QAAQ,CACtD;AACD,YAAU,KACN,0BACA,KAAK,MAAM,CAAC,QAAQ,QAAQ,SAAS,IAAI,EAAE,QAAQ,CACtD;AACD,YAAU,IACN,sBACA,KAAK,MAAM,CAAC,QAAQ,QAAQ,SAAS,IAAI,EAAE,QAAQ,CACtD;AACD,YAAU,IACN,sBACA,KAAK,MAAM,CAAC,QAAQ,QAAQ,SAAS,IAAI,EAAE,QAAQ,CACtD;AACD,YAAU,MACN,sBACA,KAAK,MAAM,CAAC,QAAQ,QAAQ,SAAS,IAAI,EAAE,QAAQ,CACtD;AACD,YAAU,OACN,sBACA,KAAK,MAAM,CAAC,QAAQ,QAAQ,SAAS,IAAI,EAAE,QAAQ,CACtD;CACJ;CAED,mBACIC,SACAO,QACAC,KACAC,IACsB;EACtB,MAAM,YAAa,QAA+C,OAAO;AACzE,aAAW,cAAc,WACrB,OAAM,IAAI,WAAW,gCAAgC,OAAO,KAAK;AAErE,SAAO,UAEL,KAAK,SAAS,KAAK,GAAG;CAC3B;CAED,uBACIT,SACAO,QACAC,KACsB;EACtB,MAAM,YAAa,QAA+C,OAAO;AACzE,aAAW,cAAc,WACrB,OAAM,IAAI,WAAW,oCAAoC,OAAO,KAAK;AAEzE,SAAO,UAAyF,KAC5F,SACA,IACH;CACJ;CAED,cACIV,SACAC,SACc;AACd,SAAO,OAAOF,KAAqBa,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,KAAK,gBAAgB,IAAI,OAAO;IAE7C,MAAM,QAAQ,IAAI,OAAO;IACzB,MAAM,KAAK,MAAM,QAAQ,MAAM,GAAG,MAAM,KAAK;IAC7C,MAAM,WAAW,CAAC,MAAM,KAAK,YAAY,SAAS,KAAK,GAAG,EAAE,eAAe;AAE3E,QAAI,OAAO,SAAS,OAAO;AAE3B,aAAS,QAAQ,QAAQ,CAAC,OAAO,QAAQ;AACrC,SAAI,UAAU,KAAK,MAAM;IAC5B,EAAC;AAEF,SAAK,SAAS,MAAM;AAChB,SAAI,KAAK;AACT;IACH;IAED,MAAM,OAAO,MAAM,SAAS,MAAM;AAClC,QAAI,KAAK,KAAK;GACjB,SAAQ,OAAO;AACZ,SAAK,MAAM;GACd;EACJ;CACJ;CAED,gBAAwBC,QAAmE;AACvF,SAAO,OAAO,YACV,OAAO,QAAQ,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,MAAM,KAAK,CAAC,KAAK,MAAM,QAAQ,MAAM,GAAI,MAAM,MAAM,KAAM,KAAM,EAAC,CACvG;CACJ;CAED,MAAc,YACVd,SACAU,KACAK,IACsB;AACtB,MAAI,MAAM,QAAQ,SAAS,EACvB,QAAO,QAAQ,KAAK,GAAG;AAE3B,SAAO,QAAQ,IAAI;CACtB;CAED,qBAA6BhB,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,SAAS,QAAQ,IAAI,SAAS,UAClC,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,WAAmBiB,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,UAAU,QAAQ,UAAU,aAAa,OAAO,UAAU,SAAS,KAAK,MAAM,MAAM,UAAU,IAAI;CAC5G;CAED,eACIb,WACAc,QACAZ,MACAa,SACI;AACJ,UAAQ,QAAR;AACI,QAAK;AACD,cAAU,IAAI,MAAM,QAAQ;AAC5B;AACJ,QAAK;AACD,cAAU,KAAK,MAAM,QAAQ;AAC7B;AACJ,QAAK;AACD,cAAU,MAAM,MAAM,QAAQ;AAC9B;AACJ,QAAK;AACD,cAAU,IAAI,MAAM,QAAQ;AAC5B;AACJ,WACI,WAAU,OAAO,MAAM,QAAQ;EACtC;CACJ;CAED,kBAA0Bd,UAA0B;EAChD,MAAM,UAAU,SAAS,MAAM;AAC/B,OAAK,WAAW,YAAY,IACxB,QAAO;AAEX,SAAO,QAAQ,WAAW,IAAI,GAAG,WAAW,GAAG,QAAQ;CAC1D;CAED,SAAiBe,MAAcC,SAAyB;EACpD,MAAM,oBAAoB,QAAQ,QAAQ,cAAc,GAAG;AAC3D,SAAO,SAAS,OAAO,GAAG,kBAAkB,KAAK,EAAE,KAAK,GAAG,kBAAkB;CAChF;CAED,sBACIjB,WACAkB,SACAhB,MACAa,SACI;AACJ,OAAK,MAAM,UAAU,QACjB,MAAK,eAAe,WAAW,QAAQ,MAAM,QAAQ;CAE5D;CAED,mBAA2Bf,WAAkCE,MAAca,SAA+B;AACtG,OAAK,eAAe,WAAW,OAAO,MAAM,QAAQ;AACpD,OAAK,eAAe,WAAW,QAAQ,MAAM,QAAQ;AACrD,OAAK,eAAe,WAAW,OAAO,MAAM,QAAQ;AACpD,OAAK,eAAe,WAAW,SAAS,MAAM,QAAQ;AACtD,OAAK,eAAe,WAAW,UAAU,MAAM,QAAQ;CAC1D;CAED,kBAA0BjB,SAAyE;EAC/F,MAAM,mBAAmB,QAAQ;AAIjC,aAAW,iBAAiB,eAAe,WACvC,QAAO,CAAE;AAGb,SAAO,iBAAiB,WAAW,QAAQ;CAC9C;AACJ"}
|