@danceroutine/tango-adapters-express 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+ import type { Request as ExpressRequest, RequestHandler } from 'express';
2
+ import { RequestContext } from '@danceroutine/tango-resources';
3
+ import type { FrameworkAdapter, FrameworkAdapterOptions } from '@danceroutine/tango-adapters-core/adapter';
4
+ export interface AdaptExpressOptions extends FrameworkAdapterOptions<ExpressRequest> {
5
+ }
6
+ export declare class ExpressAdapter implements FrameworkAdapter<Response, RequestHandler, ExpressRequest> {
7
+ adapt(handler: (ctx: RequestContext, ...args: unknown[]) => Promise<Response>, options?: AdaptExpressOptions): RequestHandler;
8
+ private createHandler;
9
+ private toRequestFromExpress;
10
+ private normalizeBody;
11
+ private isJsonLike;
12
+ private hasTag;
13
+ }
@@ -0,0 +1,88 @@
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
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Domain boundary barrel: centralizes this subdomain's public contract.
3
+ */
4
+ export { ExpressAdapter, type AdaptExpressOptions } from './ExpressAdapter';
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Domain boundary barrel: centralizes this subdomain's public contract.
3
+ */
4
+ export { ExpressAdapter } from './ExpressAdapter';
@@ -0,0 +1,81 @@
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
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Bundled exports for Django-style domain drill-down imports, plus curated
3
+ * top-level symbols for TS-native ergonomic imports.
4
+ */
5
+ export * as adapter from './adapter/index';
6
+ export { ExpressAdapter, type AdaptExpressOptions } from './adapter/index';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import { ExpressAdapter, adapter_exports } from "./adapter-BllmY5Er.js";
2
+
3
+ export { ExpressAdapter, adapter_exports as adapter };
package/package.json ADDED
@@ -0,0 +1,55 @@
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
+ }
17
+ },
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"
54
+ }
55
+ }