@usebetterdev/tenant-drizzle 0.2.0-beta.9 → 0.2.1-beta.1
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/README.md +16 -12
- package/dist/index.cjs +44 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -21
- package/dist/index.d.ts +34 -21
- package/dist/index.js +42 -16
- package/dist/index.js.map +1 -1
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -13,34 +13,38 @@ pnpm add @usebetterdev/tenant drizzle-orm pg
|
|
|
13
13
|
```ts
|
|
14
14
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
15
15
|
import { betterTenant } from "@usebetterdev/tenant";
|
|
16
|
-
import {
|
|
17
|
-
drizzleAdapter,
|
|
18
|
-
createGetTenantRepository,
|
|
19
|
-
tenantsTable,
|
|
20
|
-
} from "@usebetterdev/tenant/drizzle";
|
|
16
|
+
import { drizzleDatabase } from "@usebetterdev/tenant/drizzle";
|
|
21
17
|
|
|
22
18
|
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
23
19
|
const db = drizzle(pool);
|
|
24
20
|
|
|
25
|
-
const adapter = drizzleAdapter(db);
|
|
26
21
|
const tenant = betterTenant({
|
|
27
|
-
|
|
22
|
+
database: drizzleDatabase(db),
|
|
28
23
|
tenantResolver: { header: "x-tenant-id" },
|
|
29
|
-
getTenantRepository: createGetTenantRepository(tenantsTable),
|
|
30
24
|
});
|
|
31
25
|
|
|
32
26
|
// tenant.api.createTenant, updateTenant, listTenants, deleteTenant now work (via runAsSystem).
|
|
33
|
-
// In middleware: resolve tenantId, then runWithTenantAndDatabase(tenantId, adapter, next).
|
|
34
27
|
// In handlers: tenant.getDatabase() returns the transaction handle; tenant.getContext().tenant is the loaded Tenant (set loadTenant: false to skip loading).
|
|
35
28
|
```
|
|
36
29
|
|
|
37
|
-
|
|
30
|
+
`drizzleDatabase(db)` bundles the adapter and tenant repository into a single `DatabaseProvider`. If you use a custom tenants table, pass it via the `table` option:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { drizzleDatabase } from "@usebetterdev/tenant/drizzle";
|
|
34
|
+
|
|
35
|
+
const tenant = betterTenant({
|
|
36
|
+
database: drizzleDatabase(db, { table: myCustomTenantsTable }),
|
|
37
|
+
tenantResolver: { header: "x-tenant-id" },
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Table shape for tenants must match the CLI-generated schema: `id` (UUID), `name`, `slug`, `created_at`. Use the exported `tenantsTable` or pass your own table with the same column contract.
|
|
38
42
|
|
|
39
43
|
## Behaviour
|
|
40
44
|
|
|
41
45
|
- **runWithTenant(tenantId, fn)** — Starts a transaction, runs `SELECT set_config('app.current_tenant', tenantId, true)`, then calls `fn(tx)`. The `tx` handle is the Drizzle transaction; use only this for tenant-scoped tables so RLS sees the tenant.
|
|
42
46
|
- **runAsSystem(fn)** — Starts a transaction, runs `SELECT set_config('app.bypass_rls', 'true', true)`, then calls `fn(tx)`. Use for admin/cron only (e.g. tenant.api.\*). RLS policies must allow rows when `current_setting('app.bypass_rls', true) = 'true'` (CLI generates this bypass policy).
|
|
43
|
-
- **loadTenant** —
|
|
47
|
+
- **loadTenant** — The full Tenant row is loaded by default into `getContext().tenant`. Set `loadTenant: false` to opt out (saves one query per request).
|
|
44
48
|
- The handle passed to `fn` is the same type as your Drizzle db but transaction-scoped (full Drizzle API on `tx`).
|
|
45
49
|
|
|
46
50
|
## Testing
|
|
@@ -51,4 +55,4 @@ Integration tests in `src/adapter.integration.test.ts` use [@testcontainers/post
|
|
|
51
55
|
|
|
52
56
|
## Peer dependency
|
|
53
57
|
|
|
54
|
-
Requires `pg` (node-postgres)
|
|
58
|
+
Requires either `pg` (node-postgres) or `postgres` (postgres.js). Install whichever driver you use.
|
package/dist/index.cjs
CHANGED
|
@@ -20,8 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
drizzleDatabase: () => drizzleDatabase,
|
|
24
|
+
tenantId: () => tenantId,
|
|
25
25
|
tenantsTable: () => tenantsTable
|
|
26
26
|
});
|
|
27
27
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -30,10 +30,10 @@ module.exports = __toCommonJS(index_exports);
|
|
|
30
30
|
var import_drizzle_orm = require("drizzle-orm");
|
|
31
31
|
function drizzleAdapter(database) {
|
|
32
32
|
return {
|
|
33
|
-
async runWithTenant(
|
|
33
|
+
async runWithTenant(tenantId2, fn) {
|
|
34
34
|
return database.transaction(async (tx) => {
|
|
35
35
|
await tx.execute(
|
|
36
|
-
import_drizzle_orm.sql`SELECT set_config('app.current_tenant', ${
|
|
36
|
+
import_drizzle_orm.sql`SELECT set_config('app.current_tenant', ${tenantId2}, true)`
|
|
37
37
|
);
|
|
38
38
|
return fn(tx);
|
|
39
39
|
});
|
|
@@ -49,27 +49,37 @@ function drizzleAdapter(database) {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// src/repository.ts
|
|
53
|
+
var import_drizzle_orm3 = require("drizzle-orm");
|
|
54
|
+
|
|
52
55
|
// src/schema.ts
|
|
56
|
+
var import_drizzle_orm2 = require("drizzle-orm");
|
|
53
57
|
var import_pg_core = require("drizzle-orm/pg-core");
|
|
54
58
|
var tenantsTable = (0, import_pg_core.pgTable)("tenants", {
|
|
55
59
|
id: (0, import_pg_core.uuid)("id").primaryKey().defaultRandom(),
|
|
56
60
|
name: (0, import_pg_core.text)("name").notNull(),
|
|
57
61
|
slug: (0, import_pg_core.text)("slug").notNull().unique(),
|
|
58
62
|
createdAt: (0, import_pg_core.timestamp)("created_at", { withTimezone: true }).defaultNow().notNull()
|
|
59
|
-
});
|
|
63
|
+
}).enableRLS();
|
|
64
|
+
var tenantId = {
|
|
65
|
+
tenantId: (0, import_pg_core.uuid)("tenant_id").notNull().references(() => tenantsTable.id).default(import_drizzle_orm2.sql`(current_setting('app.current_tenant', true))::uuid`)
|
|
66
|
+
};
|
|
60
67
|
|
|
61
68
|
// src/repository.ts
|
|
62
|
-
var import_drizzle_orm2 = require("drizzle-orm");
|
|
63
69
|
function rowToTenant(row) {
|
|
70
|
+
const createdAt = row.createdAt ?? row.created_at;
|
|
71
|
+
if (createdAt === void 0 || createdAt === null) {
|
|
72
|
+
throw new Error("better-tenant: row missing created_at / createdAt");
|
|
73
|
+
}
|
|
64
74
|
return {
|
|
65
75
|
id: String(row.id),
|
|
66
76
|
name: String(row.name),
|
|
67
77
|
slug: String(row.slug),
|
|
68
|
-
createdAt
|
|
78
|
+
createdAt,
|
|
69
79
|
...row
|
|
70
80
|
};
|
|
71
81
|
}
|
|
72
|
-
function createGetTenantRepository(table) {
|
|
82
|
+
function createGetTenantRepository(table = tenantsTable) {
|
|
73
83
|
return (database) => {
|
|
74
84
|
const db = database;
|
|
75
85
|
return {
|
|
@@ -84,27 +94,35 @@ function createGetTenantRepository(table) {
|
|
|
84
94
|
}
|
|
85
95
|
return rowToTenant(row);
|
|
86
96
|
},
|
|
87
|
-
async update(
|
|
97
|
+
async update(tenantId2, data) {
|
|
88
98
|
const set = {};
|
|
89
99
|
if (data.name !== void 0) set.name = data.name;
|
|
90
100
|
if (data.slug !== void 0) set.slug = data.slug;
|
|
91
101
|
if (Object.keys(set).length === 0) {
|
|
92
|
-
const rows2 = await db.select().from(table).where((0,
|
|
102
|
+
const rows2 = await db.select().from(table).where((0, import_drizzle_orm3.eq)(table.id, tenantId2)).limit(1);
|
|
93
103
|
const row2 = Array.isArray(rows2) ? rows2[0] : void 0;
|
|
94
104
|
if (!row2 || typeof row2 !== "object") {
|
|
95
105
|
throw new Error("better-tenant: tenant not found");
|
|
96
106
|
}
|
|
97
107
|
return rowToTenant(row2);
|
|
98
108
|
}
|
|
99
|
-
const rows = await db.update(table).set(set).where((0,
|
|
109
|
+
const rows = await db.update(table).set(set).where((0, import_drizzle_orm3.eq)(table.id, tenantId2)).returning();
|
|
100
110
|
const row = rows[0];
|
|
101
111
|
if (!row || typeof row !== "object") {
|
|
102
112
|
throw new Error("better-tenant: tenant not found");
|
|
103
113
|
}
|
|
104
114
|
return rowToTenant(row);
|
|
105
115
|
},
|
|
106
|
-
async getById(
|
|
107
|
-
const rows = await db.select().from(table).where((0,
|
|
116
|
+
async getById(tenantId2) {
|
|
117
|
+
const rows = await db.select().from(table).where((0, import_drizzle_orm3.eq)(table.id, tenantId2)).limit(1);
|
|
118
|
+
const row = Array.isArray(rows) ? rows[0] : void 0;
|
|
119
|
+
if (!row || typeof row !== "object") {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
return rowToTenant(row);
|
|
123
|
+
},
|
|
124
|
+
async getBySlug(slug) {
|
|
125
|
+
const rows = await db.select().from(table).where((0, import_drizzle_orm3.eq)(table.slug, slug)).limit(1);
|
|
108
126
|
const row = Array.isArray(rows) ? rows[0] : void 0;
|
|
109
127
|
if (!row || typeof row !== "object") {
|
|
110
128
|
return null;
|
|
@@ -112,23 +130,31 @@ function createGetTenantRepository(table) {
|
|
|
112
130
|
return rowToTenant(row);
|
|
113
131
|
},
|
|
114
132
|
async list(options = {}) {
|
|
115
|
-
const limit =
|
|
133
|
+
const limit = options.limit ?? 50;
|
|
116
134
|
const offset = Math.max(0, options.offset ?? 0);
|
|
117
135
|
const rows = await db.select().from(table).limit(limit).offset(offset);
|
|
118
136
|
return (Array.isArray(rows) ? rows : []).map(
|
|
119
137
|
(r) => rowToTenant(r)
|
|
120
138
|
);
|
|
121
139
|
},
|
|
122
|
-
async delete(
|
|
123
|
-
await db.delete(table).where((0,
|
|
140
|
+
async delete(tenantId2) {
|
|
141
|
+
await db.delete(table).where((0, import_drizzle_orm3.eq)(table.id, tenantId2));
|
|
124
142
|
}
|
|
125
143
|
};
|
|
126
144
|
};
|
|
127
145
|
}
|
|
146
|
+
|
|
147
|
+
// src/database.ts
|
|
148
|
+
function drizzleDatabase(db, options) {
|
|
149
|
+
return {
|
|
150
|
+
adapter: drizzleAdapter(db),
|
|
151
|
+
getTenantRepository: createGetTenantRepository(options?.table)
|
|
152
|
+
};
|
|
153
|
+
}
|
|
128
154
|
// Annotate the CommonJS export names for ESM import in node:
|
|
129
155
|
0 && (module.exports = {
|
|
130
|
-
|
|
131
|
-
|
|
156
|
+
drizzleDatabase,
|
|
157
|
+
tenantId,
|
|
132
158
|
tenantsTable
|
|
133
159
|
});
|
|
134
160
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/adapter.ts","../src/schema.ts","../src/repository.ts"],"sourcesContent":["export { drizzleAdapter } from \"./adapter.js\";\nexport type { DrizzlePgDatabase, DrizzlePgTransaction } from \"./adapter.js\";\nexport { tenantsTable } from \"./schema.js\";\nexport {\n createGetTenantRepository,\n type TenantsTableLike,\n} from \"./repository.js\";\n","import { sql } from \"drizzle-orm\";\nimport type { TenantAdapter } from \"@usebetterdev/tenant-core\";\n\n/**\n * Minimal shape for a Drizzle pg transaction: must support execute(sql).\n * The actual Drizzle transaction extends this with insert, select, etc.\n */\nexport interface DrizzlePgTransaction {\n execute(query: ReturnType<typeof sql> | unknown): Promise<unknown>;\n}\n\n/**\n * Minimal shape for a Drizzle pg database: must support transaction().\n * Generic over TTx so the full transaction type (with insert, select, etc.) is preserved.\n */\nexport interface DrizzlePgDatabase<TTx extends DrizzlePgTransaction = DrizzlePgTransaction> {\n transaction<T>(\n callback: (tx: TTx) => Promise<T>,\n ): Promise<T>;\n}\n\n/**\n * Creates a TenantAdapter for Drizzle (PostgreSQL).\n *\n * Uses a transaction per runWithTenant: BEGIN → SET LOCAL app.current_tenant = tenantId → fn(tx) → COMMIT.\n * The handle passed to fn is the transaction runner; use only this handle for tenant-scoped queries so RLS applies.\n *\n * runAsSystem runs in a transaction with SET LOCAL app.bypass_rls = true so RLS policies that allow\n * when current_setting('app.bypass_rls', true) = 'true' permit access (CLI generates this bypass policy).\n *\n * @param database - Drizzle pg database (from drizzle(pool) or similar)\n * @returns TenantAdapter implementation\n */\nexport function drizzleAdapter<TTx extends DrizzlePgTransaction>(\n database: DrizzlePgDatabase<TTx>,\n): TenantAdapter<TTx, TTx> {\n return {\n async runWithTenant<T>(\n tenantId: string,\n fn: (database: TTx) => Promise<T>,\n ): Promise<T> {\n return database.transaction(async (tx) => {\n await tx.execute(\n sql`SELECT set_config('app.current_tenant', ${tenantId}, true)`,\n );\n return fn(tx);\n });\n },\n\n async runAsSystem<T>(\n fn: (database: TTx) => Promise<T>,\n ): Promise<T> {\n return database.transaction(async (tx) => {\n await tx.execute(\n sql`SELECT set_config('app.bypass_rls', 'true', true)`,\n );\n return fn(tx);\n });\n },\n };\n}\n","import { pgTable, text, timestamp, uuid } from \"drizzle-orm/pg-core\";\n\n/**\n * Standard tenants table schema matching the CLI-generated shape.\n * Columns: id (UUID), name, slug, created_at.\n * Use this table or provide your own with the same column contract for createGetTenantRepository.\n */\nexport const tenantsTable = pgTable(\"tenants\", {\n id: uuid(\"id\").primaryKey().defaultRandom(),\n name: text(\"name\").notNull(),\n slug: text(\"slug\").notNull().unique(),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).defaultNow().notNull(),\n});\n","import { eq } from \"drizzle-orm\";\nimport type { Tenant, TenantRepository } from \"@usebetterdev/tenant-core\";\nimport type { tenantsTable } from \"./schema.js\";\n\n/** Table shape expected by createGetTenantRepository: id, name, slug, createdAt (or created_at). */\nexport type TenantsTableLike = typeof tenantsTable;\n\nfunction rowToTenant(row: Record<string, unknown>): Tenant {\n return {\n id: String(row.id),\n name: String(row.name),\n slug: String(row.slug),\n createdAt: (row.createdAt ?? row.created_at) as Date | string,\n ...row,\n };\n}\n\n/**\n * Creates getTenantRepository for use with betterTenant({ getTenantRepository }).\n * Pass the tenants table (e.g. tenantsTable from this package or your own with id, name, slug, created_at).\n * The returned function is called by core with the system database handle (from adapter.runAsSystem).\n */\nexport function createGetTenantRepository(\n table: TenantsTableLike,\n): (database: unknown) => TenantRepository {\n return (database: unknown) => {\n const db = database as {\n insert: (t: TenantsTableLike) => {\n values: (v: { name: string; slug: string }) => {\n returning: () => Promise<unknown[]>;\n };\n };\n update: (t: TenantsTableLike) => {\n set: (v: Partial<{ name: string; slug: string }>) => {\n where: (c: unknown) => { returning: () => Promise<unknown[]> };\n };\n };\n select: () => {\n from: (t: TenantsTableLike) => {\n where: (c: unknown) => { limit: (n: number) => Promise<unknown[]> };\n limit: (n: number) => { offset: (n: number) => Promise<unknown[]> };\n };\n };\n delete: (t: TenantsTableLike) => {\n where: (c: unknown) => Promise<unknown>;\n };\n };\n\n return {\n async create(data) {\n const rows = await db\n .insert(table)\n .values({\n name: data.name,\n slug: data.slug,\n })\n .returning();\n const row = rows[0];\n if (!row || typeof row !== \"object\") {\n throw new Error(\"better-tenant: createTenant failed to return row\");\n }\n return rowToTenant(row as Record<string, unknown>);\n },\n\n async update(tenantId, data) {\n const set: Partial<{ name: string; slug: string }> = {};\n if (data.name !== undefined) set.name = data.name;\n if (data.slug !== undefined) set.slug = data.slug;\n if (Object.keys(set).length === 0) {\n const rows = await db\n .select()\n .from(table)\n .where(eq(table.id, tenantId))\n .limit(1);\n const row = Array.isArray(rows) ? rows[0] : undefined;\n if (!row || typeof row !== \"object\") {\n throw new Error(\"better-tenant: tenant not found\");\n }\n return rowToTenant(row as Record<string, unknown>);\n }\n const rows = await db\n .update(table)\n .set(set)\n .where(eq(table.id, tenantId))\n .returning();\n const row = rows[0];\n if (!row || typeof row !== \"object\") {\n throw new Error(\"better-tenant: tenant not found\");\n }\n return rowToTenant(row as Record<string, unknown>);\n },\n\n async getById(tenantId: string) {\n const rows = await db\n .select()\n .from(table)\n .where(eq(table.id, tenantId))\n .limit(1);\n const row = Array.isArray(rows) ? rows[0] : undefined;\n if (!row || typeof row !== \"object\") {\n return null;\n }\n\n return rowToTenant(row as Record<string, unknown>);\n },\n\n async list(options = {}) {\n const limit = Math.min(options.limit ?? 50, 50);\n const offset = Math.max(0, options.offset ?? 0);\n const rows = await db.select().from(table).limit(limit).offset(offset);\n return (Array.isArray(rows) ? rows : []).map((r) =>\n rowToTenant(r as Record<string, unknown>),\n );\n },\n\n async delete(tenantId) {\n await db.delete(table).where(eq(table.id, tenantId));\n },\n };\n };\n}\n"],"mappings":"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","names":["import_drizzle_orm","rows","row"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/adapter.ts","../src/repository.ts","../src/schema.ts","../src/database.ts"],"sourcesContent":["export { drizzleDatabase } from \"./database.js\";\nexport { tenantsTable, tenantId } from \"./schema.js\";\n","import { sql } from \"drizzle-orm\";\nimport type { TenantAdapter } from \"@usebetterdev/tenant-core\";\n\n/**\n * Minimal shape for a Drizzle pg transaction: must support execute(sql).\n * The actual Drizzle transaction extends this with insert, select, etc.\n */\nexport interface DrizzlePgTransaction {\n execute(query: ReturnType<typeof sql> | unknown): Promise<unknown>;\n}\n\n/**\n * Minimal shape for a Drizzle pg database: must support transaction().\n * Generic over TTx so the full transaction type (with insert, select, etc.) is preserved.\n */\nexport interface DrizzlePgDatabase<TTx extends DrizzlePgTransaction = DrizzlePgTransaction> {\n transaction<T>(\n callback: (tx: TTx) => Promise<T>,\n ): Promise<T>;\n}\n\n/**\n * Creates a TenantAdapter for Drizzle (PostgreSQL).\n *\n * Uses a transaction per runWithTenant: BEGIN → SET LOCAL app.current_tenant = tenantId → fn(tx) → COMMIT.\n * The handle passed to fn is the transaction runner; use only this handle for tenant-scoped queries so RLS applies.\n *\n * runAsSystem runs in a transaction with SET LOCAL app.bypass_rls = true so RLS policies that allow\n * when current_setting('app.bypass_rls', true) = 'true' permit access (CLI generates this bypass policy).\n *\n * @param database - Drizzle pg database (from drizzle(pool) or similar)\n * @returns TenantAdapter implementation\n */\nexport function drizzleAdapter<TTx extends DrizzlePgTransaction>(\n database: DrizzlePgDatabase<TTx>,\n): TenantAdapter<TTx, TTx> {\n return {\n async runWithTenant<T>(\n tenantId: string,\n fn: (database: TTx) => Promise<T>,\n ): Promise<T> {\n return database.transaction(async (tx) => {\n await tx.execute(\n sql`SELECT set_config('app.current_tenant', ${tenantId}, true)`,\n );\n return fn(tx);\n });\n },\n\n async runAsSystem<T>(\n fn: (database: TTx) => Promise<T>,\n ): Promise<T> {\n return database.transaction(async (tx) => {\n await tx.execute(\n sql`SELECT set_config('app.bypass_rls', 'true', true)`,\n );\n return fn(tx);\n });\n },\n };\n}\n","import { eq } from \"drizzle-orm\";\nimport type { Tenant, TenantRepository } from \"@usebetterdev/tenant-core\";\nimport { tenantsTable } from \"./schema.js\";\n\n/** Table shape expected by createGetTenantRepository: id, name, slug, createdAt (or created_at). */\nexport type TenantsTableLike = typeof tenantsTable;\n\nfunction rowToTenant(row: Record<string, unknown>): Tenant {\n const createdAt = row.createdAt ?? row.created_at;\n if (createdAt === undefined || createdAt === null) {\n throw new Error(\"better-tenant: row missing created_at / createdAt\");\n }\n return {\n id: String(row.id),\n name: String(row.name),\n slug: String(row.slug),\n createdAt: createdAt as Date | string,\n ...row,\n };\n}\n\n/**\n * Creates getTenantRepository for use with betterTenant({ getTenantRepository }).\n * Pass the tenants table (e.g. tenantsTable from this package or your own with id, name, slug, created_at).\n * The returned function is called by core with the system database handle (from adapter.runAsSystem).\n */\nexport function createGetTenantRepository(\n table: TenantsTableLike = tenantsTable,\n): (database: unknown) => TenantRepository {\n return (database: unknown) => {\n const db = database as {\n insert: (t: TenantsTableLike) => {\n values: (v: { name: string; slug: string }) => {\n returning: () => Promise<unknown[]>;\n };\n };\n update: (t: TenantsTableLike) => {\n set: (v: Partial<{ name: string; slug: string }>) => {\n where: (c: unknown) => { returning: () => Promise<unknown[]> };\n };\n };\n select: () => {\n from: (t: TenantsTableLike) => {\n where: (c: unknown) => { limit: (n: number) => Promise<unknown[]> };\n limit: (n: number) => { offset: (n: number) => Promise<unknown[]> };\n };\n };\n delete: (t: TenantsTableLike) => {\n where: (c: unknown) => Promise<unknown>;\n };\n };\n\n return {\n async create(data) {\n const rows = await db\n .insert(table)\n .values({\n name: data.name,\n slug: data.slug,\n })\n .returning();\n const row = rows[0];\n if (!row || typeof row !== \"object\") {\n throw new Error(\"better-tenant: createTenant failed to return row\");\n }\n return rowToTenant(row as Record<string, unknown>);\n },\n\n async update(tenantId, data) {\n const set: Partial<{ name: string; slug: string }> = {};\n if (data.name !== undefined) set.name = data.name;\n if (data.slug !== undefined) set.slug = data.slug;\n if (Object.keys(set).length === 0) {\n const rows = await db\n .select()\n .from(table)\n .where(eq(table.id, tenantId))\n .limit(1);\n const row = Array.isArray(rows) ? rows[0] : undefined;\n if (!row || typeof row !== \"object\") {\n throw new Error(\"better-tenant: tenant not found\");\n }\n return rowToTenant(row as Record<string, unknown>);\n }\n const rows = await db\n .update(table)\n .set(set)\n .where(eq(table.id, tenantId))\n .returning();\n const row = rows[0];\n if (!row || typeof row !== \"object\") {\n throw new Error(\"better-tenant: tenant not found\");\n }\n return rowToTenant(row as Record<string, unknown>);\n },\n\n async getById(tenantId: string) {\n const rows = await db\n .select()\n .from(table)\n .where(eq(table.id, tenantId))\n .limit(1);\n const row = Array.isArray(rows) ? rows[0] : undefined;\n if (!row || typeof row !== \"object\") {\n return null;\n }\n\n return rowToTenant(row as Record<string, unknown>);\n },\n\n async getBySlug(slug: string) {\n const rows = await db\n .select()\n .from(table)\n .where(eq(table.slug, slug))\n .limit(1);\n const row = Array.isArray(rows) ? rows[0] : undefined;\n if (!row || typeof row !== \"object\") {\n return null;\n }\n return rowToTenant(row as Record<string, unknown>);\n },\n\n async list(options = {}) {\n const limit = options.limit ?? 50;\n const offset = Math.max(0, options.offset ?? 0);\n const rows = await db.select().from(table).limit(limit).offset(offset);\n return (Array.isArray(rows) ? rows : []).map((r) =>\n rowToTenant(r as Record<string, unknown>),\n );\n },\n\n async delete(tenantId) {\n await db.delete(table).where(eq(table.id, tenantId));\n },\n };\n };\n}\n","import { sql } from \"drizzle-orm\";\nimport { pgTable, text, timestamp, uuid } from \"drizzle-orm/pg-core\";\n\n/**\n * Standard tenants table schema matching the CLI-generated shape.\n * Columns: id (UUID), name, slug, created_at.\n * Use this table or provide your own with the same column contract for createGetTenantRepository.\n */\nexport const tenantsTable = pgTable(\"tenants\", {\n id: uuid(\"id\").primaryKey().defaultRandom(),\n name: text(\"name\").notNull(),\n slug: text(\"slug\").notNull().unique(),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).defaultNow().notNull(),\n}).enableRLS();\n\n/**\n * Pre-configured tenant_id column for use in tenant-scoped tables.\n * References tenants.id and auto-populates from the current session variable.\n *\n * Usage:\n * ```ts\n * import { tenantId } from \"@usebetterdev/tenant/drizzle\";\n *\n * export const projectsTable = pgTable(\"projects\", {\n * id: serial(\"id\").primaryKey(),\n * name: text(\"name\").notNull(),\n * ...tenantId,\n * }).enableRLS();\n * ```\n */\nexport const tenantId = {\n tenantId: uuid(\"tenant_id\")\n .notNull()\n .references(() => tenantsTable.id)\n .default(sql`(current_setting('app.current_tenant', true))::uuid`),\n} as const;\n","import type { DatabaseProvider } from \"@usebetterdev/tenant-core\";\nimport { drizzleAdapter } from \"./adapter.js\";\nimport type { DrizzlePgDatabase, DrizzlePgTransaction } from \"./adapter.js\";\nimport { createGetTenantRepository, type TenantsTableLike } from \"./repository.js\";\n\n/**\n * Creates a DatabaseProvider for Drizzle (PostgreSQL).\n *\n * Bundles drizzleAdapter + createGetTenantRepository into a single config value\n * for use with `betterTenant({ database: drizzleDatabase(db) })`.\n *\n * @param db - Drizzle pg database (from drizzle(pool) or similar)\n * @param options - Optional: custom tenants table\n */\nexport function drizzleDatabase<TTx extends DrizzlePgTransaction>(\n db: DrizzlePgDatabase<TTx>,\n options?: { table?: TenantsTableLike },\n): DatabaseProvider<TTx, TTx> {\n return {\n adapter: drizzleAdapter(db),\n getTenantRepository: createGetTenantRepository(options?.table),\n };\n}\n"],"mappings":"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","names":["tenantId","import_drizzle_orm","import_drizzle_orm","tenantId","rows","row"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { DatabaseProvider } from '@usebetterdev/tenant-core';
|
|
2
|
+
import * as drizzle_orm from 'drizzle-orm';
|
|
1
3
|
import { sql } from 'drizzle-orm';
|
|
2
|
-
import { TenantAdapter, TenantRepository } from '@usebetterdev/tenant-core';
|
|
3
4
|
import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -16,26 +17,13 @@ interface DrizzlePgTransaction {
|
|
|
16
17
|
interface DrizzlePgDatabase<TTx extends DrizzlePgTransaction = DrizzlePgTransaction> {
|
|
17
18
|
transaction<T>(callback: (tx: TTx) => Promise<T>): Promise<T>;
|
|
18
19
|
}
|
|
19
|
-
/**
|
|
20
|
-
* Creates a TenantAdapter for Drizzle (PostgreSQL).
|
|
21
|
-
*
|
|
22
|
-
* Uses a transaction per runWithTenant: BEGIN → SET LOCAL app.current_tenant = tenantId → fn(tx) → COMMIT.
|
|
23
|
-
* The handle passed to fn is the transaction runner; use only this handle for tenant-scoped queries so RLS applies.
|
|
24
|
-
*
|
|
25
|
-
* runAsSystem runs in a transaction with SET LOCAL app.bypass_rls = true so RLS policies that allow
|
|
26
|
-
* when current_setting('app.bypass_rls', true) = 'true' permit access (CLI generates this bypass policy).
|
|
27
|
-
*
|
|
28
|
-
* @param database - Drizzle pg database (from drizzle(pool) or similar)
|
|
29
|
-
* @returns TenantAdapter implementation
|
|
30
|
-
*/
|
|
31
|
-
declare function drizzleAdapter<TTx extends DrizzlePgTransaction>(database: DrizzlePgDatabase<TTx>): TenantAdapter<TTx, TTx>;
|
|
32
20
|
|
|
33
21
|
/**
|
|
34
22
|
* Standard tenants table schema matching the CLI-generated shape.
|
|
35
23
|
* Columns: id (UUID), name, slug, created_at.
|
|
36
24
|
* Use this table or provide your own with the same column contract for createGetTenantRepository.
|
|
37
25
|
*/
|
|
38
|
-
declare const tenantsTable: drizzle_orm_pg_core.PgTableWithColumns<{
|
|
26
|
+
declare const tenantsTable: Omit<drizzle_orm_pg_core.PgTableWithColumns<{
|
|
39
27
|
name: "tenants";
|
|
40
28
|
schema: undefined;
|
|
41
29
|
columns: {
|
|
@@ -109,15 +97,40 @@ declare const tenantsTable: drizzle_orm_pg_core.PgTableWithColumns<{
|
|
|
109
97
|
}, {}, {}>;
|
|
110
98
|
};
|
|
111
99
|
dialect: "pg";
|
|
112
|
-
}>;
|
|
100
|
+
}>, "enableRLS">;
|
|
101
|
+
/**
|
|
102
|
+
* Pre-configured tenant_id column for use in tenant-scoped tables.
|
|
103
|
+
* References tenants.id and auto-populates from the current session variable.
|
|
104
|
+
*
|
|
105
|
+
* Usage:
|
|
106
|
+
* ```ts
|
|
107
|
+
* import { tenantId } from "@usebetterdev/tenant/drizzle";
|
|
108
|
+
*
|
|
109
|
+
* export const projectsTable = pgTable("projects", {
|
|
110
|
+
* id: serial("id").primaryKey(),
|
|
111
|
+
* name: text("name").notNull(),
|
|
112
|
+
* ...tenantId,
|
|
113
|
+
* }).enableRLS();
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
declare const tenantId: {
|
|
117
|
+
readonly tenantId: drizzle_orm.HasDefault<drizzle_orm.NotNull<drizzle_orm_pg_core.PgUUIDBuilderInitial<"tenant_id">>>;
|
|
118
|
+
};
|
|
113
119
|
|
|
114
120
|
/** Table shape expected by createGetTenantRepository: id, name, slug, createdAt (or created_at). */
|
|
115
121
|
type TenantsTableLike = typeof tenantsTable;
|
|
122
|
+
|
|
116
123
|
/**
|
|
117
|
-
* Creates
|
|
118
|
-
*
|
|
119
|
-
*
|
|
124
|
+
* Creates a DatabaseProvider for Drizzle (PostgreSQL).
|
|
125
|
+
*
|
|
126
|
+
* Bundles drizzleAdapter + createGetTenantRepository into a single config value
|
|
127
|
+
* for use with `betterTenant({ database: drizzleDatabase(db) })`.
|
|
128
|
+
*
|
|
129
|
+
* @param db - Drizzle pg database (from drizzle(pool) or similar)
|
|
130
|
+
* @param options - Optional: custom tenants table
|
|
120
131
|
*/
|
|
121
|
-
declare function
|
|
132
|
+
declare function drizzleDatabase<TTx extends DrizzlePgTransaction>(db: DrizzlePgDatabase<TTx>, options?: {
|
|
133
|
+
table?: TenantsTableLike;
|
|
134
|
+
}): DatabaseProvider<TTx, TTx>;
|
|
122
135
|
|
|
123
|
-
export {
|
|
136
|
+
export { drizzleDatabase, tenantId, tenantsTable };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { DatabaseProvider } from '@usebetterdev/tenant-core';
|
|
2
|
+
import * as drizzle_orm from 'drizzle-orm';
|
|
1
3
|
import { sql } from 'drizzle-orm';
|
|
2
|
-
import { TenantAdapter, TenantRepository } from '@usebetterdev/tenant-core';
|
|
3
4
|
import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -16,26 +17,13 @@ interface DrizzlePgTransaction {
|
|
|
16
17
|
interface DrizzlePgDatabase<TTx extends DrizzlePgTransaction = DrizzlePgTransaction> {
|
|
17
18
|
transaction<T>(callback: (tx: TTx) => Promise<T>): Promise<T>;
|
|
18
19
|
}
|
|
19
|
-
/**
|
|
20
|
-
* Creates a TenantAdapter for Drizzle (PostgreSQL).
|
|
21
|
-
*
|
|
22
|
-
* Uses a transaction per runWithTenant: BEGIN → SET LOCAL app.current_tenant = tenantId → fn(tx) → COMMIT.
|
|
23
|
-
* The handle passed to fn is the transaction runner; use only this handle for tenant-scoped queries so RLS applies.
|
|
24
|
-
*
|
|
25
|
-
* runAsSystem runs in a transaction with SET LOCAL app.bypass_rls = true so RLS policies that allow
|
|
26
|
-
* when current_setting('app.bypass_rls', true) = 'true' permit access (CLI generates this bypass policy).
|
|
27
|
-
*
|
|
28
|
-
* @param database - Drizzle pg database (from drizzle(pool) or similar)
|
|
29
|
-
* @returns TenantAdapter implementation
|
|
30
|
-
*/
|
|
31
|
-
declare function drizzleAdapter<TTx extends DrizzlePgTransaction>(database: DrizzlePgDatabase<TTx>): TenantAdapter<TTx, TTx>;
|
|
32
20
|
|
|
33
21
|
/**
|
|
34
22
|
* Standard tenants table schema matching the CLI-generated shape.
|
|
35
23
|
* Columns: id (UUID), name, slug, created_at.
|
|
36
24
|
* Use this table or provide your own with the same column contract for createGetTenantRepository.
|
|
37
25
|
*/
|
|
38
|
-
declare const tenantsTable: drizzle_orm_pg_core.PgTableWithColumns<{
|
|
26
|
+
declare const tenantsTable: Omit<drizzle_orm_pg_core.PgTableWithColumns<{
|
|
39
27
|
name: "tenants";
|
|
40
28
|
schema: undefined;
|
|
41
29
|
columns: {
|
|
@@ -109,15 +97,40 @@ declare const tenantsTable: drizzle_orm_pg_core.PgTableWithColumns<{
|
|
|
109
97
|
}, {}, {}>;
|
|
110
98
|
};
|
|
111
99
|
dialect: "pg";
|
|
112
|
-
}>;
|
|
100
|
+
}>, "enableRLS">;
|
|
101
|
+
/**
|
|
102
|
+
* Pre-configured tenant_id column for use in tenant-scoped tables.
|
|
103
|
+
* References tenants.id and auto-populates from the current session variable.
|
|
104
|
+
*
|
|
105
|
+
* Usage:
|
|
106
|
+
* ```ts
|
|
107
|
+
* import { tenantId } from "@usebetterdev/tenant/drizzle";
|
|
108
|
+
*
|
|
109
|
+
* export const projectsTable = pgTable("projects", {
|
|
110
|
+
* id: serial("id").primaryKey(),
|
|
111
|
+
* name: text("name").notNull(),
|
|
112
|
+
* ...tenantId,
|
|
113
|
+
* }).enableRLS();
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
declare const tenantId: {
|
|
117
|
+
readonly tenantId: drizzle_orm.HasDefault<drizzle_orm.NotNull<drizzle_orm_pg_core.PgUUIDBuilderInitial<"tenant_id">>>;
|
|
118
|
+
};
|
|
113
119
|
|
|
114
120
|
/** Table shape expected by createGetTenantRepository: id, name, slug, createdAt (or created_at). */
|
|
115
121
|
type TenantsTableLike = typeof tenantsTable;
|
|
122
|
+
|
|
116
123
|
/**
|
|
117
|
-
* Creates
|
|
118
|
-
*
|
|
119
|
-
*
|
|
124
|
+
* Creates a DatabaseProvider for Drizzle (PostgreSQL).
|
|
125
|
+
*
|
|
126
|
+
* Bundles drizzleAdapter + createGetTenantRepository into a single config value
|
|
127
|
+
* for use with `betterTenant({ database: drizzleDatabase(db) })`.
|
|
128
|
+
*
|
|
129
|
+
* @param db - Drizzle pg database (from drizzle(pool) or similar)
|
|
130
|
+
* @param options - Optional: custom tenants table
|
|
120
131
|
*/
|
|
121
|
-
declare function
|
|
132
|
+
declare function drizzleDatabase<TTx extends DrizzlePgTransaction>(db: DrizzlePgDatabase<TTx>, options?: {
|
|
133
|
+
table?: TenantsTableLike;
|
|
134
|
+
}): DatabaseProvider<TTx, TTx>;
|
|
122
135
|
|
|
123
|
-
export {
|
|
136
|
+
export { drizzleDatabase, tenantId, tenantsTable };
|
package/dist/index.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { sql } from "drizzle-orm";
|
|
3
3
|
function drizzleAdapter(database) {
|
|
4
4
|
return {
|
|
5
|
-
async runWithTenant(
|
|
5
|
+
async runWithTenant(tenantId2, fn) {
|
|
6
6
|
return database.transaction(async (tx) => {
|
|
7
7
|
await tx.execute(
|
|
8
|
-
sql`SELECT set_config('app.current_tenant', ${
|
|
8
|
+
sql`SELECT set_config('app.current_tenant', ${tenantId2}, true)`
|
|
9
9
|
);
|
|
10
10
|
return fn(tx);
|
|
11
11
|
});
|
|
@@ -21,27 +21,37 @@ function drizzleAdapter(database) {
|
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// src/repository.ts
|
|
25
|
+
import { eq } from "drizzle-orm";
|
|
26
|
+
|
|
24
27
|
// src/schema.ts
|
|
28
|
+
import { sql as sql2 } from "drizzle-orm";
|
|
25
29
|
import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
|
26
30
|
var tenantsTable = pgTable("tenants", {
|
|
27
31
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
28
32
|
name: text("name").notNull(),
|
|
29
33
|
slug: text("slug").notNull().unique(),
|
|
30
34
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull()
|
|
31
|
-
});
|
|
35
|
+
}).enableRLS();
|
|
36
|
+
var tenantId = {
|
|
37
|
+
tenantId: uuid("tenant_id").notNull().references(() => tenantsTable.id).default(sql2`(current_setting('app.current_tenant', true))::uuid`)
|
|
38
|
+
};
|
|
32
39
|
|
|
33
40
|
// src/repository.ts
|
|
34
|
-
import { eq } from "drizzle-orm";
|
|
35
41
|
function rowToTenant(row) {
|
|
42
|
+
const createdAt = row.createdAt ?? row.created_at;
|
|
43
|
+
if (createdAt === void 0 || createdAt === null) {
|
|
44
|
+
throw new Error("better-tenant: row missing created_at / createdAt");
|
|
45
|
+
}
|
|
36
46
|
return {
|
|
37
47
|
id: String(row.id),
|
|
38
48
|
name: String(row.name),
|
|
39
49
|
slug: String(row.slug),
|
|
40
|
-
createdAt
|
|
50
|
+
createdAt,
|
|
41
51
|
...row
|
|
42
52
|
};
|
|
43
53
|
}
|
|
44
|
-
function createGetTenantRepository(table) {
|
|
54
|
+
function createGetTenantRepository(table = tenantsTable) {
|
|
45
55
|
return (database) => {
|
|
46
56
|
const db = database;
|
|
47
57
|
return {
|
|
@@ -56,27 +66,35 @@ function createGetTenantRepository(table) {
|
|
|
56
66
|
}
|
|
57
67
|
return rowToTenant(row);
|
|
58
68
|
},
|
|
59
|
-
async update(
|
|
69
|
+
async update(tenantId2, data) {
|
|
60
70
|
const set = {};
|
|
61
71
|
if (data.name !== void 0) set.name = data.name;
|
|
62
72
|
if (data.slug !== void 0) set.slug = data.slug;
|
|
63
73
|
if (Object.keys(set).length === 0) {
|
|
64
|
-
const rows2 = await db.select().from(table).where(eq(table.id,
|
|
74
|
+
const rows2 = await db.select().from(table).where(eq(table.id, tenantId2)).limit(1);
|
|
65
75
|
const row2 = Array.isArray(rows2) ? rows2[0] : void 0;
|
|
66
76
|
if (!row2 || typeof row2 !== "object") {
|
|
67
77
|
throw new Error("better-tenant: tenant not found");
|
|
68
78
|
}
|
|
69
79
|
return rowToTenant(row2);
|
|
70
80
|
}
|
|
71
|
-
const rows = await db.update(table).set(set).where(eq(table.id,
|
|
81
|
+
const rows = await db.update(table).set(set).where(eq(table.id, tenantId2)).returning();
|
|
72
82
|
const row = rows[0];
|
|
73
83
|
if (!row || typeof row !== "object") {
|
|
74
84
|
throw new Error("better-tenant: tenant not found");
|
|
75
85
|
}
|
|
76
86
|
return rowToTenant(row);
|
|
77
87
|
},
|
|
78
|
-
async getById(
|
|
79
|
-
const rows = await db.select().from(table).where(eq(table.id,
|
|
88
|
+
async getById(tenantId2) {
|
|
89
|
+
const rows = await db.select().from(table).where(eq(table.id, tenantId2)).limit(1);
|
|
90
|
+
const row = Array.isArray(rows) ? rows[0] : void 0;
|
|
91
|
+
if (!row || typeof row !== "object") {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return rowToTenant(row);
|
|
95
|
+
},
|
|
96
|
+
async getBySlug(slug) {
|
|
97
|
+
const rows = await db.select().from(table).where(eq(table.slug, slug)).limit(1);
|
|
80
98
|
const row = Array.isArray(rows) ? rows[0] : void 0;
|
|
81
99
|
if (!row || typeof row !== "object") {
|
|
82
100
|
return null;
|
|
@@ -84,22 +102,30 @@ function createGetTenantRepository(table) {
|
|
|
84
102
|
return rowToTenant(row);
|
|
85
103
|
},
|
|
86
104
|
async list(options = {}) {
|
|
87
|
-
const limit =
|
|
105
|
+
const limit = options.limit ?? 50;
|
|
88
106
|
const offset = Math.max(0, options.offset ?? 0);
|
|
89
107
|
const rows = await db.select().from(table).limit(limit).offset(offset);
|
|
90
108
|
return (Array.isArray(rows) ? rows : []).map(
|
|
91
109
|
(r) => rowToTenant(r)
|
|
92
110
|
);
|
|
93
111
|
},
|
|
94
|
-
async delete(
|
|
95
|
-
await db.delete(table).where(eq(table.id,
|
|
112
|
+
async delete(tenantId2) {
|
|
113
|
+
await db.delete(table).where(eq(table.id, tenantId2));
|
|
96
114
|
}
|
|
97
115
|
};
|
|
98
116
|
};
|
|
99
117
|
}
|
|
118
|
+
|
|
119
|
+
// src/database.ts
|
|
120
|
+
function drizzleDatabase(db, options) {
|
|
121
|
+
return {
|
|
122
|
+
adapter: drizzleAdapter(db),
|
|
123
|
+
getTenantRepository: createGetTenantRepository(options?.table)
|
|
124
|
+
};
|
|
125
|
+
}
|
|
100
126
|
export {
|
|
101
|
-
|
|
102
|
-
|
|
127
|
+
drizzleDatabase,
|
|
128
|
+
tenantId,
|
|
103
129
|
tenantsTable
|
|
104
130
|
};
|
|
105
131
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/adapter.ts","../src/schema.ts","../src/repository.ts"],"sourcesContent":["import { sql } from \"drizzle-orm\";\nimport type { TenantAdapter } from \"@usebetterdev/tenant-core\";\n\n/**\n * Minimal shape for a Drizzle pg transaction: must support execute(sql).\n * The actual Drizzle transaction extends this with insert, select, etc.\n */\nexport interface DrizzlePgTransaction {\n execute(query: ReturnType<typeof sql> | unknown): Promise<unknown>;\n}\n\n/**\n * Minimal shape for a Drizzle pg database: must support transaction().\n * Generic over TTx so the full transaction type (with insert, select, etc.) is preserved.\n */\nexport interface DrizzlePgDatabase<TTx extends DrizzlePgTransaction = DrizzlePgTransaction> {\n transaction<T>(\n callback: (tx: TTx) => Promise<T>,\n ): Promise<T>;\n}\n\n/**\n * Creates a TenantAdapter for Drizzle (PostgreSQL).\n *\n * Uses a transaction per runWithTenant: BEGIN → SET LOCAL app.current_tenant = tenantId → fn(tx) → COMMIT.\n * The handle passed to fn is the transaction runner; use only this handle for tenant-scoped queries so RLS applies.\n *\n * runAsSystem runs in a transaction with SET LOCAL app.bypass_rls = true so RLS policies that allow\n * when current_setting('app.bypass_rls', true) = 'true' permit access (CLI generates this bypass policy).\n *\n * @param database - Drizzle pg database (from drizzle(pool) or similar)\n * @returns TenantAdapter implementation\n */\nexport function drizzleAdapter<TTx extends DrizzlePgTransaction>(\n database: DrizzlePgDatabase<TTx>,\n): TenantAdapter<TTx, TTx> {\n return {\n async runWithTenant<T>(\n tenantId: string,\n fn: (database: TTx) => Promise<T>,\n ): Promise<T> {\n return database.transaction(async (tx) => {\n await tx.execute(\n sql`SELECT set_config('app.current_tenant', ${tenantId}, true)`,\n );\n return fn(tx);\n });\n },\n\n async runAsSystem<T>(\n fn: (database: TTx) => Promise<T>,\n ): Promise<T> {\n return database.transaction(async (tx) => {\n await tx.execute(\n sql`SELECT set_config('app.bypass_rls', 'true', true)`,\n );\n return fn(tx);\n });\n },\n };\n}\n","import { pgTable, text, timestamp, uuid } from \"drizzle-orm/pg-core\";\n\n/**\n * Standard tenants table schema matching the CLI-generated shape.\n * Columns: id (UUID), name, slug, created_at.\n * Use this table or provide your own with the same column contract for createGetTenantRepository.\n */\nexport const tenantsTable = pgTable(\"tenants\", {\n id: uuid(\"id\").primaryKey().defaultRandom(),\n name: text(\"name\").notNull(),\n slug: text(\"slug\").notNull().unique(),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).defaultNow().notNull(),\n});\n","import { eq } from \"drizzle-orm\";\nimport type { Tenant, TenantRepository } from \"@usebetterdev/tenant-core\";\nimport type { tenantsTable } from \"./schema.js\";\n\n/** Table shape expected by createGetTenantRepository: id, name, slug, createdAt (or created_at). */\nexport type TenantsTableLike = typeof tenantsTable;\n\nfunction rowToTenant(row: Record<string, unknown>): Tenant {\n return {\n id: String(row.id),\n name: String(row.name),\n slug: String(row.slug),\n createdAt: (row.createdAt ?? row.created_at) as Date | string,\n ...row,\n };\n}\n\n/**\n * Creates getTenantRepository for use with betterTenant({ getTenantRepository }).\n * Pass the tenants table (e.g. tenantsTable from this package or your own with id, name, slug, created_at).\n * The returned function is called by core with the system database handle (from adapter.runAsSystem).\n */\nexport function createGetTenantRepository(\n table: TenantsTableLike,\n): (database: unknown) => TenantRepository {\n return (database: unknown) => {\n const db = database as {\n insert: (t: TenantsTableLike) => {\n values: (v: { name: string; slug: string }) => {\n returning: () => Promise<unknown[]>;\n };\n };\n update: (t: TenantsTableLike) => {\n set: (v: Partial<{ name: string; slug: string }>) => {\n where: (c: unknown) => { returning: () => Promise<unknown[]> };\n };\n };\n select: () => {\n from: (t: TenantsTableLike) => {\n where: (c: unknown) => { limit: (n: number) => Promise<unknown[]> };\n limit: (n: number) => { offset: (n: number) => Promise<unknown[]> };\n };\n };\n delete: (t: TenantsTableLike) => {\n where: (c: unknown) => Promise<unknown>;\n };\n };\n\n return {\n async create(data) {\n const rows = await db\n .insert(table)\n .values({\n name: data.name,\n slug: data.slug,\n })\n .returning();\n const row = rows[0];\n if (!row || typeof row !== \"object\") {\n throw new Error(\"better-tenant: createTenant failed to return row\");\n }\n return rowToTenant(row as Record<string, unknown>);\n },\n\n async update(tenantId, data) {\n const set: Partial<{ name: string; slug: string }> = {};\n if (data.name !== undefined) set.name = data.name;\n if (data.slug !== undefined) set.slug = data.slug;\n if (Object.keys(set).length === 0) {\n const rows = await db\n .select()\n .from(table)\n .where(eq(table.id, tenantId))\n .limit(1);\n const row = Array.isArray(rows) ? rows[0] : undefined;\n if (!row || typeof row !== \"object\") {\n throw new Error(\"better-tenant: tenant not found\");\n }\n return rowToTenant(row as Record<string, unknown>);\n }\n const rows = await db\n .update(table)\n .set(set)\n .where(eq(table.id, tenantId))\n .returning();\n const row = rows[0];\n if (!row || typeof row !== \"object\") {\n throw new Error(\"better-tenant: tenant not found\");\n }\n return rowToTenant(row as Record<string, unknown>);\n },\n\n async getById(tenantId: string) {\n const rows = await db\n .select()\n .from(table)\n .where(eq(table.id, tenantId))\n .limit(1);\n const row = Array.isArray(rows) ? rows[0] : undefined;\n if (!row || typeof row !== \"object\") {\n return null;\n }\n\n return rowToTenant(row as Record<string, unknown>);\n },\n\n async list(options = {}) {\n const limit = Math.min(options.limit ?? 50, 50);\n const offset = Math.max(0, options.offset ?? 0);\n const rows = await db.select().from(table).limit(limit).offset(offset);\n return (Array.isArray(rows) ? rows : []).map((r) =>\n rowToTenant(r as Record<string, unknown>),\n );\n },\n\n async delete(tenantId) {\n await db.delete(table).where(eq(table.id, tenantId));\n },\n };\n };\n}\n"],"mappings":"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","names":["rows","row"]}
|
|
1
|
+
{"version":3,"sources":["../src/adapter.ts","../src/repository.ts","../src/schema.ts","../src/database.ts"],"sourcesContent":["import { sql } from \"drizzle-orm\";\nimport type { TenantAdapter } from \"@usebetterdev/tenant-core\";\n\n/**\n * Minimal shape for a Drizzle pg transaction: must support execute(sql).\n * The actual Drizzle transaction extends this with insert, select, etc.\n */\nexport interface DrizzlePgTransaction {\n execute(query: ReturnType<typeof sql> | unknown): Promise<unknown>;\n}\n\n/**\n * Minimal shape for a Drizzle pg database: must support transaction().\n * Generic over TTx so the full transaction type (with insert, select, etc.) is preserved.\n */\nexport interface DrizzlePgDatabase<TTx extends DrizzlePgTransaction = DrizzlePgTransaction> {\n transaction<T>(\n callback: (tx: TTx) => Promise<T>,\n ): Promise<T>;\n}\n\n/**\n * Creates a TenantAdapter for Drizzle (PostgreSQL).\n *\n * Uses a transaction per runWithTenant: BEGIN → SET LOCAL app.current_tenant = tenantId → fn(tx) → COMMIT.\n * The handle passed to fn is the transaction runner; use only this handle for tenant-scoped queries so RLS applies.\n *\n * runAsSystem runs in a transaction with SET LOCAL app.bypass_rls = true so RLS policies that allow\n * when current_setting('app.bypass_rls', true) = 'true' permit access (CLI generates this bypass policy).\n *\n * @param database - Drizzle pg database (from drizzle(pool) or similar)\n * @returns TenantAdapter implementation\n */\nexport function drizzleAdapter<TTx extends DrizzlePgTransaction>(\n database: DrizzlePgDatabase<TTx>,\n): TenantAdapter<TTx, TTx> {\n return {\n async runWithTenant<T>(\n tenantId: string,\n fn: (database: TTx) => Promise<T>,\n ): Promise<T> {\n return database.transaction(async (tx) => {\n await tx.execute(\n sql`SELECT set_config('app.current_tenant', ${tenantId}, true)`,\n );\n return fn(tx);\n });\n },\n\n async runAsSystem<T>(\n fn: (database: TTx) => Promise<T>,\n ): Promise<T> {\n return database.transaction(async (tx) => {\n await tx.execute(\n sql`SELECT set_config('app.bypass_rls', 'true', true)`,\n );\n return fn(tx);\n });\n },\n };\n}\n","import { eq } from \"drizzle-orm\";\nimport type { Tenant, TenantRepository } from \"@usebetterdev/tenant-core\";\nimport { tenantsTable } from \"./schema.js\";\n\n/** Table shape expected by createGetTenantRepository: id, name, slug, createdAt (or created_at). */\nexport type TenantsTableLike = typeof tenantsTable;\n\nfunction rowToTenant(row: Record<string, unknown>): Tenant {\n const createdAt = row.createdAt ?? row.created_at;\n if (createdAt === undefined || createdAt === null) {\n throw new Error(\"better-tenant: row missing created_at / createdAt\");\n }\n return {\n id: String(row.id),\n name: String(row.name),\n slug: String(row.slug),\n createdAt: createdAt as Date | string,\n ...row,\n };\n}\n\n/**\n * Creates getTenantRepository for use with betterTenant({ getTenantRepository }).\n * Pass the tenants table (e.g. tenantsTable from this package or your own with id, name, slug, created_at).\n * The returned function is called by core with the system database handle (from adapter.runAsSystem).\n */\nexport function createGetTenantRepository(\n table: TenantsTableLike = tenantsTable,\n): (database: unknown) => TenantRepository {\n return (database: unknown) => {\n const db = database as {\n insert: (t: TenantsTableLike) => {\n values: (v: { name: string; slug: string }) => {\n returning: () => Promise<unknown[]>;\n };\n };\n update: (t: TenantsTableLike) => {\n set: (v: Partial<{ name: string; slug: string }>) => {\n where: (c: unknown) => { returning: () => Promise<unknown[]> };\n };\n };\n select: () => {\n from: (t: TenantsTableLike) => {\n where: (c: unknown) => { limit: (n: number) => Promise<unknown[]> };\n limit: (n: number) => { offset: (n: number) => Promise<unknown[]> };\n };\n };\n delete: (t: TenantsTableLike) => {\n where: (c: unknown) => Promise<unknown>;\n };\n };\n\n return {\n async create(data) {\n const rows = await db\n .insert(table)\n .values({\n name: data.name,\n slug: data.slug,\n })\n .returning();\n const row = rows[0];\n if (!row || typeof row !== \"object\") {\n throw new Error(\"better-tenant: createTenant failed to return row\");\n }\n return rowToTenant(row as Record<string, unknown>);\n },\n\n async update(tenantId, data) {\n const set: Partial<{ name: string; slug: string }> = {};\n if (data.name !== undefined) set.name = data.name;\n if (data.slug !== undefined) set.slug = data.slug;\n if (Object.keys(set).length === 0) {\n const rows = await db\n .select()\n .from(table)\n .where(eq(table.id, tenantId))\n .limit(1);\n const row = Array.isArray(rows) ? rows[0] : undefined;\n if (!row || typeof row !== \"object\") {\n throw new Error(\"better-tenant: tenant not found\");\n }\n return rowToTenant(row as Record<string, unknown>);\n }\n const rows = await db\n .update(table)\n .set(set)\n .where(eq(table.id, tenantId))\n .returning();\n const row = rows[0];\n if (!row || typeof row !== \"object\") {\n throw new Error(\"better-tenant: tenant not found\");\n }\n return rowToTenant(row as Record<string, unknown>);\n },\n\n async getById(tenantId: string) {\n const rows = await db\n .select()\n .from(table)\n .where(eq(table.id, tenantId))\n .limit(1);\n const row = Array.isArray(rows) ? rows[0] : undefined;\n if (!row || typeof row !== \"object\") {\n return null;\n }\n\n return rowToTenant(row as Record<string, unknown>);\n },\n\n async getBySlug(slug: string) {\n const rows = await db\n .select()\n .from(table)\n .where(eq(table.slug, slug))\n .limit(1);\n const row = Array.isArray(rows) ? rows[0] : undefined;\n if (!row || typeof row !== \"object\") {\n return null;\n }\n return rowToTenant(row as Record<string, unknown>);\n },\n\n async list(options = {}) {\n const limit = options.limit ?? 50;\n const offset = Math.max(0, options.offset ?? 0);\n const rows = await db.select().from(table).limit(limit).offset(offset);\n return (Array.isArray(rows) ? rows : []).map((r) =>\n rowToTenant(r as Record<string, unknown>),\n );\n },\n\n async delete(tenantId) {\n await db.delete(table).where(eq(table.id, tenantId));\n },\n };\n };\n}\n","import { sql } from \"drizzle-orm\";\nimport { pgTable, text, timestamp, uuid } from \"drizzle-orm/pg-core\";\n\n/**\n * Standard tenants table schema matching the CLI-generated shape.\n * Columns: id (UUID), name, slug, created_at.\n * Use this table or provide your own with the same column contract for createGetTenantRepository.\n */\nexport const tenantsTable = pgTable(\"tenants\", {\n id: uuid(\"id\").primaryKey().defaultRandom(),\n name: text(\"name\").notNull(),\n slug: text(\"slug\").notNull().unique(),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).defaultNow().notNull(),\n}).enableRLS();\n\n/**\n * Pre-configured tenant_id column for use in tenant-scoped tables.\n * References tenants.id and auto-populates from the current session variable.\n *\n * Usage:\n * ```ts\n * import { tenantId } from \"@usebetterdev/tenant/drizzle\";\n *\n * export const projectsTable = pgTable(\"projects\", {\n * id: serial(\"id\").primaryKey(),\n * name: text(\"name\").notNull(),\n * ...tenantId,\n * }).enableRLS();\n * ```\n */\nexport const tenantId = {\n tenantId: uuid(\"tenant_id\")\n .notNull()\n .references(() => tenantsTable.id)\n .default(sql`(current_setting('app.current_tenant', true))::uuid`),\n} as const;\n","import type { DatabaseProvider } from \"@usebetterdev/tenant-core\";\nimport { drizzleAdapter } from \"./adapter.js\";\nimport type { DrizzlePgDatabase, DrizzlePgTransaction } from \"./adapter.js\";\nimport { createGetTenantRepository, type TenantsTableLike } from \"./repository.js\";\n\n/**\n * Creates a DatabaseProvider for Drizzle (PostgreSQL).\n *\n * Bundles drizzleAdapter + createGetTenantRepository into a single config value\n * for use with `betterTenant({ database: drizzleDatabase(db) })`.\n *\n * @param db - Drizzle pg database (from drizzle(pool) or similar)\n * @param options - Optional: custom tenants table\n */\nexport function drizzleDatabase<TTx extends DrizzlePgTransaction>(\n db: DrizzlePgDatabase<TTx>,\n options?: { table?: TenantsTableLike },\n): DatabaseProvider<TTx, TTx> {\n return {\n adapter: drizzleAdapter(db),\n getTenantRepository: createGetTenantRepository(options?.table),\n };\n}\n"],"mappings":"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","names":["tenantId","sql","tenantId","rows","row"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usebetterdev/tenant-drizzle",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1-beta.1",
|
|
4
4
|
"repository": "github:usebetter-dev/usebetter",
|
|
5
5
|
"bugs": "https://github.com/usebetter-dev/usebetter/issues",
|
|
6
6
|
"homepage": "https://github.com/usebetter-dev/usebetter#readme",
|
|
@@ -25,14 +25,18 @@
|
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"drizzle-orm": "^0.36.0",
|
|
28
|
-
"@usebetterdev/tenant-core": "0.2.
|
|
28
|
+
"@usebetterdev/tenant-core": "0.2.1-beta.1"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"pg": ">=8.0.0"
|
|
31
|
+
"pg": ">=8.0.0",
|
|
32
|
+
"postgres": ">=3.0.0"
|
|
32
33
|
},
|
|
33
34
|
"peerDependenciesMeta": {
|
|
34
35
|
"pg": {
|
|
35
|
-
"optional":
|
|
36
|
+
"optional": true
|
|
37
|
+
},
|
|
38
|
+
"postgres": {
|
|
39
|
+
"optional": true
|
|
36
40
|
}
|
|
37
41
|
},
|
|
38
42
|
"devDependencies": {
|
|
@@ -43,7 +47,7 @@
|
|
|
43
47
|
"tsup": "^8.3.5",
|
|
44
48
|
"typescript": "~5.7.2",
|
|
45
49
|
"vitest": "^2.1.6",
|
|
46
|
-
"@usebetterdev/test-utils": "0.1.
|
|
50
|
+
"@usebetterdev/test-utils": "0.2.1-beta.1"
|
|
47
51
|
},
|
|
48
52
|
"engines": {
|
|
49
53
|
"node": ">=22"
|