@usebetterdev/tenant-express 0.2.1-beta.1 → 0.4.0-beta.2

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 CHANGED
@@ -1,3 +1,35 @@
1
1
  # @usebetterdev/tenant-express
2
2
 
3
- Express middleware helpers for Better Tenant.
3
+ Express middleware for [@usebetterdev/tenant](https://github.com/usebetter-dev/usebetter). Resolves the tenant from the incoming request and opens a database transaction with `SET LOCAL` so Postgres RLS applies.
4
+
5
+ Requires **Express 5+**.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @usebetterdev/tenant express
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import express from "express";
17
+ import { createExpressMiddleware } from "@usebetterdev/tenant/express";
18
+ import { tenant } from "./tenant.js";
19
+
20
+ const app = express();
21
+ app.use(express.json());
22
+ app.use("/api", createExpressMiddleware(tenant));
23
+
24
+ app.get("/api/projects", async (req, res) => {
25
+ const db = tenant.getDatabase();
26
+ if (!db) {
27
+ return res.status(500).json({ error: "No tenant-scoped database" });
28
+ }
29
+ res.json(await db.project.findMany());
30
+ });
31
+ ```
32
+
33
+ ## Peer dependency
34
+
35
+ Requires `express` (>= 5.0.0).
package/dist/index.cjs CHANGED
@@ -30,9 +30,17 @@ function createExpressMiddleware(tenant, options = {}) {
30
30
  try {
31
31
  await tenant.handleRequest(
32
32
  (0, import_tenant_core.toResolvableRequest)(request),
33
- async () => {
33
+ () => new Promise((resolve) => {
34
+ const done = () => resolve();
35
+ if (response.on) {
36
+ response.on("finish", done);
37
+ response.on("close", done);
38
+ }
34
39
  next();
35
- },
40
+ if (!response.on) {
41
+ done();
42
+ }
43
+ }),
36
44
  {
37
45
  onMissingTenant: async () => {
38
46
  if (options.onMissingTenant) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { toResolvableRequest } from \"@usebetterdev/tenant-core\";\nimport type {\n BetterTenantInstance,\n NodeLikeRequest,\n ResolvableRequest,\n TenantContext,\n} from \"@usebetterdev/tenant-core\";\n\nexport interface ExpressRequestLike extends NodeLikeRequest {\n headers: Record<string, string | string[] | number | undefined>;\n originalUrl?: string;\n hostname?: string;\n}\n\nexport interface ExpressResponseLike {\n status: (code: number) => ExpressResponseLike;\n json: (body: unknown) => unknown;\n}\n\nexport type ExpressNextFunction = (error?: unknown) => void;\n\nexport interface CreateExpressMiddlewareOptions {\n missingTenantStatus?: 401 | 404;\n onMissingTenant?: (\n request: ExpressRequestLike,\n response: ExpressResponseLike,\n ) => void | Promise<void>;\n}\n\nexport interface ExpressTenantLike extends Pick<\n BetterTenantInstance,\n \"handleRequest\" | \"resolverStrategies\"\n> {}\n\nexport type ExpressMiddleware = (\n request: ExpressRequestLike,\n response: ExpressResponseLike,\n next: ExpressNextFunction,\n) => Promise<void>;\n\nexport function createExpressMiddleware(\n tenant: ExpressTenantLike,\n options: CreateExpressMiddlewareOptions = {},\n): ExpressMiddleware {\n const missingTenantStatus = options.missingTenantStatus ?? 404;\n\n return async (request, response, next) => {\n try {\n await tenant.handleRequest<ResolvableRequest, void>(\n toResolvableRequest(request),\n async () => {\n next();\n },\n {\n onMissingTenant: async () => {\n if (options.onMissingTenant) {\n await options.onMissingTenant(request, response);\n return;\n }\n const checked = tenant.resolverStrategies;\n const message =\n checked.length > 0\n ? `Tenant could not be resolved. Checked: ${checked.join(\", \")}`\n : \"Tenant could not be resolved. No resolution strategies configured\";\n response.status(missingTenantStatus).json({ error: message });\n },\n },\n );\n } catch (error) {\n next(error);\n }\n };\n}\n\nexport type { TenantContext };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAAoC;AAwC7B,SAAS,wBACd,QACA,UAA0C,CAAC,GACxB;AACnB,QAAM,sBAAsB,QAAQ,uBAAuB;AAE3D,SAAO,OAAO,SAAS,UAAU,SAAS;AACxC,QAAI;AACF,YAAM,OAAO;AAAA,YACX,wCAAoB,OAAO;AAAA,QAC3B,YAAY;AACV,eAAK;AAAA,QACP;AAAA,QACA;AAAA,UACE,iBAAiB,YAAY;AAC3B,gBAAI,QAAQ,iBAAiB;AAC3B,oBAAM,QAAQ,gBAAgB,SAAS,QAAQ;AAC/C;AAAA,YACF;AACA,kBAAM,UAAU,OAAO;AACvB,kBAAM,UACJ,QAAQ,SAAS,IACb,0CAA0C,QAAQ,KAAK,IAAI,CAAC,KAC5D;AACN,qBAAS,OAAO,mBAAmB,EAAE,KAAK,EAAE,OAAO,QAAQ,CAAC;AAAA,UAC9D;AAAA,QACF;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AACd,WAAK,KAAK;AAAA,IACZ;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { toResolvableRequest } from \"@usebetterdev/tenant-core\";\nimport type {\n BetterTenantInstance,\n NodeLikeRequest,\n ResolvableRequest,\n TenantContext,\n} from \"@usebetterdev/tenant-core\";\n\nexport interface ExpressRequestLike extends NodeLikeRequest {\n headers: Record<string, string | string[] | number | undefined>;\n originalUrl?: string;\n hostname?: string;\n}\n\nexport interface ExpressResponseLike {\n status: (code: number) => ExpressResponseLike;\n json: (body: unknown) => unknown;\n /** Used to detect when the response is finished so the transaction stays open. Real Express responses always have this (inherited from http.ServerResponse). */\n on?: (event: string, listener: (...args: unknown[]) => void) => unknown;\n}\n\nexport type ExpressNextFunction = (error?: unknown) => void;\n\nexport interface CreateExpressMiddlewareOptions {\n missingTenantStatus?: 401 | 404;\n onMissingTenant?: (\n request: ExpressRequestLike,\n response: ExpressResponseLike,\n ) => void | Promise<void>;\n}\n\nexport interface ExpressTenantLike extends Pick<\n BetterTenantInstance,\n \"handleRequest\" | \"resolverStrategies\"\n> {}\n\nexport type ExpressMiddleware = (\n request: ExpressRequestLike,\n response: ExpressResponseLike,\n next: ExpressNextFunction,\n) => Promise<void>;\n\nexport function createExpressMiddleware(\n tenant: ExpressTenantLike,\n options: CreateExpressMiddlewareOptions = {},\n): ExpressMiddleware {\n const missingTenantStatus = options.missingTenantStatus ?? 404;\n\n return async (request, response, next) => {\n try {\n await tenant.handleRequest<ResolvableRequest, void>(\n toResolvableRequest(request),\n () =>\n new Promise<void>((resolve) => {\n const done = () => resolve();\n if (response.on) {\n response.on(\"finish\", done);\n response.on(\"close\", done);\n }\n next();\n if (!response.on) {\n done();\n }\n }),\n {\n onMissingTenant: async () => {\n if (options.onMissingTenant) {\n await options.onMissingTenant(request, response);\n return;\n }\n const checked = tenant.resolverStrategies;\n const message =\n checked.length > 0\n ? `Tenant could not be resolved. Checked: ${checked.join(\", \")}`\n : \"Tenant could not be resolved. No resolution strategies configured\";\n response.status(missingTenantStatus).json({ error: message });\n },\n },\n );\n } catch (error) {\n next(error);\n }\n };\n}\n\nexport type { TenantContext };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAAoC;AA0C7B,SAAS,wBACd,QACA,UAA0C,CAAC,GACxB;AACnB,QAAM,sBAAsB,QAAQ,uBAAuB;AAE3D,SAAO,OAAO,SAAS,UAAU,SAAS;AACxC,QAAI;AACF,YAAM,OAAO;AAAA,YACX,wCAAoB,OAAO;AAAA,QAC3B,MACE,IAAI,QAAc,CAAC,YAAY;AAC7B,gBAAM,OAAO,MAAM,QAAQ;AAC3B,cAAI,SAAS,IAAI;AACf,qBAAS,GAAG,UAAU,IAAI;AAC1B,qBAAS,GAAG,SAAS,IAAI;AAAA,UAC3B;AACA,eAAK;AACL,cAAI,CAAC,SAAS,IAAI;AAChB,iBAAK;AAAA,UACP;AAAA,QACF,CAAC;AAAA,QACH;AAAA,UACE,iBAAiB,YAAY;AAC3B,gBAAI,QAAQ,iBAAiB;AAC3B,oBAAM,QAAQ,gBAAgB,SAAS,QAAQ;AAC/C;AAAA,YACF;AACA,kBAAM,UAAU,OAAO;AACvB,kBAAM,UACJ,QAAQ,SAAS,IACb,0CAA0C,QAAQ,KAAK,IAAI,CAAC,KAC5D;AACN,qBAAS,OAAO,mBAAmB,EAAE,KAAK,EAAE,OAAO,QAAQ,CAAC;AAAA,UAC9D;AAAA,QACF;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AACd,WAAK,KAAK;AAAA,IACZ;AAAA,EACF;AACF;","names":[]}
package/dist/index.d.cts CHANGED
@@ -9,6 +9,8 @@ interface ExpressRequestLike extends NodeLikeRequest {
9
9
  interface ExpressResponseLike {
10
10
  status: (code: number) => ExpressResponseLike;
11
11
  json: (body: unknown) => unknown;
12
+ /** Used to detect when the response is finished so the transaction stays open. Real Express responses always have this (inherited from http.ServerResponse). */
13
+ on?: (event: string, listener: (...args: unknown[]) => void) => unknown;
12
14
  }
13
15
  type ExpressNextFunction = (error?: unknown) => void;
14
16
  interface CreateExpressMiddlewareOptions {
package/dist/index.d.ts CHANGED
@@ -9,6 +9,8 @@ interface ExpressRequestLike extends NodeLikeRequest {
9
9
  interface ExpressResponseLike {
10
10
  status: (code: number) => ExpressResponseLike;
11
11
  json: (body: unknown) => unknown;
12
+ /** Used to detect when the response is finished so the transaction stays open. Real Express responses always have this (inherited from http.ServerResponse). */
13
+ on?: (event: string, listener: (...args: unknown[]) => void) => unknown;
12
14
  }
13
15
  type ExpressNextFunction = (error?: unknown) => void;
14
16
  interface CreateExpressMiddlewareOptions {
package/dist/index.js CHANGED
@@ -6,9 +6,17 @@ function createExpressMiddleware(tenant, options = {}) {
6
6
  try {
7
7
  await tenant.handleRequest(
8
8
  toResolvableRequest(request),
9
- async () => {
9
+ () => new Promise((resolve) => {
10
+ const done = () => resolve();
11
+ if (response.on) {
12
+ response.on("finish", done);
13
+ response.on("close", done);
14
+ }
10
15
  next();
11
- },
16
+ if (!response.on) {
17
+ done();
18
+ }
19
+ }),
12
20
  {
13
21
  onMissingTenant: async () => {
14
22
  if (options.onMissingTenant) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { toResolvableRequest } from \"@usebetterdev/tenant-core\";\nimport type {\n BetterTenantInstance,\n NodeLikeRequest,\n ResolvableRequest,\n TenantContext,\n} from \"@usebetterdev/tenant-core\";\n\nexport interface ExpressRequestLike extends NodeLikeRequest {\n headers: Record<string, string | string[] | number | undefined>;\n originalUrl?: string;\n hostname?: string;\n}\n\nexport interface ExpressResponseLike {\n status: (code: number) => ExpressResponseLike;\n json: (body: unknown) => unknown;\n}\n\nexport type ExpressNextFunction = (error?: unknown) => void;\n\nexport interface CreateExpressMiddlewareOptions {\n missingTenantStatus?: 401 | 404;\n onMissingTenant?: (\n request: ExpressRequestLike,\n response: ExpressResponseLike,\n ) => void | Promise<void>;\n}\n\nexport interface ExpressTenantLike extends Pick<\n BetterTenantInstance,\n \"handleRequest\" | \"resolverStrategies\"\n> {}\n\nexport type ExpressMiddleware = (\n request: ExpressRequestLike,\n response: ExpressResponseLike,\n next: ExpressNextFunction,\n) => Promise<void>;\n\nexport function createExpressMiddleware(\n tenant: ExpressTenantLike,\n options: CreateExpressMiddlewareOptions = {},\n): ExpressMiddleware {\n const missingTenantStatus = options.missingTenantStatus ?? 404;\n\n return async (request, response, next) => {\n try {\n await tenant.handleRequest<ResolvableRequest, void>(\n toResolvableRequest(request),\n async () => {\n next();\n },\n {\n onMissingTenant: async () => {\n if (options.onMissingTenant) {\n await options.onMissingTenant(request, response);\n return;\n }\n const checked = tenant.resolverStrategies;\n const message =\n checked.length > 0\n ? `Tenant could not be resolved. Checked: ${checked.join(\", \")}`\n : \"Tenant could not be resolved. No resolution strategies configured\";\n response.status(missingTenantStatus).json({ error: message });\n },\n },\n );\n } catch (error) {\n next(error);\n }\n };\n}\n\nexport type { TenantContext };\n"],"mappings":";AAAA,SAAS,2BAA2B;AAwC7B,SAAS,wBACd,QACA,UAA0C,CAAC,GACxB;AACnB,QAAM,sBAAsB,QAAQ,uBAAuB;AAE3D,SAAO,OAAO,SAAS,UAAU,SAAS;AACxC,QAAI;AACF,YAAM,OAAO;AAAA,QACX,oBAAoB,OAAO;AAAA,QAC3B,YAAY;AACV,eAAK;AAAA,QACP;AAAA,QACA;AAAA,UACE,iBAAiB,YAAY;AAC3B,gBAAI,QAAQ,iBAAiB;AAC3B,oBAAM,QAAQ,gBAAgB,SAAS,QAAQ;AAC/C;AAAA,YACF;AACA,kBAAM,UAAU,OAAO;AACvB,kBAAM,UACJ,QAAQ,SAAS,IACb,0CAA0C,QAAQ,KAAK,IAAI,CAAC,KAC5D;AACN,qBAAS,OAAO,mBAAmB,EAAE,KAAK,EAAE,OAAO,QAAQ,CAAC;AAAA,UAC9D;AAAA,QACF;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AACd,WAAK,KAAK;AAAA,IACZ;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { toResolvableRequest } from \"@usebetterdev/tenant-core\";\nimport type {\n BetterTenantInstance,\n NodeLikeRequest,\n ResolvableRequest,\n TenantContext,\n} from \"@usebetterdev/tenant-core\";\n\nexport interface ExpressRequestLike extends NodeLikeRequest {\n headers: Record<string, string | string[] | number | undefined>;\n originalUrl?: string;\n hostname?: string;\n}\n\nexport interface ExpressResponseLike {\n status: (code: number) => ExpressResponseLike;\n json: (body: unknown) => unknown;\n /** Used to detect when the response is finished so the transaction stays open. Real Express responses always have this (inherited from http.ServerResponse). */\n on?: (event: string, listener: (...args: unknown[]) => void) => unknown;\n}\n\nexport type ExpressNextFunction = (error?: unknown) => void;\n\nexport interface CreateExpressMiddlewareOptions {\n missingTenantStatus?: 401 | 404;\n onMissingTenant?: (\n request: ExpressRequestLike,\n response: ExpressResponseLike,\n ) => void | Promise<void>;\n}\n\nexport interface ExpressTenantLike extends Pick<\n BetterTenantInstance,\n \"handleRequest\" | \"resolverStrategies\"\n> {}\n\nexport type ExpressMiddleware = (\n request: ExpressRequestLike,\n response: ExpressResponseLike,\n next: ExpressNextFunction,\n) => Promise<void>;\n\nexport function createExpressMiddleware(\n tenant: ExpressTenantLike,\n options: CreateExpressMiddlewareOptions = {},\n): ExpressMiddleware {\n const missingTenantStatus = options.missingTenantStatus ?? 404;\n\n return async (request, response, next) => {\n try {\n await tenant.handleRequest<ResolvableRequest, void>(\n toResolvableRequest(request),\n () =>\n new Promise<void>((resolve) => {\n const done = () => resolve();\n if (response.on) {\n response.on(\"finish\", done);\n response.on(\"close\", done);\n }\n next();\n if (!response.on) {\n done();\n }\n }),\n {\n onMissingTenant: async () => {\n if (options.onMissingTenant) {\n await options.onMissingTenant(request, response);\n return;\n }\n const checked = tenant.resolverStrategies;\n const message =\n checked.length > 0\n ? `Tenant could not be resolved. Checked: ${checked.join(\", \")}`\n : \"Tenant could not be resolved. No resolution strategies configured\";\n response.status(missingTenantStatus).json({ error: message });\n },\n },\n );\n } catch (error) {\n next(error);\n }\n };\n}\n\nexport type { TenantContext };\n"],"mappings":";AAAA,SAAS,2BAA2B;AA0C7B,SAAS,wBACd,QACA,UAA0C,CAAC,GACxB;AACnB,QAAM,sBAAsB,QAAQ,uBAAuB;AAE3D,SAAO,OAAO,SAAS,UAAU,SAAS;AACxC,QAAI;AACF,YAAM,OAAO;AAAA,QACX,oBAAoB,OAAO;AAAA,QAC3B,MACE,IAAI,QAAc,CAAC,YAAY;AAC7B,gBAAM,OAAO,MAAM,QAAQ;AAC3B,cAAI,SAAS,IAAI;AACf,qBAAS,GAAG,UAAU,IAAI;AAC1B,qBAAS,GAAG,SAAS,IAAI;AAAA,UAC3B;AACA,eAAK;AACL,cAAI,CAAC,SAAS,IAAI;AAChB,iBAAK;AAAA,UACP;AAAA,QACF,CAAC;AAAA,QACH;AAAA,UACE,iBAAiB,YAAY;AAC3B,gBAAI,QAAQ,iBAAiB;AAC3B,oBAAM,QAAQ,gBAAgB,SAAS,QAAQ;AAC/C;AAAA,YACF;AACA,kBAAM,UAAU,OAAO;AACvB,kBAAM,UACJ,QAAQ,SAAS,IACb,0CAA0C,QAAQ,KAAK,IAAI,CAAC,KAC5D;AACN,qBAAS,OAAO,mBAAmB,EAAE,KAAK,EAAE,OAAO,QAAQ,CAAC;AAAA,UAC9D;AAAA,QACF;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AACd,WAAK,KAAK;AAAA,IACZ;AAAA,EACF;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usebetterdev/tenant-express",
3
- "version": "0.2.1-beta.1",
3
+ "version": "0.4.0-beta.2",
4
4
  "repository": "github:usebetter-dev/usebetter",
5
5
  "bugs": "https://github.com/usebetter-dev/usebetter/issues",
6
6
  "homepage": "https://github.com/usebetter-dev/usebetter#readme",
@@ -24,10 +24,10 @@
24
24
  "README.md"
25
25
  ],
26
26
  "dependencies": {
27
- "@usebetterdev/tenant-core": "0.2.1-beta.1"
27
+ "@usebetterdev/tenant-core": "0.4.0-beta.2"
28
28
  },
29
29
  "peerDependencies": {
30
- "express": ">=4.18.0"
30
+ "express": ">=5.0.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/express": "^5.0.0",
@@ -38,7 +38,7 @@
38
38
  "tsup": "^8.3.5",
39
39
  "typescript": "~5.7.2",
40
40
  "vitest": "^2.1.6",
41
- "@usebetterdev/test-utils": "0.2.1-beta.1"
41
+ "@usebetterdev/test-utils": "0.4.0-beta.2"
42
42
  },
43
43
  "engines": {
44
44
  "node": ">=22"