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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +263 -0
  3. package/dist/__tests__/connection-pool.test.d.ts +2 -0
  4. package/dist/__tests__/connection-pool.test.d.ts.map +1 -0
  5. package/dist/__tests__/connection-pool.test.js +88 -0
  6. package/dist/__tests__/connection-pool.test.js.map +1 -0
  7. package/dist/__tests__/hono-middleware.test.d.ts +2 -0
  8. package/dist/__tests__/hono-middleware.test.d.ts.map +1 -0
  9. package/dist/__tests__/hono-middleware.test.js +121 -0
  10. package/dist/__tests__/hono-middleware.test.js.map +1 -0
  11. package/dist/__tests__/tenanso.test.d.ts +2 -0
  12. package/dist/__tests__/tenanso.test.d.ts.map +1 -0
  13. package/dist/__tests__/tenanso.test.js +105 -0
  14. package/dist/__tests__/tenanso.test.js.map +1 -0
  15. package/dist/__tests__/turso-api.test.d.ts +2 -0
  16. package/dist/__tests__/turso-api.test.d.ts.map +1 -0
  17. package/dist/__tests__/turso-api.test.js +103 -0
  18. package/dist/__tests__/turso-api.test.js.map +1 -0
  19. package/dist/connection-pool.d.ts +14 -0
  20. package/dist/connection-pool.d.ts.map +1 -0
  21. package/dist/connection-pool.js +57 -0
  22. package/dist/connection-pool.js.map +1 -0
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +2 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/middleware/hono.d.ts +198 -0
  28. package/dist/middleware/hono.d.ts.map +1 -0
  29. package/dist/middleware/hono.js +123 -0
  30. package/dist/middleware/hono.js.map +1 -0
  31. package/dist/tenanso.d.ts +63 -0
  32. package/dist/tenanso.d.ts.map +1 -0
  33. package/dist/tenanso.js +92 -0
  34. package/dist/tenanso.js.map +1 -0
  35. package/dist/turso-api.d.ts +13 -0
  36. package/dist/turso-api.d.ts.map +1 -0
  37. package/dist/turso-api.js +83 -0
  38. package/dist/turso-api.js.map +1 -0
  39. package/dist/types.d.ts +349 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/types.js +2 -0
  42. package/dist/types.js.map +1 -0
  43. package/package.json +72 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 yoshixi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,263 @@
1
+ # tenanso
2
+
3
+ Multi-tenant SQLite for TypeScript — database-per-tenant isolation using [Drizzle ORM](https://orm.drizzle.team/) and [Turso](https://turso.tech/).
4
+
5
+ Each tenant gets their own SQLite database managed by Turso. Your application code stays tenant-unaware — tenanso handles connection routing, tenant lifecycle, and framework integration.
6
+
7
+ Inspired by Rails 8's [activerecord-tenanted](https://github.com/basecamp/activerecord-tenanted).
8
+
9
+ ## Features
10
+
11
+ - **Database-per-tenant isolation** — each tenant's data is physically separated
12
+ - **Runtime-agnostic** — works on Cloudflare Workers, Deno, Bun, and Node.js (no `node:` imports)
13
+ - **Turso Platform API integration** — create and delete tenant databases dynamically
14
+ - **Connection pooling** — LRU cache of Drizzle instances with configurable max connections
15
+ - **Hono middleware** — optional first-class integration with [Hono](https://hono.dev/)
16
+ - **Type-safe** — full TypeScript support with Drizzle's type inference
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pnpm add tenanso drizzle-orm @libsql/client
22
+
23
+ # If using Hono middleware
24
+ pnpm add hono
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ### 1. Define your Drizzle schema
30
+
31
+ ```typescript
32
+ // db/schema.ts
33
+ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
34
+
35
+ export const users = sqliteTable("users", {
36
+ id: integer("id").primaryKey(),
37
+ name: text("name").notNull(),
38
+ email: text("email").notNull(),
39
+ });
40
+ ```
41
+
42
+ ### 2. Create a tenanso instance
43
+
44
+ ```typescript
45
+ import { createTenanso } from "tenanso";
46
+ import * as schema from "./db/schema.js";
47
+
48
+ const tenanso = createTenanso({
49
+ turso: {
50
+ organizationSlug: "my-org",
51
+ apiToken: process.env.TURSO_API_TOKEN!,
52
+ group: "my-app",
53
+ },
54
+ databaseUrl: "libsql://{tenant}-my-app-my-account.turso.io",
55
+ authToken: process.env.TURSO_GROUP_AUTH_TOKEN!,
56
+ schema,
57
+ // New tenant databases are cloned from the seed database
58
+ seed: { database: "seed-db" },
59
+ });
60
+ ```
61
+
62
+ ### 3. Use it
63
+
64
+ ```typescript
65
+ // Create a tenant database
66
+ await tenanso.createTenant("acme-corp");
67
+
68
+ // Query a tenant's database
69
+ await tenanso.withTenant("acme-corp", async (db) => {
70
+ await db.insert(users).values({ name: "Alice", email: "alice@acme.com" });
71
+ const allUsers = await db.select().from(users);
72
+ });
73
+
74
+ // Or get a db instance directly
75
+ const db = tenanso.dbFor("acme-corp");
76
+ ```
77
+
78
+ ## Hono Integration
79
+
80
+ tenanso provides an optional Hono middleware that sets `c.var.db` and `c.var.tenant` for each request.
81
+
82
+ ```typescript
83
+ import { Hono } from "hono";
84
+ import { contextStorage } from "hono/context-storage";
85
+ import { createTenanso } from "tenanso";
86
+ import { tenantMiddleware, type TenansoEnv } from "tenanso/hono";
87
+
88
+ const tenanso = createTenanso({ /* ... */ });
89
+ const app = new Hono<TenansoEnv>();
90
+
91
+ app.use(contextStorage());
92
+ app.use("/api/*", tenantMiddleware(tenanso, {
93
+ resolve: (c) => c.req.header("x-tenant-id"),
94
+ }));
95
+
96
+ app.get("/api/users", async (c) => {
97
+ const db = c.var.db; // DrizzleDb — fully typed
98
+ const tenant = c.var.tenant; // string
99
+ const users = await db.select().from(usersTable);
100
+ return c.json(users);
101
+ });
102
+ ```
103
+
104
+ ### Accessing the db outside handlers
105
+
106
+ With Hono's `contextStorage()` middleware enabled, you can access the tenant db from anywhere in the async call stack:
107
+
108
+ ```typescript
109
+ import { getTenantDb, getTenantName } from "tenanso/hono";
110
+
111
+ async function getActiveUserCount(): Promise<number> {
112
+ const db = getTenantDb();
113
+ const result = await db.select().from(users);
114
+ return result.length;
115
+ }
116
+ ```
117
+
118
+ ### Tenant resolution strategies
119
+
120
+ The `resolve` function determines which tenant a request belongs to. Here are common patterns:
121
+
122
+ ```typescript
123
+ // From header
124
+ resolve: (c) => c.req.header("x-tenant-id")
125
+
126
+ // From URL path parameter (/t/:tenantId/*)
127
+ resolve: (c) => c.req.param("tenantId")
128
+
129
+ // From subdomain (acme.myapp.com → "acme")
130
+ resolve: (c) => {
131
+ const url = new URL(c.req.url);
132
+ return url.hostname.split(".")[0];
133
+ }
134
+
135
+ // From a verified JWT claim
136
+ resolve: (c) => {
137
+ const payload = c.get("jwtPayload");
138
+ return payload.tenant;
139
+ }
140
+ ```
141
+
142
+ ## Authentication
143
+
144
+ tenanso handles tenant resolution, not authentication. Auth is your application's responsibility, but how you wire them together matters for security.
145
+
146
+ **The tenant must come from a verified source.** Never trust a raw client header without authentication.
147
+
148
+ ### Recommended: JWT with tenant claim
149
+
150
+ ```typescript
151
+ import { jwt } from "hono/jwt";
152
+
153
+ // 1. Verify JWT first
154
+ app.use("/api/*", jwt({ secret: "your-secret", alg: "HS256" }));
155
+
156
+ // 2. Resolve tenant from the verified payload
157
+ app.use("/api/*", tenantMiddleware(tenanso, {
158
+ resolve: (c) => c.get("jwtPayload").tenant,
159
+ }));
160
+ ```
161
+
162
+ ### External auth provider (Clerk, Auth0)
163
+
164
+ ```typescript
165
+ app.use("/api/*", clerkMiddleware());
166
+ app.use("/api/*", tenantMiddleware(tenanso, {
167
+ resolve: (c) => c.get("clerkAuth").tenantSlug,
168
+ }));
169
+ ```
170
+
171
+ ### API key
172
+
173
+ ```typescript
174
+ app.use("/api/*", async (c, next) => {
175
+ const key = c.req.header("Authorization")?.slice(7);
176
+ const tenant = await lookupTenantByApiKey(key);
177
+ if (!tenant) return c.json({ error: "Invalid API key" }, 401);
178
+ c.set("resolvedTenant", tenant);
179
+ await next();
180
+ });
181
+
182
+ app.use("/api/*", tenantMiddleware(tenanso, {
183
+ resolve: (c) => c.get("resolvedTenant"),
184
+ }));
185
+ ```
186
+
187
+ ## API Reference
188
+
189
+ ### `createTenanso(config)`
190
+
191
+ Creates a tenanso instance.
192
+
193
+ ```typescript
194
+ const tenanso = createTenanso({
195
+ turso: {
196
+ organizationSlug: string; // Turso org slug
197
+ apiToken: string; // Turso Platform API token
198
+ group: string; // Database group (e.g. "my-app")
199
+ },
200
+ databaseUrl: string; // URL template: "libsql://{tenant}-my-app-my-account.turso.io"
201
+ authToken: string; // Turso group auth token
202
+ schema: Record<string, unknown>; // Drizzle schema
203
+ seed?: { database: string }; // Clone new tenants from this database
204
+ maxConnections?: number; // Max cached connections (default: 50)
205
+ });
206
+ ```
207
+
208
+ ### `TenansoInstance`
209
+
210
+ | Method | Description |
211
+ |---|---|
212
+ | `dbFor(tenant)` | Returns a cached `DrizzleDb` instance for the tenant |
213
+ | `withTenant(tenant, fn)` | Runs a callback with the tenant's `DrizzleDb` |
214
+ | `createTenant(name)` | Creates a new database via Turso Platform API |
215
+ | `deleteTenant(name)` | Deletes a database via Turso Platform API |
216
+ | `listTenants()` | Lists all databases in the organization |
217
+ | `tenantExists(name)` | Checks if a tenant database exists |
218
+
219
+ ### `tenantMiddleware(tenanso, options)` (from `tenanso/hono`)
220
+
221
+ Hono middleware that resolves the tenant from the request and sets `c.var.db` and `c.var.tenant`.
222
+
223
+ Returns `400` if `resolve` returns `undefined`.
224
+
225
+ ### `getTenantDb()` / `getTenantName()` (from `tenanso/hono`)
226
+
227
+ Access the current tenant's db or name from outside Hono handlers. Requires Hono's `contextStorage()` middleware.
228
+
229
+ ## Turso Setup
230
+
231
+ ### Create a group
232
+
233
+ Use a [group](https://docs.turso.tech/features/groups) per application to organize databases:
234
+
235
+ ```bash
236
+ turso group create my-app --location nrt
237
+ turso group tokens create my-app # save as TURSO_GROUP_AUTH_TOKEN
238
+ ```
239
+
240
+ ### Create a seed database
241
+
242
+ New tenant databases are cloned from a seed database that has your schema already applied:
243
+
244
+ ```bash
245
+ turso db create seed-db --group my-app
246
+ npx drizzle-kit push --url libsql://seed-db-my-app-my-account.turso.io --auth-token $TURSO_GROUP_AUTH_TOKEN
247
+ ```
248
+
249
+ See the [Turso Setup guide](/guide/turso-setup) for more details.
250
+
251
+ ## Design Decisions
252
+
253
+ | Decision | Rationale |
254
+ |---|---|
255
+ | No Node.js runtime dependency | Core uses `fetch` and `Map` only. Works on Cloudflare Workers, Deno, Bun. |
256
+ | No built-in async context | Core passes `db` explicitly. Framework adapters (Hono's `contextStorage()`) handle implicit context. |
257
+ | Hono as optional peer dependency | `import "tenanso"` has zero hono imports. Only `import "tenanso/hono"` requires it. |
258
+ | LRU connection pool | Caps memory/file descriptor usage. Configurable via `maxConnections` (default 50). |
259
+ | Auth is out of scope | tenanso resolves tenants, not users. Compose with your auth stack via the `resolve` function. |
260
+
261
+ ## License
262
+
263
+ MIT
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=connection-pool.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection-pool.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/connection-pool.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ // Mock @libsql/client before importing ConnectionPool
3
+ vi.mock("@libsql/client", () => ({
4
+ createClient: vi.fn((opts) => ({
5
+ url: opts.url,
6
+ authToken: opts.authToken,
7
+ close: vi.fn(),
8
+ })),
9
+ }));
10
+ vi.mock("drizzle-orm/libsql", () => ({
11
+ drizzle: vi.fn((client, opts) => ({
12
+ _client: client,
13
+ _opts: opts,
14
+ _isDrizzle: true,
15
+ })),
16
+ }));
17
+ import { ConnectionPool } from "../connection-pool.js";
18
+ function makeConfig(overrides) {
19
+ return {
20
+ turso: {
21
+ organizationSlug: "test-org",
22
+ apiToken: "test-token",
23
+ group: "default",
24
+ },
25
+ databaseUrl: "libsql://{tenant}-test-org.turso.io",
26
+ authToken: "group-token",
27
+ schema: {},
28
+ ...overrides,
29
+ };
30
+ }
31
+ describe("ConnectionPool", () => {
32
+ beforeEach(() => {
33
+ vi.clearAllMocks();
34
+ });
35
+ it("creates a db instance for a tenant", () => {
36
+ const pool = new ConnectionPool(makeConfig());
37
+ const db = pool.getDb("acme");
38
+ expect(db._isDrizzle).toBe(true);
39
+ expect(db._client.url).toBe("libsql://acme-test-org.turso.io");
40
+ expect(db._client.authToken).toBe("group-token");
41
+ });
42
+ it("returns cached instance on second call", () => {
43
+ const pool = new ConnectionPool(makeConfig());
44
+ const db1 = pool.getDb("acme");
45
+ const db2 = pool.getDb("acme");
46
+ expect(db1).toBe(db2);
47
+ });
48
+ it("creates separate instances for different tenants", () => {
49
+ const pool = new ConnectionPool(makeConfig());
50
+ const db1 = pool.getDb("acme");
51
+ const db2 = pool.getDb("other");
52
+ expect(db1).not.toBe(db2);
53
+ });
54
+ it("removes a tenant from the cache", () => {
55
+ const pool = new ConnectionPool(makeConfig());
56
+ const db1 = pool.getDb("acme");
57
+ pool.remove("acme");
58
+ const db2 = pool.getDb("acme");
59
+ expect(db1).not.toBe(db2);
60
+ });
61
+ it("tracks pool size", () => {
62
+ const pool = new ConnectionPool(makeConfig());
63
+ expect(pool.size).toBe(0);
64
+ pool.getDb("a");
65
+ expect(pool.size).toBe(1);
66
+ pool.getDb("b");
67
+ expect(pool.size).toBe(2);
68
+ pool.remove("a");
69
+ expect(pool.size).toBe(1);
70
+ });
71
+ it("evicts LRU entry when max is reached", () => {
72
+ const pool = new ConnectionPool(makeConfig({ maxConnections: 2 }));
73
+ pool.getDb("a");
74
+ pool.getDb("b");
75
+ expect(pool.size).toBe(2);
76
+ // Access "a" again to make "b" the LRU
77
+ pool.getDb("a");
78
+ // Adding "c" should evict "b"
79
+ pool.getDb("c");
80
+ expect(pool.size).toBe(2);
81
+ // "a" should still be cached, "b" should be evicted (new instance)
82
+ const dbA = pool.getDb("a");
83
+ const dbB = pool.getDb("b");
84
+ expect(dbA).toBeDefined();
85
+ expect(dbB).toBeDefined();
86
+ });
87
+ });
88
+ //# sourceMappingURL=connection-pool.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection-pool.test.js","sourceRoot":"","sources":["../../src/__tests__/connection-pool.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=hono-middleware.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hono-middleware.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/hono-middleware.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,121 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { Hono } from "hono";
3
+ import { tenantMiddleware } from "../middleware/hono.js";
4
+ function createMockTenanso() {
5
+ const dbs = new Map();
6
+ return {
7
+ dbFor(tenant) {
8
+ let db = dbs.get(tenant);
9
+ if (!db) {
10
+ db = { _tenant: tenant, _isDrizzle: true };
11
+ dbs.set(tenant, db);
12
+ }
13
+ return db;
14
+ },
15
+ async withTenant(tenant, fn) {
16
+ return fn(this.dbFor(tenant));
17
+ },
18
+ async createTenant() { },
19
+ async deleteTenant() { },
20
+ async listTenants() {
21
+ return [];
22
+ },
23
+ async tenantExists() {
24
+ return false;
25
+ },
26
+ };
27
+ }
28
+ describe("tenantMiddleware", () => {
29
+ let tenanso;
30
+ beforeEach(() => {
31
+ tenanso = createMockTenanso();
32
+ });
33
+ it("sets tenant and db on context from header", async () => {
34
+ const app = new Hono();
35
+ app.use("*", tenantMiddleware(tenanso, {
36
+ resolve: (c) => c.req.header("x-tenant-id"),
37
+ }));
38
+ app.get("/test", (c) => {
39
+ return c.json({
40
+ tenant: c.var.tenant,
41
+ hasDb: c.var.db !== undefined,
42
+ });
43
+ });
44
+ const res = await app.request("/test", {
45
+ headers: { "x-tenant-id": "acme" },
46
+ });
47
+ expect(res.status).toBe(200);
48
+ const body = await res.json();
49
+ expect(body).toEqual({ tenant: "acme", hasDb: true });
50
+ });
51
+ it("returns 400 when tenant is not specified", async () => {
52
+ const app = new Hono();
53
+ app.use("*", tenantMiddleware(tenanso, {
54
+ resolve: (c) => c.req.header("x-tenant-id"),
55
+ }));
56
+ app.get("/test", (c) => c.text("ok"));
57
+ const res = await app.request("/test");
58
+ expect(res.status).toBe(400);
59
+ const body = await res.json();
60
+ expect(body).toEqual({ error: "Tenant not specified" });
61
+ });
62
+ it("supports async resolve", async () => {
63
+ const app = new Hono();
64
+ app.use("*", tenantMiddleware(tenanso, {
65
+ resolve: async (c) => {
66
+ // Simulate async lookup
67
+ return c.req.header("x-tenant-id");
68
+ },
69
+ }));
70
+ app.get("/test", (c) => c.json({ tenant: c.var.tenant }));
71
+ const res = await app.request("/test", {
72
+ headers: { "x-tenant-id": "async-tenant" },
73
+ });
74
+ expect(res.status).toBe(200);
75
+ const body = await res.json();
76
+ expect(body).toEqual({ tenant: "async-tenant" });
77
+ });
78
+ it("scopes middleware to specific routes", async () => {
79
+ const app = new Hono();
80
+ // Health check — no tenant
81
+ app.get("/health", (c) => c.json({ status: "ok" }));
82
+ // Tenant-scoped routes
83
+ const api = new Hono();
84
+ api.use("*", tenantMiddleware(tenanso, {
85
+ resolve: (c) => c.req.header("x-tenant-id"),
86
+ }));
87
+ api.get("/users", (c) => c.json({ tenant: c.var.tenant }));
88
+ app.route("/api", api);
89
+ // Health check works without tenant
90
+ const healthRes = await app.request("/health");
91
+ expect(healthRes.status).toBe(200);
92
+ // API requires tenant
93
+ const apiRes = await app.request("/api/users");
94
+ expect(apiRes.status).toBe(400);
95
+ // API works with tenant
96
+ const apiResOk = await app.request("/api/users", {
97
+ headers: { "x-tenant-id": "acme" },
98
+ });
99
+ expect(apiResOk.status).toBe(200);
100
+ expect(await apiResOk.json()).toEqual({ tenant: "acme" });
101
+ });
102
+ it("resolves tenant from JWT payload via c.get", async () => {
103
+ const app = new Hono();
104
+ // Simulate JWT middleware
105
+ app.use("*", async (c, next) => {
106
+ c.set("jwtPayload", { tenant: "jwt-tenant" });
107
+ await next();
108
+ });
109
+ app.use("*", tenantMiddleware(tenanso, {
110
+ resolve: (c) => {
111
+ const payload = c.get("jwtPayload");
112
+ return payload?.tenant;
113
+ },
114
+ }));
115
+ app.get("/test", (c) => c.json({ tenant: c.var.tenant }));
116
+ const res = await app.request("/test");
117
+ expect(res.status).toBe(200);
118
+ expect(await res.json()).toEqual({ tenant: "jwt-tenant" });
119
+ });
120
+ });
121
+ //# sourceMappingURL=hono-middleware.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hono-middleware.test.js","sourceRoot":"","sources":["../../src/__tests__/hono-middleware.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=tenanso.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenanso.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/tenanso.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,105 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ vi.mock("@libsql/client", () => ({
3
+ createClient: vi.fn((opts) => ({
4
+ url: opts.url,
5
+ authToken: opts.authToken,
6
+ close: vi.fn(),
7
+ })),
8
+ }));
9
+ vi.mock("drizzle-orm/libsql", () => ({
10
+ drizzle: vi.fn((client, opts) => ({
11
+ _client: client,
12
+ _opts: opts,
13
+ _isDrizzle: true,
14
+ })),
15
+ }));
16
+ import { createTenanso } from "../tenanso.js";
17
+ const config = {
18
+ turso: {
19
+ organizationSlug: "test-org",
20
+ apiToken: "test-token",
21
+ group: "default",
22
+ },
23
+ databaseUrl: "libsql://{tenant}-test-org.turso.io",
24
+ authToken: "group-token",
25
+ schema: {},
26
+ };
27
+ describe("createTenanso", () => {
28
+ beforeEach(() => {
29
+ vi.restoreAllMocks();
30
+ });
31
+ it("throws if databaseUrl is missing {tenant} placeholder", () => {
32
+ expect(() => createTenanso({
33
+ ...config,
34
+ databaseUrl: "libsql://no-placeholder.turso.io",
35
+ })).toThrow("databaseUrl must contain a {tenant} placeholder");
36
+ });
37
+ describe("dbFor", () => {
38
+ it("returns a drizzle instance for the tenant", () => {
39
+ const tenanso = createTenanso(config);
40
+ const db = tenanso.dbFor("acme");
41
+ expect(db._isDrizzle).toBe(true);
42
+ expect(db._client.url).toBe("libsql://acme-test-org.turso.io");
43
+ });
44
+ it("returns the same instance for the same tenant", () => {
45
+ const tenanso = createTenanso(config);
46
+ expect(tenanso.dbFor("acme")).toBe(tenanso.dbFor("acme"));
47
+ });
48
+ });
49
+ describe("withTenant", () => {
50
+ it("passes the db to the callback and returns its result", async () => {
51
+ const tenanso = createTenanso(config);
52
+ const result = await tenanso.withTenant("acme", async (db) => {
53
+ expect(db._isDrizzle).toBe(true);
54
+ return "hello from acme";
55
+ });
56
+ expect(result).toBe("hello from acme");
57
+ });
58
+ });
59
+ describe("createTenant", () => {
60
+ it("calls Turso Platform API", async () => {
61
+ const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("{}", { status: 200 }));
62
+ const tenanso = createTenanso(config);
63
+ await tenanso.createTenant("new-tenant");
64
+ expect(fetchSpy).toHaveBeenCalledOnce();
65
+ const body = JSON.parse(fetchSpy.mock.calls[0][1]?.body);
66
+ expect(body.name).toBe("new-tenant");
67
+ });
68
+ });
69
+ describe("deleteTenant", () => {
70
+ it("calls Turso API and removes from connection pool", async () => {
71
+ const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("{}", { status: 200 }));
72
+ const tenanso = createTenanso(config);
73
+ // Access db to populate pool
74
+ tenanso.dbFor("old-tenant");
75
+ await tenanso.deleteTenant("old-tenant");
76
+ expect(fetchSpy).toHaveBeenCalledOnce();
77
+ // Should get a new instance (cache was cleared)
78
+ const db = tenanso.dbFor("old-tenant");
79
+ expect(db).toBeDefined();
80
+ });
81
+ });
82
+ describe("listTenants", () => {
83
+ it("returns tenant names from Turso API", async () => {
84
+ vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify({
85
+ databases: [{ Name: "a" }, { Name: "b" }],
86
+ }), { status: 200 }));
87
+ const tenanso = createTenanso(config);
88
+ const tenants = await tenanso.listTenants();
89
+ expect(tenants).toEqual(["a", "b"]);
90
+ });
91
+ });
92
+ describe("tenantExists", () => {
93
+ it("returns true for existing tenant", async () => {
94
+ vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify({ database: { Name: "acme" } }), { status: 200 }));
95
+ const tenanso = createTenanso(config);
96
+ expect(await tenanso.tenantExists("acme")).toBe(true);
97
+ });
98
+ it("returns false for non-existing tenant", async () => {
99
+ vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("not found", { status: 404 }));
100
+ const tenanso = createTenanso(config);
101
+ expect(await tenanso.tenantExists("nope")).toBe(false);
102
+ });
103
+ });
104
+ });
105
+ //# sourceMappingURL=tenanso.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenanso.test.js","sourceRoot":"","sources":["../../src/__tests__/tenanso.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=turso-api.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"turso-api.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/turso-api.test.ts"],"names":[],"mappings":""}