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/dist/tenanso.js
ADDED
|
@@ -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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"}
|
package/dist/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
}
|