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
@@ -0,0 +1,92 @@
1
+ import { ConnectionPool } from "./connection-pool.js";
2
+ import { TursoApi } from "./turso-api.js";
3
+ /**
4
+ * Create a tenanso instance for multi-tenant database management.
5
+ *
6
+ * This is the main entry point for the library. It returns a {@link TenansoInstance}
7
+ * that provides tenant lifecycle management and tenant-scoped database access.
8
+ *
9
+ * Internally, it creates:
10
+ * - A {@link ConnectionPool} that caches Drizzle instances per tenant with LRU eviction
11
+ * - A Turso Platform API client for creating, deleting, and listing tenant databases
12
+ *
13
+ * @param config - Configuration options. See {@link TenansoConfig}.
14
+ * @returns A {@link TenansoInstance} for managing tenants and accessing their databases.
15
+ *
16
+ * @example Basic setup
17
+ * ```typescript
18
+ * import { createTenanso } from "tenanso";
19
+ * import * as schema from "./db/schema.js";
20
+ *
21
+ * const tenanso = createTenanso({
22
+ * turso: {
23
+ * organizationSlug: "my-org",
24
+ * apiToken: process.env.TURSO_API_TOKEN!,
25
+ * group: "my-app",
26
+ * },
27
+ * databaseUrl: "libsql://{tenant}-my-app-my-account.turso.io",
28
+ * authToken: process.env.TURSO_GROUP_AUTH_TOKEN!,
29
+ * schema,
30
+ * seed: { database: "seed-db" },
31
+ * });
32
+ * ```
33
+ *
34
+ * @example Tenant lifecycle
35
+ * ```typescript
36
+ * // Create a tenant (cloned from seed-db)
37
+ * await tenanso.createTenant("acme");
38
+ *
39
+ * // Query tenant's database
40
+ * await tenanso.withTenant("acme", async (db) => {
41
+ * const users = await db.select().from(usersTable);
42
+ * });
43
+ *
44
+ * // Or get the db directly
45
+ * const db = tenanso.dbFor("acme");
46
+ * ```
47
+ *
48
+ * @example With Hono middleware
49
+ * ```typescript
50
+ * import { tenantMiddleware, type TenansoEnv } from "tenanso/hono";
51
+ *
52
+ * const app = new Hono<TenansoEnv>();
53
+ * app.use("/api/*", tenantMiddleware(tenanso, {
54
+ * resolve: (c) => c.get("jwtPayload").tenant,
55
+ * }));
56
+ *
57
+ * app.get("/api/users", async (c) => {
58
+ * const users = await c.var.db.select().from(usersTable);
59
+ * return c.json(users);
60
+ * });
61
+ * ```
62
+ */
63
+ export function createTenanso(config) {
64
+ if (!config.databaseUrl.includes("{tenant}")) {
65
+ throw new Error(`databaseUrl must contain a {tenant} placeholder. Got: "${config.databaseUrl}"`);
66
+ }
67
+ const pool = new ConnectionPool(config);
68
+ const api = new TursoApi(config.turso, config.seed);
69
+ return {
70
+ async createTenant(name) {
71
+ await api.createDatabase(name);
72
+ },
73
+ async deleteTenant(name) {
74
+ await api.deleteDatabase(name);
75
+ pool.remove(name);
76
+ },
77
+ async listTenants() {
78
+ return api.listDatabases();
79
+ },
80
+ async tenantExists(name) {
81
+ return api.databaseExists(name);
82
+ },
83
+ dbFor(tenant) {
84
+ return pool.getDb(tenant);
85
+ },
86
+ async withTenant(tenant, fn) {
87
+ const db = pool.getDb(tenant);
88
+ return fn(db);
89
+ },
90
+ };
91
+ }
92
+ //# sourceMappingURL=tenanso.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenanso.js","sourceRoot":"","sources":["../src/tenanso.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAG1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH,MAAM,UAAU,aAAa,CAAC,MAAqB;IACjD,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;QAC7C,MAAM,IAAI,KAAK,CACb,0DAA0D,MAAM,CAAC,WAAW,GAAG,CAChF,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;IAEpD,OAAO;QACL,KAAK,CAAC,YAAY,CAAC,IAAY;YAC7B,MAAM,GAAG,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;QACjC,CAAC;QAED,KAAK,CAAC,YAAY,CAAC,IAAY;YAC7B,MAAM,GAAG,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;YAC/B,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACpB,CAAC;QAED,KAAK,CAAC,WAAW;YACf,OAAO,GAAG,CAAC,aAAa,EAAE,CAAC;QAC7B,CAAC;QAED,KAAK,CAAC,YAAY,CAAC,IAAY;YAC7B,OAAO,GAAG,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;QAClC,CAAC;QAED,KAAK,CAAC,MAAc;YAClB,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC5B,CAAC;QAED,KAAK,CAAC,UAAU,CACd,MAAc,EACd,EAAiC;YAEjC,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAC9B,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;QAChB,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { SeedConfig, TursoConfig } from "./types.js";
2
+ export declare class TursoApi {
3
+ private readonly baseUrl;
4
+ private readonly apiToken;
5
+ private readonly group;
6
+ private readonly seed;
7
+ constructor(config: TursoConfig, seed: SeedConfig | undefined);
8
+ createDatabase(name: string): Promise<void>;
9
+ deleteDatabase(name: string): Promise<void>;
10
+ listDatabases(): Promise<string[]>;
11
+ databaseExists(name: string): Promise<boolean>;
12
+ }
13
+ //# sourceMappingURL=turso-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"turso-api.d.ts","sourceRoot":"","sources":["../src/turso-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAoB1D,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyB;gBAElC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,GAAG,SAAS;IAQvD,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgC3C,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB3C,aAAa,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAelC,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAerD"}
@@ -0,0 +1,83 @@
1
+ const TENANT_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
2
+ function validateTenantName(name) {
3
+ if (!name || !TENANT_NAME_PATTERN.test(name)) {
4
+ throw new Error(`Invalid tenant name "${name}". Must match ${TENANT_NAME_PATTERN} (lowercase alphanumeric and hyphens, cannot start with a hyphen).`);
5
+ }
6
+ }
7
+ export class TursoApi {
8
+ baseUrl;
9
+ apiToken;
10
+ group;
11
+ seed;
12
+ constructor(config, seed) {
13
+ const base = config.baseUrl ?? "https://api.turso.tech";
14
+ this.baseUrl = `${base}/v1/organizations/${config.organizationSlug}`;
15
+ this.apiToken = config.apiToken;
16
+ this.group = config.group;
17
+ this.seed = seed;
18
+ }
19
+ async createDatabase(name) {
20
+ validateTenantName(name);
21
+ const body = {
22
+ name,
23
+ group: this.group,
24
+ };
25
+ if (this.seed) {
26
+ body["seed"] = {
27
+ type: "database",
28
+ name: this.seed.database,
29
+ };
30
+ }
31
+ const res = await fetch(`${this.baseUrl}/databases`, {
32
+ method: "POST",
33
+ headers: {
34
+ Authorization: `Bearer ${this.apiToken}`,
35
+ "Content-Type": "application/json",
36
+ },
37
+ body: JSON.stringify(body),
38
+ });
39
+ if (!res.ok) {
40
+ const text = await res.text();
41
+ throw new Error(`Failed to create database "${name}": ${res.status} ${text}`);
42
+ }
43
+ }
44
+ async deleteDatabase(name) {
45
+ validateTenantName(name);
46
+ const res = await fetch(`${this.baseUrl}/databases/${name}`, {
47
+ method: "DELETE",
48
+ headers: {
49
+ Authorization: `Bearer ${this.apiToken}`,
50
+ },
51
+ });
52
+ if (!res.ok) {
53
+ const text = await res.text();
54
+ throw new Error(`Failed to delete database "${name}": ${res.status} ${text}`);
55
+ }
56
+ }
57
+ async listDatabases() {
58
+ const res = await fetch(`${this.baseUrl}/databases`, {
59
+ headers: {
60
+ Authorization: `Bearer ${this.apiToken}`,
61
+ },
62
+ });
63
+ if (!res.ok) {
64
+ throw new Error(`Failed to list databases: ${res.status}`);
65
+ }
66
+ const data = (await res.json());
67
+ return data.databases.map((db) => db.Name);
68
+ }
69
+ async databaseExists(name) {
70
+ const res = await fetch(`${this.baseUrl}/databases/${name}`, {
71
+ headers: {
72
+ Authorization: `Bearer ${this.apiToken}`,
73
+ },
74
+ });
75
+ if (res.ok)
76
+ return true;
77
+ if (res.status === 404)
78
+ return false;
79
+ const text = await res.text();
80
+ throw new Error(`Failed to check database "${name}": ${res.status} ${text}`);
81
+ }
82
+ }
83
+ //# sourceMappingURL=turso-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"turso-api.js","sourceRoot":"","sources":["../src/turso-api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,349 @@
1
+ import type { drizzle } from "drizzle-orm/libsql";
2
+ /**
3
+ * Drizzle ORM database instance type.
4
+ *
5
+ * This is the return type of `drizzle()` from `drizzle-orm/libsql`.
6
+ * You can use this type to annotate variables or function parameters
7
+ * that accept a Drizzle database instance.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import type { DrizzleDb } from "tenanso";
12
+ *
13
+ * async function getUsers(db: DrizzleDb) {
14
+ * return db.select().from(usersTable);
15
+ * }
16
+ * ```
17
+ */
18
+ export type DrizzleDb = ReturnType<typeof drizzle>;
19
+ /**
20
+ * Turso Platform API configuration.
21
+ *
22
+ * These credentials are used to manage tenant databases (create, delete, list)
23
+ * via the [Turso Platform API](https://docs.turso.tech/api-reference/introduction).
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * const tursoConfig: TursoConfig = {
28
+ * organizationSlug: "my-org",
29
+ * apiToken: process.env.TURSO_API_TOKEN!,
30
+ * group: "my-app",
31
+ * };
32
+ * ```
33
+ */
34
+ export interface TursoConfig {
35
+ /**
36
+ * Your Turso organization slug.
37
+ *
38
+ * Found in your Turso dashboard URL: `https://app.turso.tech/{organizationSlug}`
39
+ */
40
+ organizationSlug: string;
41
+ /**
42
+ * Turso Platform API token for managing databases.
43
+ *
44
+ * Generate one with:
45
+ * ```bash
46
+ * turso auth api-tokens mint tenanso
47
+ * ```
48
+ */
49
+ apiToken: string;
50
+ /**
51
+ * Database group name. All tenant databases are created within this group.
52
+ *
53
+ * Use a group per application to organize databases — especially important
54
+ * when your Turso account hosts multiple services.
55
+ *
56
+ * Create a group with:
57
+ * ```bash
58
+ * turso group create my-app --location nrt
59
+ * ```
60
+ */
61
+ group: string;
62
+ /**
63
+ * Override the Turso Platform API base URL.
64
+ *
65
+ * Defaults to `https://api.turso.tech`. Useful for testing with a
66
+ * mock server or for self-hosted Turso deployments.
67
+ *
68
+ * @defaultValue `"https://api.turso.tech"`
69
+ */
70
+ baseUrl?: string | undefined;
71
+ }
72
+ /**
73
+ * Seed database configuration.
74
+ *
75
+ * When configured, new tenant databases are created by cloning an existing
76
+ * "seed" database. This seed database should have your schema and any initial
77
+ * data already applied, so new tenants are ready instantly without running migrations.
78
+ *
79
+ * To set up a seed database:
80
+ * ```bash
81
+ * turso db create seed-db --group my-app
82
+ * npx drizzle-kit push --url libsql://seed-db-my-app-my-account.turso.io --auth-token $TURSO_GROUP_AUTH_TOKEN
83
+ * ```
84
+ *
85
+ * @example
86
+ * ```typescript
87
+ * const tenanso = createTenanso({
88
+ * // ...
89
+ * seed: { database: "seed-db" },
90
+ * });
91
+ *
92
+ * // New tenant is cloned from seed-db with schema ready
93
+ * await tenanso.createTenant("acme");
94
+ * ```
95
+ */
96
+ export interface SeedConfig {
97
+ /**
98
+ * Name of an existing Turso database to clone when creating new tenants.
99
+ *
100
+ * This database should be in the same group as the tenant databases
101
+ * and have the current schema applied.
102
+ */
103
+ database: string;
104
+ }
105
+ /**
106
+ * Configuration for creating a tenanso instance.
107
+ *
108
+ * @example
109
+ * ```typescript
110
+ * import { createTenanso } from "tenanso";
111
+ * import * as schema from "./db/schema.js";
112
+ *
113
+ * const tenanso = createTenanso({
114
+ * turso: {
115
+ * organizationSlug: "my-org",
116
+ * apiToken: process.env.TURSO_API_TOKEN!,
117
+ * group: "my-app",
118
+ * },
119
+ * databaseUrl: "libsql://{tenant}-my-app-my-account.turso.io",
120
+ * authToken: process.env.TURSO_GROUP_AUTH_TOKEN!,
121
+ * schema,
122
+ * seed: { database: "seed-db" },
123
+ * });
124
+ * ```
125
+ */
126
+ export interface TenansoConfig {
127
+ /** Turso Platform API configuration. See {@link TursoConfig}. */
128
+ turso: TursoConfig;
129
+ /**
130
+ * URL template with `{tenant}` placeholder.
131
+ *
132
+ * The `{tenant}` placeholder is replaced with the tenant name when creating connections.
133
+ * Turso database URLs follow the pattern `libsql://{database-name}-{app}-{account}.turso.io`.
134
+ *
135
+ * @example
136
+ * ```typescript
137
+ * databaseUrl: "libsql://{tenant}-my-app-my-account.turso.io"
138
+ * // For tenant "acme" → "libsql://acme-my-app-my-account.turso.io"
139
+ * ```
140
+ */
141
+ databaseUrl: string;
142
+ /**
143
+ * Turso group auth token.
144
+ *
145
+ * A single token that works for all databases in a group.
146
+ * Generate one with:
147
+ * ```bash
148
+ * turso group tokens create my-app
149
+ * ```
150
+ */
151
+ authToken: string;
152
+ /**
153
+ * Drizzle schema for type-safe queries.
154
+ *
155
+ * Pass your Drizzle table definitions so that the Drizzle instances
156
+ * created for each tenant support relational queries.
157
+ *
158
+ * @example
159
+ * ```typescript
160
+ * import * as schema from "./db/schema.js";
161
+ *
162
+ * const tenanso = createTenanso({
163
+ * // ...
164
+ * schema,
165
+ * });
166
+ * ```
167
+ */
168
+ schema: Record<string, unknown>;
169
+ /**
170
+ * Seed database configuration.
171
+ *
172
+ * When set, {@link TenansoInstance.createTenant} clones the seed database
173
+ * instead of creating an empty database. The seed database should have
174
+ * your schema and any initial data already applied.
175
+ *
176
+ * See {@link SeedConfig} for setup instructions.
177
+ */
178
+ seed?: SeedConfig | undefined;
179
+ /**
180
+ * Maximum number of cached Drizzle connections.
181
+ *
182
+ * When this limit is reached, the least recently used connection is evicted.
183
+ * Tune this based on your memory constraints and expected number of
184
+ * concurrently active tenants.
185
+ *
186
+ * @defaultValue 50
187
+ */
188
+ maxConnections?: number | undefined;
189
+ }
190
+ /**
191
+ * The main tenanso instance returned by {@link createTenanso}.
192
+ *
193
+ * Provides methods for tenant lifecycle management (create, delete, list)
194
+ * and tenant-scoped database access (dbFor, withTenant).
195
+ *
196
+ * @example
197
+ * ```typescript
198
+ * const tenanso = createTenanso({ ... });
199
+ *
200
+ * // Lifecycle
201
+ * await tenanso.createTenant("acme");
202
+ * const tenants = await tenanso.listTenants();
203
+ *
204
+ * // Database access
205
+ * const db = tenanso.dbFor("acme");
206
+ * await tenanso.withTenant("acme", async (db) => {
207
+ * const users = await db.select().from(usersTable);
208
+ * });
209
+ * ```
210
+ */
211
+ export interface TenansoInstance {
212
+ /**
213
+ * Create a new tenant database via Turso Platform API.
214
+ *
215
+ * If {@link TenansoConfig.seed} is configured, the new database is cloned
216
+ * from the seed database with the schema already applied.
217
+ * Otherwise, an empty database is created and you must apply migrations separately.
218
+ *
219
+ * @param name - Unique tenant identifier, used as the Turso database name.
220
+ * Must be valid as a Turso database name (lowercase alphanumeric and hyphens).
221
+ * @throws Error if the Turso API call fails (e.g., database already exists, quota exceeded).
222
+ *
223
+ * @example
224
+ * ```typescript
225
+ * // During user signup
226
+ * await tenanso.createTenant("acme-corp");
227
+ *
228
+ * // Seed initial data
229
+ * await tenanso.withTenant("acme-corp", async (db) => {
230
+ * await db.insert(usersTable).values({ name: "Admin", email: "admin@acme.com" });
231
+ * });
232
+ * ```
233
+ */
234
+ createTenant(name: string): Promise<void>;
235
+ /**
236
+ * Delete a tenant database via Turso Platform API and remove it from the connection pool.
237
+ *
238
+ * This permanently destroys the database and all its data. The cached
239
+ * connection is also removed from the pool.
240
+ *
241
+ * @param name - Tenant identifier to delete.
242
+ * @throws Error if the Turso API call fails (e.g., database not found).
243
+ *
244
+ * @example
245
+ * ```typescript
246
+ * await tenanso.deleteTenant("acme-corp");
247
+ * ```
248
+ */
249
+ deleteTenant(name: string): Promise<void>;
250
+ /**
251
+ * List all tenant database names via Turso Platform API.
252
+ *
253
+ * Returns the names of all databases in the configured Turso organization.
254
+ * Note that this includes all databases, not just those created by tenanso.
255
+ *
256
+ * This calls the Turso Platform API, so it has network latency (~100-200ms).
257
+ * For per-request tenant validation, consider caching the result or using
258
+ * a JWT-based approach where the tenant is embedded in the token.
259
+ *
260
+ * @returns Array of database names.
261
+ *
262
+ * @example
263
+ * ```typescript
264
+ * const tenants = await tenanso.listTenants();
265
+ * // ["acme-corp", "other-corp", "startup-inc"]
266
+ *
267
+ * // Iterate over all tenants
268
+ * for (const tenant of tenants) {
269
+ * await tenanso.withTenant(tenant, async (db) => {
270
+ * // Run migrations, aggregate stats, etc.
271
+ * });
272
+ * }
273
+ * ```
274
+ */
275
+ listTenants(): Promise<string[]>;
276
+ /**
277
+ * Check if a tenant database exists.
278
+ *
279
+ * Calls {@link listTenants} under the hood, so it has the same
280
+ * network latency considerations.
281
+ *
282
+ * @param name - Tenant identifier to check.
283
+ * @returns `true` if the database exists, `false` otherwise.
284
+ *
285
+ * @example
286
+ * ```typescript
287
+ * if (await tenanso.tenantExists("acme-corp")) {
288
+ * // Tenant exists
289
+ * }
290
+ * ```
291
+ */
292
+ tenantExists(name: string): Promise<boolean>;
293
+ /**
294
+ * Get a cached Drizzle db instance for a specific tenant.
295
+ *
296
+ * Returns an existing cached connection or creates a new one.
297
+ * The connection is cached in an LRU pool — the least recently used
298
+ * connection is evicted when {@link TenansoConfig.maxConnections} is reached.
299
+ *
300
+ * @param tenant - Tenant identifier.
301
+ * @returns A Drizzle database instance connected to the tenant's database.
302
+ *
303
+ * @example
304
+ * ```typescript
305
+ * const db = tenanso.dbFor("acme-corp");
306
+ * const users = await db.select().from(usersTable);
307
+ *
308
+ * // In a Hono handler (without middleware)
309
+ * app.get("/api/users", async (c) => {
310
+ * const tenantId = c.get("jwtPayload").tenant;
311
+ * const db = tenanso.dbFor(tenantId);
312
+ * return c.json(await db.select().from(usersTable));
313
+ * });
314
+ * ```
315
+ */
316
+ dbFor(tenant: string): DrizzleDb;
317
+ /**
318
+ * Run a callback with a tenant-scoped Drizzle db instance.
319
+ *
320
+ * A convenience wrapper around {@link dbFor} that passes the db
321
+ * instance to a callback. Useful when you want to scope a block of
322
+ * operations to a specific tenant.
323
+ *
324
+ * @param tenant - Tenant identifier.
325
+ * @param fn - Async callback receiving the tenant's Drizzle db instance.
326
+ * @returns The return value of the callback.
327
+ *
328
+ * @example
329
+ * ```typescript
330
+ * // Signup flow: create tenant and seed data
331
+ * await tenanso.createTenant("acme-corp");
332
+ * await tenanso.withTenant("acme-corp", async (db) => {
333
+ * await db.insert(usersTable).values({
334
+ * name: "Admin",
335
+ * email: "admin@acme.com",
336
+ * role: "admin",
337
+ * });
338
+ * });
339
+ *
340
+ * // Cross-tenant aggregation
341
+ * const stats = await tenanso.withTenant("acme-corp", async (db) => {
342
+ * const users = await db.select().from(usersTable);
343
+ * return { userCount: users.length };
344
+ * });
345
+ * ```
346
+ */
347
+ withTenant<T>(tenant: string, fn: (db: DrizzleDb) => Promise<T>): Promise<T>;
348
+ }
349
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAEnD;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;;;;;OAOG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;;;;;OAUG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,WAAW,UAAU;IACzB;;;;;OAKG;IACH,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,aAAa;IAC5B,iEAAiE;IACjE,KAAK,EAAE,WAAW,CAAC;IACnB;;;;;;;;;;;OAWG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;;;OAQG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;;;;;;;;;;OAeG;IACH,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;IAC9B;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE1C;;;;;;;;;;;;;OAaG;IACH,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE1C;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,WAAW,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAEjC;;;;;;;;;;;;;;;OAeG;IACH,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE7C;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAEjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAC9E"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "tenanso",
3
+ "version": "0.1.0",
4
+ "description": "Multi-tenant SQLite with Drizzle ORM and Turso",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ },
13
+ "./hono": {
14
+ "import": "./dist/middleware/hono.js",
15
+ "types": "./dist/middleware/hono.d.ts"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "scripts": {
22
+ "prepublishOnly": "pnpm build",
23
+ "build": "tsc",
24
+ "typecheck": "tsc --noEmit",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest",
27
+ "test:e2e": "vitest run --config vitest.config.e2e.ts",
28
+ "docs:typedoc": "typedoc",
29
+ "docs:dev": "pnpm docs:typedoc && vitepress dev docs",
30
+ "docs:build": "pnpm docs:typedoc && vitepress build docs",
31
+ "docs:preview": "vitepress preview docs",
32
+ "docs:deploy": "pnpm docs:build && wrangler deploy"
33
+ },
34
+ "keywords": [
35
+ "multi-tenant",
36
+ "sqlite",
37
+ "turso",
38
+ "drizzle",
39
+ "hono"
40
+ ],
41
+ "author": "yoshixi",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/yoshixi/tenanso.git"
45
+ },
46
+ "homepage": "https://github.com/yoshixi/tenanso",
47
+ "license": "MIT",
48
+ "packageManager": "pnpm@10.12.1",
49
+ "dependencies": {
50
+ "@libsql/client": "^0.17.0",
51
+ "drizzle-orm": "^0.45.1"
52
+ },
53
+ "peerDependencies": {
54
+ "hono": "^4.0.0"
55
+ },
56
+ "peerDependenciesMeta": {
57
+ "hono": {
58
+ "optional": true
59
+ }
60
+ },
61
+ "devDependencies": {
62
+ "@hono/node-server": "^1.19.11",
63
+ "hono": "^4.12.7",
64
+ "typedoc": "^0.28.17",
65
+ "typedoc-plugin-markdown": "^4.10.0",
66
+ "typedoc-vitepress-theme": "^1.1.2",
67
+ "typescript": "^5.9.3",
68
+ "vitepress": "^1.6.4",
69
+ "vitest": "^3.2.3",
70
+ "wrangler": "^4.73.0"
71
+ }
72
+ }