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
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 @@
|
|
|
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 @@
|
|
|
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":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAC9D,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAAmB,MAAM,uBAAuB,CAAC;AAG1E,SAAS,iBAAiB;IACxB,MAAM,GAAG,GAAG,IAAI,GAAG,EAAqB,CAAC;IAEzC,OAAO;QACL,KAAK,CAAC,MAAc;YAClB,IAAI,EAAE,GAAG,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACzB,IAAI,CAAC,EAAE,EAAE,CAAC;gBACR,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAA0B,CAAC;gBACnE,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;YACtB,CAAC;YACD,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,KAAK,CAAC,UAAU,CACd,MAAc,EACd,EAAiC;YAEjC,OAAO,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;QAChC,CAAC;QACD,KAAK,CAAC,YAAY,KAAI,CAAC;QACvB,KAAK,CAAC,YAAY,KAAI,CAAC;QACvB,KAAK,CAAC,WAAW;YACf,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,KAAK,CAAC,YAAY;YAChB,OAAO,KAAK,CAAC;QACf,CAAC;KACF,CAAC;AACJ,CAAC;AAED,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,IAAI,OAAwB,CAAC;IAE7B,UAAU,CAAC,GAAG,EAAE;QACd,OAAO,GAAG,iBAAiB,EAAE,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,KAAK,IAAI,EAAE;QACzD,MAAM,GAAG,GAAG,IAAI,IAAI,EAAc,CAAC;QAEnC,GAAG,CAAC,GAAG,CACL,GAAG,EACH,gBAAgB,CAAC,OAAO,EAAE;YACxB,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC;SAC5C,CAAC,CACH,CAAC;QAEF,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE;YACrB,OAAO,CAAC,CAAC,IAAI,CAAC;gBACZ,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,MAAM;gBACpB,KAAK,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,KAAK,SAAS;aAC9B,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE;YACrC,OAAO,EAAE,EAAE,aAAa,EAAE,MAAM,EAAE;SACnC,CAAC,CAAC;QAEH,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC7B,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QAC9B,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,KAAK,IAAI,EAAE;QACxD,MAAM,GAAG,GAAG,IAAI,IAAI,EAAc,CAAC;QAEnC,GAAG,CAAC,GAAG,CACL,GAAG,EACH,gBAAgB,CAAC,OAAO,EAAE;YACxB,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC;SAC5C,CAAC,CACH,CAAC;QAEF,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAEtC,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACvC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAE7B,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QAC9B,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,sBAAsB,EAAE,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wBAAwB,EAAE,KAAK,IAAI,EAAE;QACtC,MAAM,GAAG,GAAG,IAAI,IAAI,EAAc,CAAC;QAEnC,GAAG,CAAC,GAAG,CACL,GAAG,EACH,gBAAgB,CAAC,OAAO,EAAE;YACxB,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;gBACnB,wBAAwB;gBACxB,OAAO,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;YACrC,CAAC;SACF,CAAC,CACH,CAAC;QAEF,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAE1D,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE;YACrC,OAAO,EAAE,EAAE,aAAa,EAAE,cAAc,EAAE;SAC3C,CAAC,CAAC;QAEH,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC7B,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QAC9B,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,KAAK,IAAI,EAAE;QACpD,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;QAEvB,2BAA2B;QAC3B,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QAEpD,uBAAuB;QACvB,MAAM,GAAG,GAAG,IAAI,IAAI,EAAc,CAAC;QACnC,GAAG,CAAC,GAAG,CACL,GAAG,EACH,gBAAgB,CAAC,OAAO,EAAE;YACxB,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC;SAC5C,CAAC,CACH,CAAC;QACF,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAE3D,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAEvB,oCAAoC;QACpC,MAAM,SAAS,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAC/C,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAEnC,sBAAsB;QACtB,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;QAC/C,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAEhC,wBAAwB;QACxB,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,YAAY,EAAE;YAC/C,OAAO,EAAE,EAAE,aAAa,EAAE,MAAM,EAAE;SACnC,CAAC,CAAC;QACH,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAClC,MAAM,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4CAA4C,EAAE,KAAK,IAAI,EAAE;QAS1D,MAAM,GAAG,GAAG,IAAI,IAAI,EAAU,CAAC;QAE/B,0BAA0B;QAC1B,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;YAC7B,CAAC,CAAC,GAAG,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;YAC9C,MAAM,IAAI,EAAE,CAAC;QACf,CAAC,CAAC,CAAC;QAEH,GAAG,CAAC,GAAG,CACL,GAAG,EACH,gBAAgB,CAAC,OAAO,EAAE;YACxB,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE;gBACb,MAAM,OAAO,GAAG,CAAC,CAAC,GAAG,CAAC,YAAY,CAErB,CAAC;gBACd,OAAO,OAAO,EAAE,MAAM,CAAC;YACzB,CAAC;SACF,CAAC,CACH,CAAC;QAEF,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAE1D,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACvC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC7B,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -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":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAE9D,EAAE,CAAC,IAAI,CAAC,gBAAgB,EAAE,GAAG,EAAE,CAAC,CAAC;IAC/B,YAAY,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,IAAwC,EAAE,EAAE,CAAC,CAAC;QACjE,GAAG,EAAE,IAAI,CAAC,GAAG;QACb,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,KAAK,EAAE,EAAE,CAAC,EAAE,EAAE;KACf,CAAC,CAAC;CACJ,CAAC,CAAC,CAAC;AAEJ,EAAE,CAAC,IAAI,CAAC,oBAAoB,EAAE,GAAG,EAAE,CAAC,CAAC;IACnC,OAAO,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,MAAe,EAAE,IAAa,EAAE,EAAE,CAAC,CAAC;QAClD,OAAO,EAAE,MAAM;QACf,KAAK,EAAE,IAAI;QACX,UAAU,EAAE,IAAI;KACjB,CAAC,CAAC;CACJ,CAAC,CAAC,CAAC;AAEJ,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAG9C,MAAM,MAAM,GAAkB;IAC5B,KAAK,EAAE;QACL,gBAAgB,EAAE,UAAU;QAC5B,QAAQ,EAAE,YAAY;QACtB,KAAK,EAAE,SAAS;KACjB;IACD,WAAW,EAAE,qCAAqC;IAClD,SAAS,EAAE,aAAa;IACxB,MAAM,EAAE,EAAE;CACX,CAAC;AAEF,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,UAAU,CAAC,GAAG,EAAE;QACd,EAAE,CAAC,eAAe,EAAE,CAAC;IACvB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,MAAM,CAAC,GAAG,EAAE,CACV,aAAa,CAAC;YACZ,GAAG,MAAM;YACT,WAAW,EAAE,kCAAkC;SAChD,CAAC,CACH,CAAC,OAAO,CAAC,iDAAiD,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,OAAO,EAAE,GAAG,EAAE;QACrB,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;YACnD,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;YACtC,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAQ,CAAC;YAExC,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACjC,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,iCAAiC,CAAC,CAAC;QACjE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;YACvD,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;YACtC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;QAC5D,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;QAC1B,EAAE,CAAC,sDAAsD,EAAE,KAAK,IAAI,EAAE;YACpE,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;YAEtC,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE;gBAC3D,MAAM,CAAE,EAAU,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC1C,OAAO,iBAAiB,CAAC;YAC3B,CAAC,CAAC,CAAC;YAEH,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;QACzC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;QAC5B,EAAE,CAAC,0BAA0B,EAAE,KAAK,IAAI,EAAE;YACxC,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,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;YACtC,MAAM,OAAO,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;YAEzC,MAAM,CAAC,QAAQ,CAAC,CAAC,oBAAoB,EAAE,CAAC;YACxC,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,IAAI,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QACvC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;QAC5B,EAAE,CAAC,kDAAkD,EAAE,KAAK,IAAI,EAAE;YAChE,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,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;YAEtC,6BAA6B;YAC7B,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;YAE5B,MAAM,OAAO,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;YAEzC,MAAM,CAAC,QAAQ,CAAC,CAAC,oBAAoB,EAAE,CAAC;YAExC,gDAAgD;YAChD,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;YACvC,MAAM,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;QAC3B,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;QAC3B,EAAE,CAAC,qCAAqC,EAAE,KAAK,IAAI,EAAE;YACnD,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,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC;aAC1C,CAAC,EACF,EAAE,MAAM,EAAE,GAAG,EAAE,CAChB,CACF,CAAC;YAEF,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;YACtC,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC;YAC5C,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;QACtC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;QAC5B,EAAE,CAAC,kCAAkC,EAAE,KAAK,IAAI,EAAE;YAChD,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,iBAAiB,CAC7C,IAAI,QAAQ,CACV,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,EAC9C,EAAE,MAAM,EAAE,GAAG,EAAE,CAChB,CACF,CAAC;YAEF,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;YACtC,MAAM,CAAC,MAAM,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,uCAAuC,EAAE,KAAK,IAAI,EAAE;YACrD,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,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;YACtC,MAAM,CAAC,MAAM,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turso-api.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/turso-api.test.ts"],"names":[],"mappings":""}
|