tenanso 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.
- package/LICENSE +21 -0
- package/README.md +263 -0
- package/dist/__tests__/connection-pool.test.d.ts +2 -0
- package/dist/__tests__/connection-pool.test.d.ts.map +1 -0
- package/dist/__tests__/connection-pool.test.js +88 -0
- package/dist/__tests__/connection-pool.test.js.map +1 -0
- package/dist/__tests__/hono-middleware.test.d.ts +2 -0
- package/dist/__tests__/hono-middleware.test.d.ts.map +1 -0
- package/dist/__tests__/hono-middleware.test.js +121 -0
- package/dist/__tests__/hono-middleware.test.js.map +1 -0
- package/dist/__tests__/tenanso.test.d.ts +2 -0
- package/dist/__tests__/tenanso.test.d.ts.map +1 -0
- package/dist/__tests__/tenanso.test.js +105 -0
- package/dist/__tests__/tenanso.test.js.map +1 -0
- package/dist/__tests__/turso-api.test.d.ts +2 -0
- package/dist/__tests__/turso-api.test.d.ts.map +1 -0
- package/dist/__tests__/turso-api.test.js +103 -0
- package/dist/__tests__/turso-api.test.js.map +1 -0
- package/dist/connection-pool.d.ts +14 -0
- package/dist/connection-pool.d.ts.map +1 -0
- package/dist/connection-pool.js +57 -0
- package/dist/connection-pool.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/hono.d.ts +198 -0
- package/dist/middleware/hono.d.ts.map +1 -0
- package/dist/middleware/hono.js +123 -0
- package/dist/middleware/hono.js.map +1 -0
- package/dist/tenanso.d.ts +63 -0
- package/dist/tenanso.d.ts.map +1 -0
- package/dist/tenanso.js +92 -0
- package/dist/tenanso.js.map +1 -0
- package/dist/turso-api.d.ts +13 -0
- package/dist/turso-api.d.ts.map +1 -0
- package/dist/turso-api.js +83 -0
- package/dist/turso-api.js.map +1 -0
- package/dist/types.d.ts +349 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { TursoApi } from "../turso-api.js";
|
|
3
|
+
const tursoConfig = {
|
|
4
|
+
organizationSlug: "test-org",
|
|
5
|
+
apiToken: "test-api-token",
|
|
6
|
+
group: "default",
|
|
7
|
+
};
|
|
8
|
+
describe("TursoApi", () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.restoreAllMocks();
|
|
11
|
+
});
|
|
12
|
+
describe("createDatabase", () => {
|
|
13
|
+
it("sends POST to Turso API without seed", async () => {
|
|
14
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify({ database: { Name: "my-tenant" } }), {
|
|
15
|
+
status: 200,
|
|
16
|
+
}));
|
|
17
|
+
const api = new TursoApi(tursoConfig, undefined);
|
|
18
|
+
await api.createDatabase("my-tenant");
|
|
19
|
+
expect(fetchSpy).toHaveBeenCalledOnce();
|
|
20
|
+
const [url, opts] = fetchSpy.mock.calls[0];
|
|
21
|
+
expect(url).toBe("https://api.turso.tech/v1/organizations/test-org/databases");
|
|
22
|
+
expect(opts?.method).toBe("POST");
|
|
23
|
+
const body = JSON.parse(opts?.body);
|
|
24
|
+
expect(body).toEqual({ name: "my-tenant", group: "default" });
|
|
25
|
+
});
|
|
26
|
+
it("includes seed when configured", async () => {
|
|
27
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("{}", { status: 200 }));
|
|
28
|
+
const api = new TursoApi(tursoConfig, { database: "seed-db" });
|
|
29
|
+
await api.createDatabase("my-tenant");
|
|
30
|
+
const body = JSON.parse(fetchSpy.mock.calls[0][1]?.body);
|
|
31
|
+
expect(body).toEqual({
|
|
32
|
+
name: "my-tenant",
|
|
33
|
+
group: "default",
|
|
34
|
+
seed: {
|
|
35
|
+
type: "database",
|
|
36
|
+
name: "seed-db",
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
it("rejects invalid tenant names", async () => {
|
|
41
|
+
const api = new TursoApi(tursoConfig, undefined);
|
|
42
|
+
await expect(api.createDatabase("")).rejects.toThrow("Invalid tenant name");
|
|
43
|
+
await expect(api.createDatabase("My Tenant")).rejects.toThrow("Invalid tenant name");
|
|
44
|
+
await expect(api.createDatabase("-starts-with-hyphen")).rejects.toThrow("Invalid tenant name");
|
|
45
|
+
await expect(api.createDatabase("has/slash")).rejects.toThrow("Invalid tenant name");
|
|
46
|
+
await expect(api.createDatabase("has space")).rejects.toThrow("Invalid tenant name");
|
|
47
|
+
});
|
|
48
|
+
it("accepts valid tenant names", async () => {
|
|
49
|
+
vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("{}", { status: 200 }));
|
|
50
|
+
const api = new TursoApi(tursoConfig, undefined);
|
|
51
|
+
await expect(api.createDatabase("valid-name")).resolves.not.toThrow();
|
|
52
|
+
await expect(api.createDatabase("tenant123")).resolves.not.toThrow();
|
|
53
|
+
await expect(api.createDatabase("a")).resolves.not.toThrow();
|
|
54
|
+
});
|
|
55
|
+
it("throws on API error", async () => {
|
|
56
|
+
vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("quota exceeded", { status: 429 }));
|
|
57
|
+
const api = new TursoApi(tursoConfig, undefined);
|
|
58
|
+
await expect(api.createDatabase("my-tenant")).rejects.toThrow('Failed to create database "my-tenant": 429');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe("deleteDatabase", () => {
|
|
62
|
+
it("sends DELETE to Turso API", async () => {
|
|
63
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("{}", { status: 200 }));
|
|
64
|
+
const api = new TursoApi(tursoConfig, undefined);
|
|
65
|
+
await api.deleteDatabase("my-tenant");
|
|
66
|
+
const [url, opts] = fetchSpy.mock.calls[0];
|
|
67
|
+
expect(url).toBe("https://api.turso.tech/v1/organizations/test-org/databases/my-tenant");
|
|
68
|
+
expect(opts?.method).toBe("DELETE");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe("listDatabases", () => {
|
|
72
|
+
it("returns database names", async () => {
|
|
73
|
+
vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify({
|
|
74
|
+
databases: [{ Name: "tenant-a" }, { Name: "tenant-b" }],
|
|
75
|
+
}), { status: 200 }));
|
|
76
|
+
const api = new TursoApi(tursoConfig, undefined);
|
|
77
|
+
const result = await api.listDatabases();
|
|
78
|
+
expect(result).toEqual(["tenant-a", "tenant-b"]);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe("databaseExists", () => {
|
|
82
|
+
it("returns true when database exists", async () => {
|
|
83
|
+
vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify({ database: { Name: "my-tenant" } }), {
|
|
84
|
+
status: 200,
|
|
85
|
+
}));
|
|
86
|
+
const api = new TursoApi(tursoConfig, undefined);
|
|
87
|
+
expect(await api.databaseExists("my-tenant")).toBe(true);
|
|
88
|
+
const [url] = vi.mocked(fetch).mock.calls[0];
|
|
89
|
+
expect(url).toBe("https://api.turso.tech/v1/organizations/test-org/databases/my-tenant");
|
|
90
|
+
});
|
|
91
|
+
it("returns false when database does not exist", async () => {
|
|
92
|
+
vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("not found", { status: 404 }));
|
|
93
|
+
const api = new TursoApi(tursoConfig, undefined);
|
|
94
|
+
expect(await api.databaseExists("nonexistent")).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
it("throws on non-404 errors", async () => {
|
|
97
|
+
vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("unauthorized", { status: 401 }));
|
|
98
|
+
const api = new TursoApi(tursoConfig, undefined);
|
|
99
|
+
await expect(api.databaseExists("my-tenant")).rejects.toThrow('Failed to check database "my-tenant": 401');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
//# sourceMappingURL=turso-api.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turso-api.test.js","sourceRoot":"","sources":["../../src/__tests__/turso-api.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAC9D,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAG3C,MAAM,WAAW,GAAgB;IAC/B,gBAAgB,EAAE,UAAU;IAC5B,QAAQ,EAAE,gBAAgB;IAC1B,KAAK,EAAE,SAAS;CACjB,CAAC;AAEF,QAAQ,CAAC,UAAU,EAAE,GAAG,EAAE;IACxB,UAAU,CAAC,GAAG,EAAE;QACd,EAAE,CAAC,eAAe,EAAE,CAAC;IACvB,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;QAC9B,EAAE,CAAC,sCAAsC,EAAE,KAAK,IAAI,EAAE;YACpD,MAAM,QAAQ,GAAG,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,iBAAiB,CAC9D,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,CAAC,EAAE;gBAChE,MAAM,EAAE,GAAG;aACZ,CAAC,CACH,CAAC;YAEF,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;YACjD,MAAM,GAAG,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;YAEtC,MAAM,CAAC,QAAQ,CAAC,CAAC,oBAAoB,EAAE,CAAC;YACxC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC;YAC5C,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CACd,4DAA4D,CAC7D,CAAC;YACF,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAElC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,IAAc,CAAC,CAAC;YAC9C,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;QAChE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,+BAA+B,EAAE,KAAK,IAAI,EAAE;YAC7C,MAAM,QAAQ,GAAG,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,iBAAiB,CAC9D,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CACpC,CAAC;YAEF,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,WAAW,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAC;YAC/D,MAAM,GAAG,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;YAEtC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,EAAE,IAAc,CAAC,CAAC;YACpE,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC;gBACnB,IAAI,EAAE,WAAW;gBACjB,KAAK,EAAE,SAAS;gBAChB,IAAI,EAAE;oBACJ,IAAI,EAAE,UAAU;oBAChB,IAAI,EAAE,SAAS;iBAChB;aACF,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,8BAA8B,EAAE,KAAK,IAAI,EAAE;YAC5C,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;YACjD,MAAM,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC;YAC5E,MAAM,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC;YACrF,MAAM,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,qBAAqB,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC;YAC/F,MAAM,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC;YACrF,MAAM,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC;QACvF,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,4BAA4B,EAAE,KAAK,IAAI,EAAE;YAC1C,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,iBAAiB,CAC7C,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CACpC,CAAC;YACF,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;YACjD,MAAM,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;YACtE,MAAM,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;YACrE,MAAM,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QAC/D,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,qBAAqB,EAAE,KAAK,IAAI,EAAE;YACnC,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,iBAAiB,CAC7C,IAAI,QAAQ,CAAC,gBAAgB,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAChD,CAAC;YAEF,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;YACjD,MAAM,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAC3D,4CAA4C,CAC7C,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;QAC9B,EAAE,CAAC,2BAA2B,EAAE,KAAK,IAAI,EAAE;YACzC,MAAM,QAAQ,GAAG,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,iBAAiB,CAC9D,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CACpC,CAAC;YAEF,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;YACjD,MAAM,GAAG,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;YAEtC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC;YAC5C,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CACd,sEAAsE,CACvE,CAAC;YACF,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACtC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;QAC7B,EAAE,CAAC,wBAAwB,EAAE,KAAK,IAAI,EAAE;YACtC,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,iBAAiB,CAC7C,IAAI,QAAQ,CACV,IAAI,CAAC,SAAS,CAAC;gBACb,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;aACxD,CAAC,EACF,EAAE,MAAM,EAAE,GAAG,EAAE,CAChB,CACF,CAAC;YAEF,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;YACjD,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,aAAa,EAAE,CAAC;YAEzC,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC;QACnD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;QAC9B,EAAE,CAAC,mCAAmC,EAAE,KAAK,IAAI,EAAE;YACjD,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,iBAAiB,CAC7C,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,CAAC,EAAE;gBAChE,MAAM,EAAE,GAAG;aACZ,CAAC,CACH,CAAC;YAEF,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;YACjD,MAAM,CAAC,MAAM,GAAG,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAEzD,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC;YAC9C,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CACd,sEAAsE,CACvE,CAAC;QACJ,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,4CAA4C,EAAE,KAAK,IAAI,EAAE;YAC1D,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,iBAAiB,CAC7C,IAAI,QAAQ,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAC3C,CAAC;YAEF,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;YACjD,MAAM,CAAC,MAAM,GAAG,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9D,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,0BAA0B,EAAE,KAAK,IAAI,EAAE;YACxC,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,iBAAiB,CAC7C,IAAI,QAAQ,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAC9C,CAAC;YAEF,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;YACjD,MAAM,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAC3D,2CAA2C,CAC5C,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DrizzleDb, TenansoConfig } from "./types.js";
|
|
2
|
+
export declare class ConnectionPool {
|
|
3
|
+
private readonly cache;
|
|
4
|
+
private readonly databaseUrl;
|
|
5
|
+
private readonly authToken;
|
|
6
|
+
private readonly schema;
|
|
7
|
+
private readonly maxConnections;
|
|
8
|
+
constructor(config: TenansoConfig);
|
|
9
|
+
getDb(tenant: string): DrizzleDb;
|
|
10
|
+
remove(tenant: string): void;
|
|
11
|
+
get size(): number;
|
|
12
|
+
private evictIfNeeded;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=connection-pool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection-pool.d.ts","sourceRoot":"","sources":["../src/connection-pool.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAQ3D,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;IACtD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqB;IAC/C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0B;IACjD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;gBAE5B,MAAM,EAAE,aAAa;IAOjC,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS;IAoBhC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAQ5B,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,OAAO,CAAC,aAAa;CAiBtB"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { drizzle } from "drizzle-orm/libsql";
|
|
2
|
+
import { createClient } from "@libsql/client";
|
|
3
|
+
export class ConnectionPool {
|
|
4
|
+
cache = new Map();
|
|
5
|
+
databaseUrl;
|
|
6
|
+
authToken;
|
|
7
|
+
schema;
|
|
8
|
+
maxConnections;
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.databaseUrl = config.databaseUrl;
|
|
11
|
+
this.authToken = config.authToken || undefined;
|
|
12
|
+
this.schema = config.schema;
|
|
13
|
+
this.maxConnections = config.maxConnections ?? 50;
|
|
14
|
+
}
|
|
15
|
+
getDb(tenant) {
|
|
16
|
+
const existing = this.cache.get(tenant);
|
|
17
|
+
if (existing) {
|
|
18
|
+
existing.lastUsed = Date.now();
|
|
19
|
+
return existing.db;
|
|
20
|
+
}
|
|
21
|
+
this.evictIfNeeded();
|
|
22
|
+
const url = this.databaseUrl.replace("{tenant}", tenant);
|
|
23
|
+
const client = createClient({
|
|
24
|
+
url,
|
|
25
|
+
...(this.authToken ? { authToken: this.authToken } : {}),
|
|
26
|
+
});
|
|
27
|
+
const db = drizzle(client, { schema: this.schema });
|
|
28
|
+
this.cache.set(tenant, { client, db, lastUsed: Date.now() });
|
|
29
|
+
return db;
|
|
30
|
+
}
|
|
31
|
+
remove(tenant) {
|
|
32
|
+
const entry = this.cache.get(tenant);
|
|
33
|
+
if (entry) {
|
|
34
|
+
entry.client.close();
|
|
35
|
+
this.cache.delete(tenant);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
get size() {
|
|
39
|
+
return this.cache.size;
|
|
40
|
+
}
|
|
41
|
+
evictIfNeeded() {
|
|
42
|
+
if (this.cache.size < this.maxConnections)
|
|
43
|
+
return;
|
|
44
|
+
let oldestKey;
|
|
45
|
+
let oldestTime = Infinity;
|
|
46
|
+
for (const [key, entry] of this.cache) {
|
|
47
|
+
if (entry.lastUsed < oldestTime) {
|
|
48
|
+
oldestTime = entry.lastUsed;
|
|
49
|
+
oldestKey = key;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (oldestKey) {
|
|
53
|
+
this.remove(oldestKey);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=connection-pool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection-pool.js","sourceRoot":"","sources":["../src/connection-pool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAe,MAAM,gBAAgB,CAAC;AAS3D,MAAM,OAAO,cAAc;IACR,KAAK,GAAG,IAAI,GAAG,EAAqB,CAAC;IACrC,WAAW,CAAS;IACpB,SAAS,CAAqB;IAC9B,MAAM,CAA0B;IAChC,cAAc,CAAS;IAExC,YAAY,MAAqB;QAC/B,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC;QACtC,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,SAAS,IAAI,SAAS,CAAC;QAC/C,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;QAC5B,IAAI,CAAC,cAAc,GAAG,MAAM,CAAC,cAAc,IAAI,EAAE,CAAC;IACpD,CAAC;IAED,KAAK,CAAC,MAAc;QAClB,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACxC,IAAI,QAAQ,EAAE,CAAC;YACb,QAAQ,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAC/B,OAAO,QAAQ,CAAC,EAAE,CAAC;QACrB,CAAC;QAED,IAAI,CAAC,aAAa,EAAE,CAAC;QAErB,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QACzD,MAAM,MAAM,GAAG,YAAY,CAAC;YAC1B,GAAG;YACH,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACzD,CAAC,CAAC;QACH,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QAEpD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QAC7D,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,CAAC,MAAc;QACnB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACrC,IAAI,KAAK,EAAE,CAAC;YACV,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;YACrB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;IACzB,CAAC;IAEO,aAAa;QACnB,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,cAAc;YAAE,OAAO;QAElD,IAAI,SAA6B,CAAC;QAClC,IAAI,UAAU,GAAG,QAAQ,CAAC;QAE1B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACtC,IAAI,KAAK,CAAC,QAAQ,GAAG,UAAU,EAAE,CAAC;gBAChC,UAAU,GAAG,KAAK,CAAC,QAAQ,CAAC;gBAC5B,SAAS,GAAG,GAAG,CAAC;YAClB,CAAC;QACH,CAAC;QAED,IAAI,SAAS,EAAE,CAAC;YACd,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACzB,CAAC;IACH,CAAC;CACF"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,YAAY,EACV,aAAa,EACb,eAAe,EACf,WAAW,EACX,UAAU,EACV,SAAS,GACV,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { TenansoInstance, DrizzleDb } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Hono environment type for tenanso middleware.
|
|
4
|
+
*
|
|
5
|
+
* Pass this as the generic to `new Hono<TenansoEnv>()` to get type-safe
|
|
6
|
+
* access to `c.var.tenant` and `c.var.db` in your route handlers.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { Hono } from "hono";
|
|
11
|
+
* import type { TenansoEnv } from "tenanso/hono";
|
|
12
|
+
*
|
|
13
|
+
* const app = new Hono<TenansoEnv>();
|
|
14
|
+
*
|
|
15
|
+
* app.get("/api/users", async (c) => {
|
|
16
|
+
* const db = c.var.db; // DrizzleDb — fully typed
|
|
17
|
+
* const tenant = c.var.tenant; // string
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export type TenansoEnv = {
|
|
22
|
+
Variables: {
|
|
23
|
+
/** Current tenant name, as resolved by {@link tenantMiddleware} */
|
|
24
|
+
tenant: string;
|
|
25
|
+
/** Drizzle db instance scoped to the current tenant */
|
|
26
|
+
db: DrizzleDb;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Options for {@link tenantMiddleware}.
|
|
31
|
+
*/
|
|
32
|
+
export interface TenantMiddlewareOptions {
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the tenant identifier from the request context.
|
|
35
|
+
*
|
|
36
|
+
* This function is called on every request. It should extract the tenant
|
|
37
|
+
* identifier from a trusted source — typically a verified JWT claim,
|
|
38
|
+
* an auth provider's organization ID, or an API key lookup.
|
|
39
|
+
*
|
|
40
|
+
* Return `undefined` to reject the request with a 400 response.
|
|
41
|
+
*
|
|
42
|
+
* **Security note:** Never trust raw client-supplied values without
|
|
43
|
+
* authentication. The tenant should come from a verified source.
|
|
44
|
+
*
|
|
45
|
+
* @param c - A subset of the Hono context with request accessors.
|
|
46
|
+
* @returns The tenant identifier, or `undefined` to reject.
|
|
47
|
+
*
|
|
48
|
+
* @example From a request header
|
|
49
|
+
* ```typescript
|
|
50
|
+
* resolve: (c) => c.req.header("x-tenant-id")
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @example From a verified JWT payload (recommended)
|
|
54
|
+
* ```typescript
|
|
55
|
+
* resolve: (c) => {
|
|
56
|
+
* const payload = c.get("jwtPayload") as { tenant: string };
|
|
57
|
+
* return payload.tenant;
|
|
58
|
+
* }
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* @example From a URL path parameter
|
|
62
|
+
* ```typescript
|
|
63
|
+
* // Route: /t/:tenantId/*
|
|
64
|
+
* resolve: (c) => c.req.param("tenantId")
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @example From a subdomain
|
|
68
|
+
* ```typescript
|
|
69
|
+
* resolve: (c) => {
|
|
70
|
+
* const url = new URL(c.req.url);
|
|
71
|
+
* const subdomain = url.hostname.split(".")[0];
|
|
72
|
+
* return subdomain === "www" ? undefined : subdomain;
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* @example Async resolution (e.g., API key lookup)
|
|
77
|
+
* ```typescript
|
|
78
|
+
* resolve: async (c) => {
|
|
79
|
+
* const apiKey = c.req.header("Authorization")?.slice(7);
|
|
80
|
+
* return apiKey ? await lookupTenantByApiKey(apiKey) : undefined;
|
|
81
|
+
* }
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
resolve: (c: {
|
|
85
|
+
req: {
|
|
86
|
+
header: (name: string) => string | undefined;
|
|
87
|
+
param: (name: string) => string | undefined;
|
|
88
|
+
url: string;
|
|
89
|
+
};
|
|
90
|
+
get: (key: string) => unknown;
|
|
91
|
+
}) => string | undefined | Promise<string | undefined>;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Hono middleware that resolves the tenant from each request and sets
|
|
95
|
+
* `c.var.db` and `c.var.tenant`.
|
|
96
|
+
*
|
|
97
|
+
* This middleware:
|
|
98
|
+
* 1. Calls `options.resolve(c)` to extract the tenant identifier from the request
|
|
99
|
+
* 2. If `undefined`, responds with `400 { error: "Tenant not specified" }`
|
|
100
|
+
* 3. Otherwise, gets a Drizzle db instance via `tenanso.dbFor(tenant)`
|
|
101
|
+
* 4. Sets `c.var.tenant` and `c.var.db` for downstream handlers
|
|
102
|
+
*
|
|
103
|
+
* Combine with Hono's `contextStorage()` middleware to enable
|
|
104
|
+
* {@link getTenantDb} and {@link getTenantName} outside of handlers.
|
|
105
|
+
*
|
|
106
|
+
* @param tenanso - The tenanso instance created by `createTenanso()`.
|
|
107
|
+
* @param options - Middleware options. See {@link TenantMiddlewareOptions}.
|
|
108
|
+
* @returns A Hono middleware function.
|
|
109
|
+
*
|
|
110
|
+
* @example Basic usage
|
|
111
|
+
* ```typescript
|
|
112
|
+
* import { Hono } from "hono";
|
|
113
|
+
* import { contextStorage } from "hono/context-storage";
|
|
114
|
+
* import { createTenanso } from "tenanso";
|
|
115
|
+
* import { tenantMiddleware, type TenansoEnv } from "tenanso/hono";
|
|
116
|
+
*
|
|
117
|
+
* const tenanso = createTenanso({ ... });
|
|
118
|
+
* const app = new Hono<TenansoEnv>();
|
|
119
|
+
*
|
|
120
|
+
* app.use(contextStorage());
|
|
121
|
+
* app.use("/api/*", tenantMiddleware(tenanso, {
|
|
122
|
+
* resolve: (c) => c.req.header("x-tenant-id"),
|
|
123
|
+
* }));
|
|
124
|
+
*
|
|
125
|
+
* app.get("/api/users", async (c) => {
|
|
126
|
+
* const users = await c.var.db.select().from(usersTable);
|
|
127
|
+
* return c.json(users);
|
|
128
|
+
* });
|
|
129
|
+
* ```
|
|
130
|
+
*
|
|
131
|
+
* @example Scoped to specific routes
|
|
132
|
+
* ```typescript
|
|
133
|
+
* const app = new Hono();
|
|
134
|
+
*
|
|
135
|
+
* // No tenant needed
|
|
136
|
+
* app.get("/health", (c) => c.json({ status: "ok" }));
|
|
137
|
+
*
|
|
138
|
+
* // Tenant-scoped routes
|
|
139
|
+
* const api = new Hono<TenansoEnv>();
|
|
140
|
+
* api.use("*", tenantMiddleware(tenanso, {
|
|
141
|
+
* resolve: (c) => c.get("jwtPayload").tenant,
|
|
142
|
+
* }));
|
|
143
|
+
* api.get("/users", async (c) => {
|
|
144
|
+
* return c.json({ tenant: c.var.tenant });
|
|
145
|
+
* });
|
|
146
|
+
* app.route("/api", api);
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
export declare function tenantMiddleware(tenanso: TenansoInstance, options: TenantMiddlewareOptions): import("hono").MiddlewareHandler<TenansoEnv, string, {}, Response>;
|
|
150
|
+
/**
|
|
151
|
+
* Get the current tenant's Drizzle db from Hono's `contextStorage()`.
|
|
152
|
+
*
|
|
153
|
+
* This allows you to access the tenant-scoped database from anywhere
|
|
154
|
+
* in the async call stack — not just inside Hono route handlers.
|
|
155
|
+
* Requires Hono's `contextStorage()` middleware to be active.
|
|
156
|
+
*
|
|
157
|
+
* @returns The Drizzle database instance for the current tenant.
|
|
158
|
+
* @throws If called outside of a request context or without `contextStorage()` middleware.
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* ```typescript
|
|
162
|
+
* import { getTenantDb } from "tenanso/hono";
|
|
163
|
+
*
|
|
164
|
+
* // Can be called from any async function during request handling
|
|
165
|
+
* async function getActiveUserCount(): Promise<number> {
|
|
166
|
+
* const db = getTenantDb();
|
|
167
|
+
* const users = await db.select().from(usersTable);
|
|
168
|
+
* return users.length;
|
|
169
|
+
* }
|
|
170
|
+
*
|
|
171
|
+
* app.get("/api/stats", async (c) => {
|
|
172
|
+
* const count = await getActiveUserCount();
|
|
173
|
+
* return c.json({ activeUsers: count });
|
|
174
|
+
* });
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
export declare function getTenantDb(): DrizzleDb;
|
|
178
|
+
/**
|
|
179
|
+
* Get the current tenant name from Hono's `contextStorage()`.
|
|
180
|
+
*
|
|
181
|
+
* This allows you to access the current tenant identifier from anywhere
|
|
182
|
+
* in the async call stack — not just inside Hono route handlers.
|
|
183
|
+
* Requires Hono's `contextStorage()` middleware to be active.
|
|
184
|
+
*
|
|
185
|
+
* @returns The current tenant name as a string.
|
|
186
|
+
* @throws If called outside of a request context or without `contextStorage()` middleware.
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```typescript
|
|
190
|
+
* import { getTenantName } from "tenanso/hono";
|
|
191
|
+
*
|
|
192
|
+
* function logAction(action: string) {
|
|
193
|
+
* console.log(`[${getTenantName()}] ${action}`);
|
|
194
|
+
* }
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
export declare function getTenantName(): string;
|
|
198
|
+
//# sourceMappingURL=hono.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hono.d.ts","sourceRoot":"","sources":["../../src/middleware/hono.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE9D;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,SAAS,EAAE;QACT,mEAAmE;QACnE,MAAM,EAAE,MAAM,CAAC;QACf,uDAAuD;QACvD,EAAE,EAAE,SAAS,CAAC;KACf,CAAC;CACH,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAkDG;IACH,OAAO,EAAE,CAAC,CAAC,EAAE;QACX,GAAG,EAAE;YACH,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;YAC7C,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;YAC5C,GAAG,EAAE,MAAM,CAAC;SACb,CAAC;QACF,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;KAC/B,KAAK,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CACxD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,uBAAuB,sEAajC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,WAAW,IAAI,SAAS,CAEvC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createMiddleware } from "hono/factory";
|
|
2
|
+
import { getContext } from "hono/context-storage";
|
|
3
|
+
/**
|
|
4
|
+
* Hono middleware that resolves the tenant from each request and sets
|
|
5
|
+
* `c.var.db` and `c.var.tenant`.
|
|
6
|
+
*
|
|
7
|
+
* This middleware:
|
|
8
|
+
* 1. Calls `options.resolve(c)` to extract the tenant identifier from the request
|
|
9
|
+
* 2. If `undefined`, responds with `400 { error: "Tenant not specified" }`
|
|
10
|
+
* 3. Otherwise, gets a Drizzle db instance via `tenanso.dbFor(tenant)`
|
|
11
|
+
* 4. Sets `c.var.tenant` and `c.var.db` for downstream handlers
|
|
12
|
+
*
|
|
13
|
+
* Combine with Hono's `contextStorage()` middleware to enable
|
|
14
|
+
* {@link getTenantDb} and {@link getTenantName} outside of handlers.
|
|
15
|
+
*
|
|
16
|
+
* @param tenanso - The tenanso instance created by `createTenanso()`.
|
|
17
|
+
* @param options - Middleware options. See {@link TenantMiddlewareOptions}.
|
|
18
|
+
* @returns A Hono middleware function.
|
|
19
|
+
*
|
|
20
|
+
* @example Basic usage
|
|
21
|
+
* ```typescript
|
|
22
|
+
* import { Hono } from "hono";
|
|
23
|
+
* import { contextStorage } from "hono/context-storage";
|
|
24
|
+
* import { createTenanso } from "tenanso";
|
|
25
|
+
* import { tenantMiddleware, type TenansoEnv } from "tenanso/hono";
|
|
26
|
+
*
|
|
27
|
+
* const tenanso = createTenanso({ ... });
|
|
28
|
+
* const app = new Hono<TenansoEnv>();
|
|
29
|
+
*
|
|
30
|
+
* app.use(contextStorage());
|
|
31
|
+
* app.use("/api/*", tenantMiddleware(tenanso, {
|
|
32
|
+
* resolve: (c) => c.req.header("x-tenant-id"),
|
|
33
|
+
* }));
|
|
34
|
+
*
|
|
35
|
+
* app.get("/api/users", async (c) => {
|
|
36
|
+
* const users = await c.var.db.select().from(usersTable);
|
|
37
|
+
* return c.json(users);
|
|
38
|
+
* });
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* @example Scoped to specific routes
|
|
42
|
+
* ```typescript
|
|
43
|
+
* const app = new Hono();
|
|
44
|
+
*
|
|
45
|
+
* // No tenant needed
|
|
46
|
+
* app.get("/health", (c) => c.json({ status: "ok" }));
|
|
47
|
+
*
|
|
48
|
+
* // Tenant-scoped routes
|
|
49
|
+
* const api = new Hono<TenansoEnv>();
|
|
50
|
+
* api.use("*", tenantMiddleware(tenanso, {
|
|
51
|
+
* resolve: (c) => c.get("jwtPayload").tenant,
|
|
52
|
+
* }));
|
|
53
|
+
* api.get("/users", async (c) => {
|
|
54
|
+
* return c.json({ tenant: c.var.tenant });
|
|
55
|
+
* });
|
|
56
|
+
* app.route("/api", api);
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export function tenantMiddleware(tenanso, options) {
|
|
60
|
+
return createMiddleware(async (c, next) => {
|
|
61
|
+
const tenantName = await options.resolve(c);
|
|
62
|
+
if (!tenantName) {
|
|
63
|
+
return c.json({ error: "Tenant not specified" }, 400);
|
|
64
|
+
}
|
|
65
|
+
const db = tenanso.dbFor(tenantName);
|
|
66
|
+
c.set("tenant", tenantName);
|
|
67
|
+
c.set("db", db);
|
|
68
|
+
await next();
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get the current tenant's Drizzle db from Hono's `contextStorage()`.
|
|
73
|
+
*
|
|
74
|
+
* This allows you to access the tenant-scoped database from anywhere
|
|
75
|
+
* in the async call stack — not just inside Hono route handlers.
|
|
76
|
+
* Requires Hono's `contextStorage()` middleware to be active.
|
|
77
|
+
*
|
|
78
|
+
* @returns The Drizzle database instance for the current tenant.
|
|
79
|
+
* @throws If called outside of a request context or without `contextStorage()` middleware.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```typescript
|
|
83
|
+
* import { getTenantDb } from "tenanso/hono";
|
|
84
|
+
*
|
|
85
|
+
* // Can be called from any async function during request handling
|
|
86
|
+
* async function getActiveUserCount(): Promise<number> {
|
|
87
|
+
* const db = getTenantDb();
|
|
88
|
+
* const users = await db.select().from(usersTable);
|
|
89
|
+
* return users.length;
|
|
90
|
+
* }
|
|
91
|
+
*
|
|
92
|
+
* app.get("/api/stats", async (c) => {
|
|
93
|
+
* const count = await getActiveUserCount();
|
|
94
|
+
* return c.json({ activeUsers: count });
|
|
95
|
+
* });
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export function getTenantDb() {
|
|
99
|
+
return getContext().var.db;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Get the current tenant name from Hono's `contextStorage()`.
|
|
103
|
+
*
|
|
104
|
+
* This allows you to access the current tenant identifier from anywhere
|
|
105
|
+
* in the async call stack — not just inside Hono route handlers.
|
|
106
|
+
* Requires Hono's `contextStorage()` middleware to be active.
|
|
107
|
+
*
|
|
108
|
+
* @returns The current tenant name as a string.
|
|
109
|
+
* @throws If called outside of a request context or without `contextStorage()` middleware.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```typescript
|
|
113
|
+
* import { getTenantName } from "tenanso/hono";
|
|
114
|
+
*
|
|
115
|
+
* function logAction(action: string) {
|
|
116
|
+
* console.log(`[${getTenantName()}] ${action}`);
|
|
117
|
+
* }
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export function getTenantName() {
|
|
121
|
+
return getContext().var.tenant;
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=hono.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hono.js","sourceRoot":"","sources":["../../src/middleware/hono.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAgGlD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,MAAM,UAAU,gBAAgB,CAC9B,OAAwB,EACxB,OAAgC;IAEhC,OAAO,gBAAgB,CAAa,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QACpD,MAAM,UAAU,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAC5C,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,sBAAsB,EAAE,EAAE,GAAG,CAAC,CAAC;QACxD,CAAC;QAED,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QACrC,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;QAC5B,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAChB,MAAM,IAAI,EAAE,CAAC;IACf,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,WAAW;IACzB,OAAO,UAAU,EAAc,CAAC,GAAG,CAAC,EAAE,CAAC;AACzC,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,UAAU,EAAc,CAAC,GAAG,CAAC,MAAM,CAAC;AAC7C,CAAC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { TenansoConfig, TenansoInstance } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Create a tenanso instance for multi-tenant database management.
|
|
4
|
+
*
|
|
5
|
+
* This is the main entry point for the library. It returns a {@link TenansoInstance}
|
|
6
|
+
* that provides tenant lifecycle management and tenant-scoped database access.
|
|
7
|
+
*
|
|
8
|
+
* Internally, it creates:
|
|
9
|
+
* - A {@link ConnectionPool} that caches Drizzle instances per tenant with LRU eviction
|
|
10
|
+
* - A Turso Platform API client for creating, deleting, and listing tenant databases
|
|
11
|
+
*
|
|
12
|
+
* @param config - Configuration options. See {@link TenansoConfig}.
|
|
13
|
+
* @returns A {@link TenansoInstance} for managing tenants and accessing their databases.
|
|
14
|
+
*
|
|
15
|
+
* @example Basic setup
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { createTenanso } from "tenanso";
|
|
18
|
+
* import * as schema from "./db/schema.js";
|
|
19
|
+
*
|
|
20
|
+
* const tenanso = createTenanso({
|
|
21
|
+
* turso: {
|
|
22
|
+
* organizationSlug: "my-org",
|
|
23
|
+
* apiToken: process.env.TURSO_API_TOKEN!,
|
|
24
|
+
* group: "my-app",
|
|
25
|
+
* },
|
|
26
|
+
* databaseUrl: "libsql://{tenant}-my-app-my-account.turso.io",
|
|
27
|
+
* authToken: process.env.TURSO_GROUP_AUTH_TOKEN!,
|
|
28
|
+
* schema,
|
|
29
|
+
* seed: { database: "seed-db" },
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @example Tenant lifecycle
|
|
34
|
+
* ```typescript
|
|
35
|
+
* // Create a tenant (cloned from seed-db)
|
|
36
|
+
* await tenanso.createTenant("acme");
|
|
37
|
+
*
|
|
38
|
+
* // Query tenant's database
|
|
39
|
+
* await tenanso.withTenant("acme", async (db) => {
|
|
40
|
+
* const users = await db.select().from(usersTable);
|
|
41
|
+
* });
|
|
42
|
+
*
|
|
43
|
+
* // Or get the db directly
|
|
44
|
+
* const db = tenanso.dbFor("acme");
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @example With Hono middleware
|
|
48
|
+
* ```typescript
|
|
49
|
+
* import { tenantMiddleware, type TenansoEnv } from "tenanso/hono";
|
|
50
|
+
*
|
|
51
|
+
* const app = new Hono<TenansoEnv>();
|
|
52
|
+
* app.use("/api/*", tenantMiddleware(tenanso, {
|
|
53
|
+
* resolve: (c) => c.get("jwtPayload").tenant,
|
|
54
|
+
* }));
|
|
55
|
+
*
|
|
56
|
+
* app.get("/api/users", async (c) => {
|
|
57
|
+
* const users = await c.var.db.select().from(usersTable);
|
|
58
|
+
* return c.json(users);
|
|
59
|
+
* });
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export declare function createTenanso(config: TenansoConfig): TenansoInstance;
|
|
63
|
+
//# sourceMappingURL=tenanso.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenanso.d.ts","sourceRoot":"","sources":["../src/tenanso.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAa,MAAM,YAAY,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,eAAe,CAwCpE"}
|