@niledatabase/express 5.0.0-alpha.14 → 5.0.0-alpha.15

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/CHANGELOG.md CHANGED
@@ -3,6 +3,12 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [5.0.0-alpha.15](https://github.com/niledatabase/nile-js/compare/v5.0.0-alpha.14...v5.0.0-alpha.15) (2025-06-25)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **server:** add better extensions ([2dd7417](https://github.com/niledatabase/nile-js/commit/2dd741726e2336b042d06a36ad091e6749098454))
11
+
6
12
  # [5.0.0-alpha.14](https://github.com/niledatabase/nile-js/compare/v5.0.0-alpha.13...v5.0.0-alpha.14) (2025-06-16)
7
13
 
8
14
  **Note:** Version bump only for package @niledatabase/express
package/dist/index.d.mts CHANGED
@@ -1,31 +1,13 @@
1
- import { Server, NileConfig } from '@niledatabase/server';
1
+ import * as _niledatabase_server from '@niledatabase/server';
2
+ import { Server } from '@niledatabase/server';
3
+ import { Request, Response, NextFunction } from 'express';
2
4
 
3
5
  declare function cleaner(val: string): string;
4
- declare function expressPaths(nile: Server): {
5
- paths: {
6
- get: string[];
7
- post: string[];
8
- put: string[];
9
- delete: string[];
10
- };
6
+ declare const express: (instance: Server) => {
7
+ id: string;
8
+ onSetContext: (req: Request, res: Response, next: NextFunction) => void;
9
+ onConfigure: () => void;
10
+ onHandleRequest: (req: Request, res: Response) => Promise<_niledatabase_server.RouteReturn>;
11
11
  };
12
- type HandlerConfig = {
13
- muteResponse?: boolean;
14
- init?: RequestInit;
15
- };
16
- declare function NileExpressHandler(nile: Server, config?: HandlerConfig & NileConfig): Promise<{
17
- handler: (req: any, res?: any) => Promise<{
18
- body: string;
19
- status: number;
20
- headers: Record<string, string | string[]>;
21
- response: Response;
22
- } | null | undefined>;
23
- paths: {
24
- get: string[];
25
- post: string[];
26
- put: string[];
27
- delete: string[];
28
- };
29
- }>;
30
12
 
31
- export { NileExpressHandler, cleaner, expressPaths };
13
+ export { cleaner, express };
package/dist/index.d.ts CHANGED
@@ -1,31 +1,13 @@
1
- import { Server, NileConfig } from '@niledatabase/server';
1
+ import * as _niledatabase_server from '@niledatabase/server';
2
+ import { Server } from '@niledatabase/server';
3
+ import { Request, Response, NextFunction } from 'express';
2
4
 
3
5
  declare function cleaner(val: string): string;
4
- declare function expressPaths(nile: Server): {
5
- paths: {
6
- get: string[];
7
- post: string[];
8
- put: string[];
9
- delete: string[];
10
- };
6
+ declare const express: (instance: Server) => {
7
+ id: string;
8
+ onSetContext: (req: Request, res: Response, next: NextFunction) => void;
9
+ onConfigure: () => void;
10
+ onHandleRequest: (req: Request, res: Response) => Promise<_niledatabase_server.RouteReturn>;
11
11
  };
12
- type HandlerConfig = {
13
- muteResponse?: boolean;
14
- init?: RequestInit;
15
- };
16
- declare function NileExpressHandler(nile: Server, config?: HandlerConfig & NileConfig): Promise<{
17
- handler: (req: any, res?: any) => Promise<{
18
- body: string;
19
- status: number;
20
- headers: Record<string, string | string[]>;
21
- response: Response;
22
- } | null | undefined>;
23
- paths: {
24
- get: string[];
25
- post: string[];
26
- put: string[];
27
- delete: string[];
28
- };
29
- }>;
30
12
 
31
- export { NileExpressHandler, cleaner, expressPaths };
13
+ export { cleaner, express };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";var R=Object.defineProperty;var x=Object.getOwnPropertyDescriptor;var P=Object.getOwnPropertyNames;var E=Object.prototype.hasOwnProperty;var T=(n,e)=>{for(var r in e)R(n,r,{get:e[r],enumerable:!0})},S=(n,e,r,d)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of P(e))!E.call(n,i)&&i!==r&&R(n,i,{get:()=>e[i],enumerable:!(d=x(e,i))||d.enumerable});return n};var U=n=>S(R({},"__esModule",{value:!0}),n);var C={};T(C,{NileExpressHandler:()=>j,cleaner:()=>p,expressPaths:()=>b});module.exports=U(C);function p(n){return n.replaceAll(/\{([^}]+)\}/g,":$1")}function b(n){let e=n.getPaths();return{paths:{get:e.get.map(p),post:e.post.map(p),put:e.put.map(p),delete:e.delete.map(p)}}}async function j(n,e){let r=e?.logger?e?.logger("express").error:()=>null;async function d(t,u){let f=new Headers;if(!t||typeof t!="object")return null;if(!("url"in t)||typeof t?.url!="string")return r("A url is necessary for the nile express handler"),null;let h="method"in t&&typeof t.method=="string"?t.method:"GET";"headers"in t&&typeof t.headers=="object"&&t.headers&&"cookie"in t.headers&&typeof t.headers.cookie=="string"&&f.set("cookie",t.headers.cookie);let g={method:h,...e?.init};"body"in t&&(h==="POST"||h==="PUT")&&(f.set("content-type","application/json"),g.body=JSON.stringify(t.body)),g.headers=f;let y=t.protocol+"://"+t.get("host")+t.originalUrl;try{new URL(y)}catch(s){return r("Invalid URL",{url:y,error:s}),null}let w=new Request(y,g),o;try{o=await n.handlers[h](w)}catch(s){r(s)}let a;if(o instanceof Response){try{a=await(await o.clone()).json()}catch{a=await o.text()}let s={};if(o.headers.forEach((c,l)=>{if(!["content-length","transfer-encoding"].includes(l.toLowerCase()))if(s[l]){let m=s[l];Array.isArray(m)?s[l]=[...m,c]:s[l]=[m,c]}else s[l]=c}),e?.muteResponse!==!0){u&&(u.status(o.status).set(s),typeof a=="string"?u.send(a):u.json(a??{}));return}return{body:a,status:o.status,headers:s,response:o}}else{r("Bad response",{response:o});return}}let{paths:i}=b(n);return{handler:d,paths:i}}0&&(module.exports={NileExpressHandler,cleaner,expressPaths});
1
+ "use strict";var x=Object.defineProperty;var R=Object.getOwnPropertyDescriptor;var y=Object.getOwnPropertyNames;var w=Object.prototype.hasOwnProperty;var m=(s,n)=>{for(var o in n)x(s,o,{get:n[o],enumerable:!0})},S=(s,n,o,e)=>{if(n&&typeof n=="object"||typeof n=="function")for(let t of y(n))!w.call(s,t)&&t!==o&&x(s,t,{get:()=>n[t],enumerable:!(e=R(n,t))||e.enumerable});return s};var T=s=>S(x({},"__esModule",{value:!0}),s);var C={};m(C,{cleaner:()=>h,express:()=>b});module.exports=T(C);var g=require("@niledatabase/server");function h(s){return s.replaceAll(/\{([^}]+)\}/g,":$1")}var b=s=>{let{error:n,debug:o}=s.logger("[EXTENSION][express]");return{id:"express",onSetContext:(e,t,a)=>{e.params.tenantId&&s.setContext({tenantId:e.params.tenantId}),e.headers&&s.setContext(e.headers),e instanceof Headers&&s.setContext(e),typeof a=="function"&&a()},onConfigure:()=>{let{paths:e}=s,t={get:e.get.map(h),post:e.post.map(h),put:e.put.map(h),delete:e.delete.map(h)};o(`paths configured ${JSON.stringify(t)}`),s.paths=t},onHandleRequest:async(e,t)=>{if(e instanceof Request)return o("using default response"),await s.handlers[e.method](e,{disableExtensions:["express"]});o("handling response"),s.setContext(e.headers);let a=e.protocol+"://"+e.get("host")+e.originalUrl;try{new URL(a)}catch{throw new Error("Invalid URL sent for handle request. Are you running express?")}let{method:f}=e,u={method:f,headers:new Headers};"headers"in e&&typeof e.headers=="object"&&e.headers&&"cookie"in e.headers&&typeof e.headers.cookie=="string"&&u.headers.set("cookie",e.headers.cookie),"body"in e&&(f==="POST"||f==="PUT")&&(u.body=JSON.stringify(e.body));let E=new Request(a,u);o(`[${f.toUpperCase()}]proxy request converted to ${a} with ${JSON.stringify(u)}`);let d=null;try{d=await s.handlers[e.method](E,{disableExtensions:["express"]})}catch(r){n(r)}let c;try{c=await(await d.clone()).json()}catch{c=await d.text()}let i={};return d.headers.forEach((r,p)=>{if(!["content-length","transfer-encoding"].includes(p.toLowerCase()))if(i[p]){let l=i[p];Array.isArray(l)?i[p]=[...l,r]:i[p]=[l,r]}else i[p]=r}),t.headersSent||(o("sending response"),t.status(d.status).set(i),typeof c=="string"?t.send(c):t.json(c??{})),g.ExtensionState.onHandleRequest}}};0&&(module.exports={cleaner,express});
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { NileConfig, Server } from '@niledatabase/server';\n\nexport function cleaner(val: string) {\n return val.replaceAll(/\\{([^}]+)\\}/g, ':$1');\n}\n\nexport function expressPaths(nile: Server) {\n const nilePaths = nile.getPaths();\n const paths = {\n get: nilePaths.get.map(cleaner),\n post: nilePaths.post.map(cleaner),\n put: nilePaths.put.map(cleaner),\n delete: nilePaths.delete.map(cleaner),\n };\n return {\n paths,\n };\n}\n\ntype HandlerConfig = { muteResponse?: boolean; init?: RequestInit };\nexport async function NileExpressHandler(\n nile: Server,\n config?: HandlerConfig & NileConfig\n) {\n const error = config?.logger ? config?.logger('express').error : () => null;\n async function handler(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n req: any,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n res?: any\n ): Promise<\n | {\n body: string;\n status: number;\n headers: Record<string, string | string[]>;\n response: Response;\n }\n | null\n | undefined\n > {\n const headers = new Headers();\n if (!req || typeof req !== 'object') {\n return null;\n }\n if (!('url' in req) || typeof req?.url !== 'string') {\n error('A url is necessary for the nile express handler');\n return null;\n }\n const method =\n 'method' in req && typeof req.method === 'string' ? req.method : 'GET';\n\n if (\n 'headers' in req &&\n typeof req.headers === 'object' &&\n req.headers &&\n 'cookie' in req.headers &&\n typeof req.headers.cookie === 'string'\n ) {\n headers.set('cookie', req.headers.cookie);\n }\n const _init: RequestInit = { method, ...config?.init };\n\n if ('body' in req) {\n if (method === 'POST' || method === 'PUT') {\n headers.set('content-type', 'application/json');\n _init.body = JSON.stringify(req.body);\n }\n }\n\n _init.headers = headers;\n\n const reqUrl = req.protocol + '://' + req.get('host') + req.originalUrl;\n\n // be sure its a valid url\n try {\n new URL(reqUrl);\n } catch (e) {\n error('Invalid URL', {\n url: reqUrl,\n error: e,\n });\n return null;\n }\n const proxyRequest = new Request(reqUrl, _init);\n let response;\n try {\n response = await nile.handlers[\n method as 'GET' | 'POST' | 'PUT' | 'DELETE'\n ](proxyRequest);\n } catch (e) {\n error(e);\n }\n\n let body;\n\n if (response instanceof Response) {\n try {\n const tryJson = await response.clone();\n body = await tryJson.json();\n } catch (e) {\n body = await response.text();\n }\n const newHeaders: Record<string, string | string[]> = {};\n response.headers.forEach((value, key) => {\n if (\n !['content-length', 'transfer-encoding'].includes(key.toLowerCase())\n ) {\n if (newHeaders[key]) {\n const prev = newHeaders[key];\n if (Array.isArray(prev)) {\n newHeaders[key] = [...prev, value];\n } else {\n newHeaders[key] = [prev, value];\n }\n } else {\n newHeaders[key] = value;\n }\n }\n });\n\n if (config?.muteResponse !== true) {\n if (res) {\n res.status(response.status).set(newHeaders);\n if (typeof body === 'string') {\n res.send(body);\n } else {\n res.json(body ?? {});\n }\n }\n return;\n }\n\n return {\n body,\n status: response.status,\n headers: newHeaders,\n response,\n };\n } else {\n error('Bad response', { response });\n return;\n }\n }\n const { paths } = expressPaths(nile);\n return { handler, paths };\n}\n"],"mappings":"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","names":["index_exports","__export","NileExpressHandler","cleaner","expressPaths","__toCommonJS","val","nile","nilePaths","config","error","handler","req","res","headers","method","_init","reqUrl","e","proxyRequest","response","body","newHeaders","value","key","prev","paths"]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { ExtensionState, Server } from '@niledatabase/server';\nimport type {\n Request as ExpressRequest,\n Response as ExpressResponse,\n NextFunction,\n} from 'express';\n\nexport function cleaner(val: string) {\n return val.replaceAll(/\\{([^}]+)\\}/g, ':$1');\n}\n\nexport const express = (instance: Server) => {\n const { error, debug } = instance.logger('[EXTENSION][express]');\n return {\n id: 'express',\n onSetContext: (\n req: ExpressRequest,\n res: ExpressResponse,\n next: NextFunction\n ) => {\n // do this first, since `cookies` does not persist across context set\n if (req.params.tenantId) {\n instance.setContext({ tenantId: req.params.tenantId });\n }\n\n if (req.headers) {\n instance.setContext(req.headers);\n }\n if (req instanceof Headers) {\n instance.setContext(req);\n }\n\n if (typeof next === 'function') {\n next();\n }\n },\n onConfigure: () => {\n const { paths: nilePaths } = instance;\n const paths = {\n get: nilePaths.get.map(cleaner),\n post: nilePaths.post.map(cleaner),\n put: nilePaths.put.map(cleaner),\n delete: nilePaths.delete.map(cleaner),\n };\n debug(`paths configured ${JSON.stringify(paths)}`);\n instance.paths = paths;\n },\n onHandleRequest: async (req: ExpressRequest, res: ExpressResponse) => {\n // handle standard request objects\n if (req instanceof Request) {\n debug('using default response');\n const response = await instance.handlers[\n req.method as 'GET' | 'POST' | 'PUT' | 'DELETE'\n // disable the extension so we don't re-handle\n ](req, { disableExtensions: ['express'] });\n return response;\n }\n\n debug('handling response');\n instance.setContext(req.headers);\n\n const reqUrl = req.protocol + '://' + req.get('host') + req.originalUrl;\n\n // be sure its a valid url\n try {\n new URL(reqUrl);\n } catch (e) {\n throw new Error(\n 'Invalid URL sent for handle request. Are you running express?'\n );\n }\n const { method } = req;\n const init: RequestInit = { method, headers: new Headers() };\n // seems like this should work without this, since `get/set context should do the things we think it should.\n if (\n 'headers' in req &&\n typeof req.headers === 'object' &&\n req.headers &&\n 'cookie' in req.headers &&\n typeof req.headers.cookie === 'string'\n ) {\n (init.headers as Headers).set('cookie', req.headers.cookie);\n }\n\n if ('body' in req) {\n if (method === 'POST' || method === 'PUT') {\n init.body = JSON.stringify(req.body);\n }\n }\n\n const proxyRequest = new Request(reqUrl, init);\n debug(\n `[${method.toUpperCase()}]proxy request converted to ${reqUrl} with ${JSON.stringify(\n init\n )}`\n );\n\n let response: Response = null as unknown as Response;\n\n try {\n response = (await instance.handlers[\n req.method as 'GET' | 'POST' | 'PUT' | 'DELETE'\n ](proxyRequest, { disableExtensions: ['express'] })) as Response;\n } catch (e) {\n error(e);\n }\n\n let body;\n try {\n const tryJson = await response.clone();\n body = await tryJson.json();\n } catch (e) {\n body = await response.text();\n }\n\n const newHeaders: Record<string, string | string[]> = {};\n response.headers.forEach((value, key) => {\n if (\n !['content-length', 'transfer-encoding'].includes(key.toLowerCase())\n ) {\n if (newHeaders[key]) {\n const prev = newHeaders[key];\n if (Array.isArray(prev)) {\n newHeaders[key] = [...prev, value];\n } else {\n newHeaders[key] = [prev, value];\n }\n } else {\n newHeaders[key] = value;\n }\n }\n });\n\n if (!res.headersSent) {\n debug('sending response');\n res.status(response.status).set(newHeaders);\n if (typeof body === 'string') {\n res.send(body);\n } else {\n res.json(body ?? {});\n }\n }\n // always return to prevent infinite loop\n return ExtensionState.onHandleRequest;\n },\n };\n};\n"],"mappings":"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","names":["index_exports","__export","cleaner","express","__toCommonJS","import_server","val","instance","error","debug","req","res","next","nilePaths","paths","reqUrl","method","init","proxyRequest","response","e","body","newHeaders","value","key","prev"]}
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- function d(r){return r.replaceAll(/\{([^}]+)\}/g,":$1")}function b(r){let n=r.getPaths();return{paths:{get:n.get.map(d),post:n.post.map(d),put:n.put.map(d),delete:n.delete.map(d)}}}async function w(r,n){let a=n?.logger?n?.logger("express").error:()=>null;async function y(e,c){let u=new Headers;if(!e||typeof e!="object")return null;if(!("url"in e)||typeof e?.url!="string")return a("A url is necessary for the nile express handler"),null;let p="method"in e&&typeof e.method=="string"?e.method:"GET";"headers"in e&&typeof e.headers=="object"&&e.headers&&"cookie"in e.headers&&typeof e.headers.cookie=="string"&&u.set("cookie",e.headers.cookie);let h={method:p,...n?.init};"body"in e&&(p==="POST"||p==="PUT")&&(u.set("content-type","application/json"),h.body=JSON.stringify(e.body)),h.headers=u;let f=e.protocol+"://"+e.get("host")+e.originalUrl;try{new URL(f)}catch(t){return a("Invalid URL",{url:f,error:t}),null}let R=new Request(f,h),s;try{s=await r.handlers[p](R)}catch(t){a(t)}let o;if(s instanceof Response){try{o=await(await s.clone()).json()}catch{o=await s.text()}let t={};if(s.headers.forEach((l,i)=>{if(!["content-length","transfer-encoding"].includes(i.toLowerCase()))if(t[i]){let g=t[i];Array.isArray(g)?t[i]=[...g,l]:t[i]=[g,l]}else t[i]=l}),n?.muteResponse!==!0){c&&(c.status(s.status).set(t),typeof o=="string"?c.send(o):c.json(o??{}));return}return{body:o,status:s.status,headers:t,response:s}}else{a("Bad response",{response:s});return}}let{paths:m}=b(r);return{handler:y,paths:m}}export{w as NileExpressHandler,d as cleaner,b as expressPaths};
1
+ import{ExtensionState as g}from"@niledatabase/server";function f(s){return s.replaceAll(/\{([^}]+)\}/g,":$1")}var y=s=>{let{error:l,debug:i}=s.logger("[EXTENSION][express]");return{id:"express",onSetContext:(e,n,o)=>{e.params.tenantId&&s.setContext({tenantId:e.params.tenantId}),e.headers&&s.setContext(e.headers),e instanceof Headers&&s.setContext(e),typeof o=="function"&&o()},onConfigure:()=>{let{paths:e}=s,n={get:e.get.map(f),post:e.post.map(f),put:e.put.map(f),delete:e.delete.map(f)};i(`paths configured ${JSON.stringify(n)}`),s.paths=n},onHandleRequest:async(e,n)=>{if(e instanceof Request)return i("using default response"),await s.handlers[e.method](e,{disableExtensions:["express"]});i("handling response"),s.setContext(e.headers);let o=e.protocol+"://"+e.get("host")+e.originalUrl;try{new URL(o)}catch{throw new Error("Invalid URL sent for handle request. Are you running express?")}let{method:c}=e,h={method:c,headers:new Headers};"headers"in e&&typeof e.headers=="object"&&e.headers&&"cookie"in e.headers&&typeof e.headers.cookie=="string"&&h.headers.set("cookie",e.headers.cookie),"body"in e&&(c==="POST"||c==="PUT")&&(h.body=JSON.stringify(e.body));let x=new Request(o,h);i(`[${c.toUpperCase()}]proxy request converted to ${o} with ${JSON.stringify(h)}`);let p=null;try{p=await s.handlers[e.method](x,{disableExtensions:["express"]})}catch(t){l(t)}let d;try{d=await(await p.clone()).json()}catch{d=await p.text()}let r={};return p.headers.forEach((t,a)=>{if(!["content-length","transfer-encoding"].includes(a.toLowerCase()))if(r[a]){let u=r[a];Array.isArray(u)?r[a]=[...u,t]:r[a]=[u,t]}else r[a]=t}),n.headersSent||(i("sending response"),n.status(p.status).set(r),typeof d=="string"?n.send(d):n.json(d??{})),g.onHandleRequest}}};export{f as cleaner,y as express};
2
2
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { NileConfig, Server } from '@niledatabase/server';\n\nexport function cleaner(val: string) {\n return val.replaceAll(/\\{([^}]+)\\}/g, ':$1');\n}\n\nexport function expressPaths(nile: Server) {\n const nilePaths = nile.getPaths();\n const paths = {\n get: nilePaths.get.map(cleaner),\n post: nilePaths.post.map(cleaner),\n put: nilePaths.put.map(cleaner),\n delete: nilePaths.delete.map(cleaner),\n };\n return {\n paths,\n };\n}\n\ntype HandlerConfig = { muteResponse?: boolean; init?: RequestInit };\nexport async function NileExpressHandler(\n nile: Server,\n config?: HandlerConfig & NileConfig\n) {\n const error = config?.logger ? config?.logger('express').error : () => null;\n async function handler(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n req: any,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n res?: any\n ): Promise<\n | {\n body: string;\n status: number;\n headers: Record<string, string | string[]>;\n response: Response;\n }\n | null\n | undefined\n > {\n const headers = new Headers();\n if (!req || typeof req !== 'object') {\n return null;\n }\n if (!('url' in req) || typeof req?.url !== 'string') {\n error('A url is necessary for the nile express handler');\n return null;\n }\n const method =\n 'method' in req && typeof req.method === 'string' ? req.method : 'GET';\n\n if (\n 'headers' in req &&\n typeof req.headers === 'object' &&\n req.headers &&\n 'cookie' in req.headers &&\n typeof req.headers.cookie === 'string'\n ) {\n headers.set('cookie', req.headers.cookie);\n }\n const _init: RequestInit = { method, ...config?.init };\n\n if ('body' in req) {\n if (method === 'POST' || method === 'PUT') {\n headers.set('content-type', 'application/json');\n _init.body = JSON.stringify(req.body);\n }\n }\n\n _init.headers = headers;\n\n const reqUrl = req.protocol + '://' + req.get('host') + req.originalUrl;\n\n // be sure its a valid url\n try {\n new URL(reqUrl);\n } catch (e) {\n error('Invalid URL', {\n url: reqUrl,\n error: e,\n });\n return null;\n }\n const proxyRequest = new Request(reqUrl, _init);\n let response;\n try {\n response = await nile.handlers[\n method as 'GET' | 'POST' | 'PUT' | 'DELETE'\n ](proxyRequest);\n } catch (e) {\n error(e);\n }\n\n let body;\n\n if (response instanceof Response) {\n try {\n const tryJson = await response.clone();\n body = await tryJson.json();\n } catch (e) {\n body = await response.text();\n }\n const newHeaders: Record<string, string | string[]> = {};\n response.headers.forEach((value, key) => {\n if (\n !['content-length', 'transfer-encoding'].includes(key.toLowerCase())\n ) {\n if (newHeaders[key]) {\n const prev = newHeaders[key];\n if (Array.isArray(prev)) {\n newHeaders[key] = [...prev, value];\n } else {\n newHeaders[key] = [prev, value];\n }\n } else {\n newHeaders[key] = value;\n }\n }\n });\n\n if (config?.muteResponse !== true) {\n if (res) {\n res.status(response.status).set(newHeaders);\n if (typeof body === 'string') {\n res.send(body);\n } else {\n res.json(body ?? {});\n }\n }\n return;\n }\n\n return {\n body,\n status: response.status,\n headers: newHeaders,\n response,\n };\n } else {\n error('Bad response', { response });\n return;\n }\n }\n const { paths } = expressPaths(nile);\n return { handler, paths };\n}\n"],"mappings":"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","names":["cleaner","val","expressPaths","nile","nilePaths","NileExpressHandler","config","error","handler","req","res","headers","method","_init","reqUrl","e","proxyRequest","response","body","newHeaders","value","key","prev","paths"]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { ExtensionState, Server } from '@niledatabase/server';\nimport type {\n Request as ExpressRequest,\n Response as ExpressResponse,\n NextFunction,\n} from 'express';\n\nexport function cleaner(val: string) {\n return val.replaceAll(/\\{([^}]+)\\}/g, ':$1');\n}\n\nexport const express = (instance: Server) => {\n const { error, debug } = instance.logger('[EXTENSION][express]');\n return {\n id: 'express',\n onSetContext: (\n req: ExpressRequest,\n res: ExpressResponse,\n next: NextFunction\n ) => {\n // do this first, since `cookies` does not persist across context set\n if (req.params.tenantId) {\n instance.setContext({ tenantId: req.params.tenantId });\n }\n\n if (req.headers) {\n instance.setContext(req.headers);\n }\n if (req instanceof Headers) {\n instance.setContext(req);\n }\n\n if (typeof next === 'function') {\n next();\n }\n },\n onConfigure: () => {\n const { paths: nilePaths } = instance;\n const paths = {\n get: nilePaths.get.map(cleaner),\n post: nilePaths.post.map(cleaner),\n put: nilePaths.put.map(cleaner),\n delete: nilePaths.delete.map(cleaner),\n };\n debug(`paths configured ${JSON.stringify(paths)}`);\n instance.paths = paths;\n },\n onHandleRequest: async (req: ExpressRequest, res: ExpressResponse) => {\n // handle standard request objects\n if (req instanceof Request) {\n debug('using default response');\n const response = await instance.handlers[\n req.method as 'GET' | 'POST' | 'PUT' | 'DELETE'\n // disable the extension so we don't re-handle\n ](req, { disableExtensions: ['express'] });\n return response;\n }\n\n debug('handling response');\n instance.setContext(req.headers);\n\n const reqUrl = req.protocol + '://' + req.get('host') + req.originalUrl;\n\n // be sure its a valid url\n try {\n new URL(reqUrl);\n } catch (e) {\n throw new Error(\n 'Invalid URL sent for handle request. Are you running express?'\n );\n }\n const { method } = req;\n const init: RequestInit = { method, headers: new Headers() };\n // seems like this should work without this, since `get/set context should do the things we think it should.\n if (\n 'headers' in req &&\n typeof req.headers === 'object' &&\n req.headers &&\n 'cookie' in req.headers &&\n typeof req.headers.cookie === 'string'\n ) {\n (init.headers as Headers).set('cookie', req.headers.cookie);\n }\n\n if ('body' in req) {\n if (method === 'POST' || method === 'PUT') {\n init.body = JSON.stringify(req.body);\n }\n }\n\n const proxyRequest = new Request(reqUrl, init);\n debug(\n `[${method.toUpperCase()}]proxy request converted to ${reqUrl} with ${JSON.stringify(\n init\n )}`\n );\n\n let response: Response = null as unknown as Response;\n\n try {\n response = (await instance.handlers[\n req.method as 'GET' | 'POST' | 'PUT' | 'DELETE'\n ](proxyRequest, { disableExtensions: ['express'] })) as Response;\n } catch (e) {\n error(e);\n }\n\n let body;\n try {\n const tryJson = await response.clone();\n body = await tryJson.json();\n } catch (e) {\n body = await response.text();\n }\n\n const newHeaders: Record<string, string | string[]> = {};\n response.headers.forEach((value, key) => {\n if (\n !['content-length', 'transfer-encoding'].includes(key.toLowerCase())\n ) {\n if (newHeaders[key]) {\n const prev = newHeaders[key];\n if (Array.isArray(prev)) {\n newHeaders[key] = [...prev, value];\n } else {\n newHeaders[key] = [prev, value];\n }\n } else {\n newHeaders[key] = value;\n }\n }\n });\n\n if (!res.headersSent) {\n debug('sending response');\n res.status(response.status).set(newHeaders);\n if (typeof body === 'string') {\n res.send(body);\n } else {\n res.json(body ?? {});\n }\n }\n // always return to prevent infinite loop\n return ExtensionState.onHandleRequest;\n },\n };\n};\n"],"mappings":"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","names":["ExtensionState","cleaner","val","express","instance","error","debug","req","res","next","nilePaths","paths","reqUrl","method","init","proxyRequest","response","e","body","newHeaders","value","key","prev"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@niledatabase/express",
3
- "version": "5.0.0-alpha.14",
3
+ "version": "5.0.0-alpha.15",
4
4
  "license": "MIT",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -34,9 +34,11 @@
34
34
  "@niledatabase/server": "*"
35
35
  },
36
36
  "devDependencies": {
37
+ "@types/express": "^5",
38
+ "express": "^5.1.0",
37
39
  "jest": "^29.7.0",
38
40
  "ts-jest": "^29.3.4",
39
41
  "tsup": "^8.5.0"
40
42
  },
41
- "gitHead": "434359be6def0e3aa431d4cf65bc9f508b4db5da"
43
+ "gitHead": "5838393fd8739e388754a43b0efcd8430d6d6027"
42
44
  }
@@ -1,54 +1,166 @@
1
- import { Server } from '@niledatabase/server';
1
+ import express from 'express';
2
+ import { ExtensionState, Server } from '@niledatabase/server';
3
+ import type {
4
+ Request as ExpressRequest,
5
+ Response as ExpressResponse,
6
+ } from 'express';
2
7
 
3
- import { expressPaths } from '.';
8
+ import { express as expressExtension, cleaner } from '.';
4
9
 
5
- describe('express', () => {
6
- it('cleans express paths', () => {
7
- const nile = new Server({
8
- apiUrl: 'http://localhost:3000',
9
- user: '123',
10
- password: '123',
11
- databaseName: '123',
10
+ describe('express extension', () => {
11
+ let app;
12
+ let instance: Server;
13
+
14
+ beforeEach(() => {
15
+ app = express();
16
+ app.use(express.json());
17
+
18
+ instance = {
19
+ logger: () => ({
20
+ error: jest.fn(),
21
+ debug: jest.fn(),
22
+ warn: jest.fn(),
23
+ info: jest.fn(),
24
+ }),
25
+ handlers: {
26
+ GET: jest
27
+ .fn()
28
+ .mockResolvedValue(
29
+ new Response(JSON.stringify({ msg: 'GET OK' }), { status: 200 })
30
+ ),
31
+ POST: jest
32
+ .fn()
33
+ .mockResolvedValue(
34
+ new Response(JSON.stringify({ msg: 'POST OK' }), { status: 201 })
35
+ ),
36
+ },
37
+ setContext: jest.fn(),
38
+ paths: {
39
+ get: ['/test/{id}'],
40
+ post: ['/submit/{formId}'],
41
+ put: [],
42
+ delete: [],
43
+ },
44
+ } as unknown as Server;
45
+ });
46
+
47
+ describe('cleaner()', () => {
48
+ it('replaces {param} with :param', () => {
49
+ expect(cleaner('/api/{tenantId}/resource')).toBe(
50
+ '/api/:tenantId/resource'
51
+ );
52
+ });
53
+ });
54
+
55
+ describe('onSetContext', () => {
56
+ it('sets context from params and headers', () => {
57
+ const ext = expressExtension(instance);
58
+ const req = {
59
+ params: { tenantId: 'abc' },
60
+ headers: { 'x-foo': 'bar' },
61
+ } as unknown as ExpressRequest;
62
+ const res = {} as unknown as ExpressResponse;
63
+ const next = jest.fn();
64
+
65
+ ext.onSetContext(req, res, next);
66
+
67
+ expect(instance.setContext).toHaveBeenCalledWith({ tenantId: 'abc' });
68
+ expect(instance.setContext).toHaveBeenCalledWith(req.headers);
69
+ expect(next).toHaveBeenCalled();
70
+ });
71
+ });
72
+
73
+ describe('onConfigure', () => {
74
+ it('cleans up path params and updates instance.paths', () => {
75
+ const ext = expressExtension(instance);
76
+ ext.onConfigure();
77
+ expect(instance.paths).toEqual({
78
+ get: ['/test/:id'],
79
+ post: ['/submit/:formId'],
80
+ put: [],
81
+ delete: [],
82
+ });
83
+ });
84
+ });
85
+
86
+ describe('onHandleRequest', () => {
87
+ it('proxies GET request with JSON response', async () => {
88
+ const ext = expressExtension(instance);
89
+ const req = {
90
+ method: 'GET',
91
+ protocol: 'http',
92
+ get: () => 'localhost',
93
+ originalUrl: '/api/test',
94
+ headers: { cookie: 'a=b' },
95
+ body: {},
96
+ } as unknown as ExpressRequest;
97
+ const res = {
98
+ headersSent: false,
99
+ status: jest.fn().mockReturnThis(),
100
+ set: jest.fn().mockReturnThis(),
101
+ json: jest.fn(),
102
+ send: jest.fn(),
103
+ } as unknown as ExpressResponse;
104
+
105
+ const result = await ext.onHandleRequest(req, res);
106
+
107
+ expect(instance.setContext).toHaveBeenCalledWith(req.headers);
108
+ expect(instance.handlers.GET).toHaveBeenCalled();
109
+ expect(res.status).toHaveBeenCalledWith(200);
110
+ expect(res.json).toHaveBeenCalledWith({ msg: 'GET OK' });
111
+ expect(result).toBe(ExtensionState.onHandleRequest);
112
+ });
113
+
114
+ it('handles non-JSON response fallback', async () => {
115
+ (instance.handlers.GET as jest.Mock).mockResolvedValue(
116
+ new Response('Plain text body', { status: 200 })
117
+ );
118
+ const ext = expressExtension(instance);
119
+
120
+ const req = {
121
+ method: 'GET',
122
+ protocol: 'http',
123
+ get: () => 'localhost',
124
+ originalUrl: '/api/test',
125
+ headers: {},
126
+ body: {},
127
+ } as unknown as ExpressRequest;
128
+ const res = {
129
+ headersSent: false,
130
+ status: jest.fn().mockReturnThis(),
131
+ set: jest.fn().mockReturnThis(),
132
+ json: jest.fn(),
133
+ send: jest.fn(),
134
+ } as unknown as ExpressResponse;
135
+
136
+ await ext.onHandleRequest(req, res);
137
+
138
+ expect(res.send).toHaveBeenCalledWith('Plain text body');
139
+ });
140
+
141
+ it('does not re-send headers if already sent', async () => {
142
+ const ext = expressExtension(instance);
143
+
144
+ const req = {
145
+ method: 'GET',
146
+ protocol: 'http',
147
+ get: () => 'localhost',
148
+ originalUrl: '/api/test',
149
+ headers: {},
150
+ body: {},
151
+ } as unknown as ExpressRequest;
152
+ const res = {
153
+ headersSent: true,
154
+ status: jest.fn(),
155
+ set: jest.fn(),
156
+ json: jest.fn(),
157
+ send: jest.fn(),
158
+ } as unknown as ExpressResponse;
159
+
160
+ const result = await ext.onHandleRequest(req, res);
161
+
162
+ expect(res.status).not.toHaveBeenCalled();
163
+ expect(result).toBe(ExtensionState.onHandleRequest);
12
164
  });
13
- const { paths } = expressPaths(nile);
14
- expect(Object.keys(paths)).toEqual(['get', 'post', 'put', 'delete']);
15
- expect(paths.delete).toEqual([
16
- '/api/tenants/:tenantId/users/:userId',
17
- '/api/tenants/:tenantId',
18
- ]);
19
- expect(paths.post).toEqual([
20
- '/api/tenants/:tenantId/users',
21
- '/api/signup',
22
- '/api/users',
23
- '/api/tenants',
24
- '/api/auth/session',
25
- '/api/auth/signin/:provider',
26
- '/api/auth/reset-password',
27
- '/api/auth/providers',
28
- '/api/auth/csrf',
29
- '/api/auth/callback/:provider',
30
- '/api/auth/signout',
31
- ]);
32
- expect(paths.put).toEqual([
33
- '/api/tenants/:tenantId/users',
34
- '/api/users',
35
- '/api/tenants/:tenantId',
36
- '/api/auth/reset-password',
37
- ]);
38
- expect(paths.get).toEqual([
39
- '/api/me',
40
- '/api/tenants/:tenantId/users',
41
- '/api/tenants',
42
- '/api/tenants/:tenantId',
43
- '/api/auth/session',
44
- '/api/auth/signin',
45
- '/api/auth/providers',
46
- '/api/auth/csrf',
47
- '/api/auth/reset-password',
48
- '/api/auth/callback',
49
- '/api/auth/signout',
50
- '/api/auth/verify-request',
51
- '/api/auth/error',
52
- ]);
53
165
  });
54
166
  });
package/src/index.ts CHANGED
@@ -1,105 +1,118 @@
1
- import { NileConfig, Server } from '@niledatabase/server';
1
+ import { ExtensionState, Server } from '@niledatabase/server';
2
+ import type {
3
+ Request as ExpressRequest,
4
+ Response as ExpressResponse,
5
+ NextFunction,
6
+ } from 'express';
2
7
 
3
8
  export function cleaner(val: string) {
4
9
  return val.replaceAll(/\{([^}]+)\}/g, ':$1');
5
10
  }
6
11
 
7
- export function expressPaths(nile: Server) {
8
- const nilePaths = nile.getPaths();
9
- const paths = {
10
- get: nilePaths.get.map(cleaner),
11
- post: nilePaths.post.map(cleaner),
12
- put: nilePaths.put.map(cleaner),
13
- delete: nilePaths.delete.map(cleaner),
14
- };
12
+ export const express = (instance: Server) => {
13
+ const { error, debug } = instance.logger('[EXTENSION][express]');
15
14
  return {
16
- paths,
17
- };
18
- }
15
+ id: 'express',
16
+ onSetContext: (
17
+ req: ExpressRequest,
18
+ res: ExpressResponse,
19
+ next: NextFunction
20
+ ) => {
21
+ // do this first, since `cookies` does not persist across context set
22
+ if (req.params.tenantId) {
23
+ instance.setContext({ tenantId: req.params.tenantId });
24
+ }
19
25
 
20
- type HandlerConfig = { muteResponse?: boolean; init?: RequestInit };
21
- export async function NileExpressHandler(
22
- nile: Server,
23
- config?: HandlerConfig & NileConfig
24
- ) {
25
- const error = config?.logger ? config?.logger('express').error : () => null;
26
- async function handler(
27
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
- req: any,
29
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
- res?: any
31
- ): Promise<
32
- | {
33
- body: string;
34
- status: number;
35
- headers: Record<string, string | string[]>;
36
- response: Response;
26
+ if (req.headers) {
27
+ instance.setContext(req.headers);
28
+ }
29
+ if (req instanceof Headers) {
30
+ instance.setContext(req);
37
31
  }
38
- | null
39
- | undefined
40
- > {
41
- const headers = new Headers();
42
- if (!req || typeof req !== 'object') {
43
- return null;
44
- }
45
- if (!('url' in req) || typeof req?.url !== 'string') {
46
- error('A url is necessary for the nile express handler');
47
- return null;
48
- }
49
- const method =
50
- 'method' in req && typeof req.method === 'string' ? req.method : 'GET';
51
32
 
52
- if (
53
- 'headers' in req &&
54
- typeof req.headers === 'object' &&
55
- req.headers &&
56
- 'cookie' in req.headers &&
57
- typeof req.headers.cookie === 'string'
58
- ) {
59
- headers.set('cookie', req.headers.cookie);
60
- }
61
- const _init: RequestInit = { method, ...config?.init };
33
+ if (typeof next === 'function') {
34
+ next();
35
+ }
36
+ },
37
+ onConfigure: () => {
38
+ const { paths: nilePaths } = instance;
39
+ const paths = {
40
+ get: nilePaths.get.map(cleaner),
41
+ post: nilePaths.post.map(cleaner),
42
+ put: nilePaths.put.map(cleaner),
43
+ delete: nilePaths.delete.map(cleaner),
44
+ };
45
+ debug(`paths configured ${JSON.stringify(paths)}`);
46
+ instance.paths = paths;
47
+ },
48
+ onHandleRequest: async (req: ExpressRequest, res: ExpressResponse) => {
49
+ // handle standard request objects
50
+ if (req instanceof Request) {
51
+ debug('using default response');
52
+ const response = await instance.handlers[
53
+ req.method as 'GET' | 'POST' | 'PUT' | 'DELETE'
54
+ // disable the extension so we don't re-handle
55
+ ](req, { disableExtensions: ['express'] });
56
+ return response;
57
+ }
58
+
59
+ debug('handling response');
60
+ instance.setContext(req.headers);
61
+
62
+ const reqUrl = req.protocol + '://' + req.get('host') + req.originalUrl;
62
63
 
63
- if ('body' in req) {
64
- if (method === 'POST' || method === 'PUT') {
65
- headers.set('content-type', 'application/json');
66
- _init.body = JSON.stringify(req.body);
64
+ // be sure its a valid url
65
+ try {
66
+ new URL(reqUrl);
67
+ } catch (e) {
68
+ throw new Error(
69
+ 'Invalid URL sent for handle request. Are you running express?'
70
+ );
71
+ }
72
+ const { method } = req;
73
+ const init: RequestInit = { method, headers: new Headers() };
74
+ // seems like this should work without this, since `get/set context should do the things we think it should.
75
+ if (
76
+ 'headers' in req &&
77
+ typeof req.headers === 'object' &&
78
+ req.headers &&
79
+ 'cookie' in req.headers &&
80
+ typeof req.headers.cookie === 'string'
81
+ ) {
82
+ (init.headers as Headers).set('cookie', req.headers.cookie);
67
83
  }
68
- }
69
84
 
70
- _init.headers = headers;
85
+ if ('body' in req) {
86
+ if (method === 'POST' || method === 'PUT') {
87
+ init.body = JSON.stringify(req.body);
88
+ }
89
+ }
71
90
 
72
- const reqUrl = req.protocol + '://' + req.get('host') + req.originalUrl;
91
+ const proxyRequest = new Request(reqUrl, init);
92
+ debug(
93
+ `[${method.toUpperCase()}]proxy request converted to ${reqUrl} with ${JSON.stringify(
94
+ init
95
+ )}`
96
+ );
73
97
 
74
- // be sure its a valid url
75
- try {
76
- new URL(reqUrl);
77
- } catch (e) {
78
- error('Invalid URL', {
79
- url: reqUrl,
80
- error: e,
81
- });
82
- return null;
83
- }
84
- const proxyRequest = new Request(reqUrl, _init);
85
- let response;
86
- try {
87
- response = await nile.handlers[
88
- method as 'GET' | 'POST' | 'PUT' | 'DELETE'
89
- ](proxyRequest);
90
- } catch (e) {
91
- error(e);
92
- }
98
+ let response: Response = null as unknown as Response;
93
99
 
94
- let body;
100
+ try {
101
+ response = (await instance.handlers[
102
+ req.method as 'GET' | 'POST' | 'PUT' | 'DELETE'
103
+ ](proxyRequest, { disableExtensions: ['express'] })) as Response;
104
+ } catch (e) {
105
+ error(e);
106
+ }
95
107
 
96
- if (response instanceof Response) {
108
+ let body;
97
109
  try {
98
110
  const tryJson = await response.clone();
99
111
  body = await tryJson.json();
100
112
  } catch (e) {
101
113
  body = await response.text();
102
114
  }
115
+
103
116
  const newHeaders: Record<string, string | string[]> = {};
104
117
  response.headers.forEach((value, key) => {
105
118
  if (
@@ -118,29 +131,17 @@ export async function NileExpressHandler(
118
131
  }
119
132
  });
120
133
 
121
- if (config?.muteResponse !== true) {
122
- if (res) {
123
- res.status(response.status).set(newHeaders);
124
- if (typeof body === 'string') {
125
- res.send(body);
126
- } else {
127
- res.json(body ?? {});
128
- }
134
+ if (!res.headersSent) {
135
+ debug('sending response');
136
+ res.status(response.status).set(newHeaders);
137
+ if (typeof body === 'string') {
138
+ res.send(body);
139
+ } else {
140
+ res.json(body ?? {});
129
141
  }
130
- return;
131
142
  }
132
-
133
- return {
134
- body,
135
- status: response.status,
136
- headers: newHeaders,
137
- response,
138
- };
139
- } else {
140
- error('Bad response', { response });
141
- return;
142
- }
143
- }
144
- const { paths } = expressPaths(nile);
145
- return { handler, paths };
146
- }
143
+ // always return to prevent infinite loop
144
+ return ExtensionState.onHandleRequest;
145
+ },
146
+ };
147
+ };