@usebetterdev/tenant-drizzle 0.2.0-beta.11 → 0.2.0-beta.12

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 CHANGED
@@ -13,34 +13,43 @@ 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
- adapter,
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
 
30
+ `drizzleDatabase(db)` bundles `drizzleAdapter` and `createGetTenantRepository` into a single `DatabaseProvider`. If you use a custom tenants table, construct the provider manually:
31
+
32
+ ```ts
33
+ import { drizzleAdapter, createGetTenantRepository } from "@usebetterdev/tenant/drizzle";
34
+
35
+ const tenant = betterTenant({
36
+ database: {
37
+ adapter: drizzleAdapter(db),
38
+ getTenantRepository: createGetTenantRepository(myCustomTenantsTable),
39
+ },
40
+ tenantResolver: { header: "x-tenant-id" },
41
+ });
42
+ ```
43
+
44
+ The `getTenantRepository` is required in the `database` provider. If you construct the provider manually instead of using `drizzleDatabase()`, you must include it.
45
+
37
46
  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 to `createGetTenantRepository`.
38
47
 
39
48
  ## Behaviour
40
49
 
41
50
  - **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
51
  - **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** — When `getTenantRepository` is set, `runWithTenantAndDatabase` loads the full Tenant row by default and sets `getContext().tenant`. Set `loadTenant: false` to opt out (saves one query per request).
52
+ - **loadTenant** — The full Tenant row is loaded by default into `getContext().tenant`. Set `loadTenant: false` to opt out (saves one query per request).
44
53
  - The handle passed to `fn` is the same type as your Drizzle db but transaction-scoped (full Drizzle API on `tx`).
45
54
 
46
55
  ## Testing
package/dist/index.cjs CHANGED
@@ -22,6 +22,7 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  createGetTenantRepository: () => createGetTenantRepository,
24
24
  drizzleAdapter: () => drizzleAdapter,
25
+ drizzleDatabase: () => drizzleDatabase,
25
26
  tenantsTable: () => tenantsTable
26
27
  });
27
28
  module.exports = __toCommonJS(index_exports);
@@ -49,6 +50,9 @@ function drizzleAdapter(database) {
49
50
  };
50
51
  }
51
52
 
53
+ // src/repository.ts
54
+ var import_drizzle_orm2 = require("drizzle-orm");
55
+
52
56
  // src/schema.ts
53
57
  var import_pg_core = require("drizzle-orm/pg-core");
54
58
  var tenantsTable = (0, import_pg_core.pgTable)("tenants", {
@@ -59,17 +63,20 @@ var tenantsTable = (0, import_pg_core.pgTable)("tenants", {
59
63
  });
60
64
 
61
65
  // src/repository.ts
62
- var import_drizzle_orm2 = require("drizzle-orm");
63
66
  function rowToTenant(row) {
67
+ const createdAt = row.createdAt ?? row.created_at;
68
+ if (createdAt === void 0 || createdAt === null) {
69
+ throw new Error("better-tenant: row missing created_at / createdAt");
70
+ }
64
71
  return {
65
72
  id: String(row.id),
66
73
  name: String(row.name),
67
74
  slug: String(row.slug),
68
- createdAt: row.createdAt ?? row.created_at,
75
+ createdAt,
69
76
  ...row
70
77
  };
71
78
  }
72
- function createGetTenantRepository(table) {
79
+ function createGetTenantRepository(table = tenantsTable) {
73
80
  return (database) => {
74
81
  const db = database;
75
82
  return {
@@ -111,6 +118,14 @@ function createGetTenantRepository(table) {
111
118
  }
112
119
  return rowToTenant(row);
113
120
  },
121
+ async getBySlug(slug) {
122
+ const rows = await db.select().from(table).where((0, import_drizzle_orm2.eq)(table.slug, slug)).limit(1);
123
+ const row = Array.isArray(rows) ? rows[0] : void 0;
124
+ if (!row || typeof row !== "object") {
125
+ return null;
126
+ }
127
+ return rowToTenant(row);
128
+ },
114
129
  async list(options = {}) {
115
130
  const limit = Math.min(options.limit ?? 50, 50);
116
131
  const offset = Math.max(0, options.offset ?? 0);
@@ -125,10 +140,19 @@ function createGetTenantRepository(table) {
125
140
  };
126
141
  };
127
142
  }
143
+
144
+ // src/database.ts
145
+ function drizzleDatabase(db, options) {
146
+ return {
147
+ adapter: drizzleAdapter(db),
148
+ getTenantRepository: createGetTenantRepository(options?.table)
149
+ };
150
+ }
128
151
  // Annotate the CommonJS export names for ESM import in node:
129
152
  0 && (module.exports = {
130
153
  createGetTenantRepository,
131
154
  drizzleAdapter,
155
+ drizzleDatabase,
132
156
  tenantsTable
133
157
  });
134
158
  //# sourceMappingURL=index.cjs.map
@@ -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 { drizzleAdapter } from \"./adapter.js\";\nexport type { DrizzlePgDatabase, DrizzlePgTransaction } from \"./adapter.js\";\nexport { drizzleDatabase } from \"./database.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 { 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 = 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","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 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":["import_drizzle_orm","rows","row"]}
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { sql } from 'drizzle-orm';
2
- import { TenantAdapter, TenantRepository } from '@usebetterdev/tenant-core';
2
+ import { TenantAdapter, TenantRepository, DatabaseProvider } from '@usebetterdev/tenant-core';
3
3
  import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
4
4
 
5
5
  /**
@@ -118,6 +118,19 @@ type TenantsTableLike = typeof tenantsTable;
118
118
  * Pass the tenants table (e.g. tenantsTable from this package or your own with id, name, slug, created_at).
119
119
  * The returned function is called by core with the system database handle (from adapter.runAsSystem).
120
120
  */
121
- declare function createGetTenantRepository(table: TenantsTableLike): (database: unknown) => TenantRepository;
121
+ declare function createGetTenantRepository(table?: TenantsTableLike): (database: unknown) => TenantRepository;
122
122
 
123
- export { type DrizzlePgDatabase, type DrizzlePgTransaction, type TenantsTableLike, createGetTenantRepository, drizzleAdapter, tenantsTable };
123
+ /**
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
131
+ */
132
+ declare function drizzleDatabase<TTx extends DrizzlePgTransaction>(db: DrizzlePgDatabase<TTx>, options?: {
133
+ table?: TenantsTableLike;
134
+ }): DatabaseProvider<TTx, TTx>;
135
+
136
+ export { type DrizzlePgDatabase, type DrizzlePgTransaction, type TenantsTableLike, createGetTenantRepository, drizzleAdapter, drizzleDatabase, tenantsTable };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { sql } from 'drizzle-orm';
2
- import { TenantAdapter, TenantRepository } from '@usebetterdev/tenant-core';
2
+ import { TenantAdapter, TenantRepository, DatabaseProvider } from '@usebetterdev/tenant-core';
3
3
  import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
4
4
 
5
5
  /**
@@ -118,6 +118,19 @@ type TenantsTableLike = typeof tenantsTable;
118
118
  * Pass the tenants table (e.g. tenantsTable from this package or your own with id, name, slug, created_at).
119
119
  * The returned function is called by core with the system database handle (from adapter.runAsSystem).
120
120
  */
121
- declare function createGetTenantRepository(table: TenantsTableLike): (database: unknown) => TenantRepository;
121
+ declare function createGetTenantRepository(table?: TenantsTableLike): (database: unknown) => TenantRepository;
122
122
 
123
- export { type DrizzlePgDatabase, type DrizzlePgTransaction, type TenantsTableLike, createGetTenantRepository, drizzleAdapter, tenantsTable };
123
+ /**
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
131
+ */
132
+ declare function drizzleDatabase<TTx extends DrizzlePgTransaction>(db: DrizzlePgDatabase<TTx>, options?: {
133
+ table?: TenantsTableLike;
134
+ }): DatabaseProvider<TTx, TTx>;
135
+
136
+ export { type DrizzlePgDatabase, type DrizzlePgTransaction, type TenantsTableLike, createGetTenantRepository, drizzleAdapter, drizzleDatabase, tenantsTable };
package/dist/index.js CHANGED
@@ -21,6 +21,9 @@ 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
25
28
  import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
26
29
  var tenantsTable = pgTable("tenants", {
@@ -31,17 +34,20 @@ var tenantsTable = pgTable("tenants", {
31
34
  });
32
35
 
33
36
  // src/repository.ts
34
- import { eq } from "drizzle-orm";
35
37
  function rowToTenant(row) {
38
+ const createdAt = row.createdAt ?? row.created_at;
39
+ if (createdAt === void 0 || createdAt === null) {
40
+ throw new Error("better-tenant: row missing created_at / createdAt");
41
+ }
36
42
  return {
37
43
  id: String(row.id),
38
44
  name: String(row.name),
39
45
  slug: String(row.slug),
40
- createdAt: row.createdAt ?? row.created_at,
46
+ createdAt,
41
47
  ...row
42
48
  };
43
49
  }
44
- function createGetTenantRepository(table) {
50
+ function createGetTenantRepository(table = tenantsTable) {
45
51
  return (database) => {
46
52
  const db = database;
47
53
  return {
@@ -83,6 +89,14 @@ function createGetTenantRepository(table) {
83
89
  }
84
90
  return rowToTenant(row);
85
91
  },
92
+ async getBySlug(slug) {
93
+ const rows = await db.select().from(table).where(eq(table.slug, slug)).limit(1);
94
+ const row = Array.isArray(rows) ? rows[0] : void 0;
95
+ if (!row || typeof row !== "object") {
96
+ return null;
97
+ }
98
+ return rowToTenant(row);
99
+ },
86
100
  async list(options = {}) {
87
101
  const limit = Math.min(options.limit ?? 50, 50);
88
102
  const offset = Math.max(0, options.offset ?? 0);
@@ -97,9 +111,18 @@ function createGetTenantRepository(table) {
97
111
  };
98
112
  };
99
113
  }
114
+
115
+ // src/database.ts
116
+ function drizzleDatabase(db, options) {
117
+ return {
118
+ adapter: drizzleAdapter(db),
119
+ getTenantRepository: createGetTenantRepository(options?.table)
120
+ };
121
+ }
100
122
  export {
101
123
  createGetTenantRepository,
102
124
  drizzleAdapter,
125
+ drizzleDatabase,
103
126
  tenantsTable
104
127
  };
105
128
  //# 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 = 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","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 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":["rows","row"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usebetterdev/tenant-drizzle",
3
- "version": "0.2.0-beta.11",
3
+ "version": "0.2.0-beta.12",
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,7 +25,7 @@
25
25
  ],
26
26
  "dependencies": {
27
27
  "drizzle-orm": "^0.36.0",
28
- "@usebetterdev/tenant-core": "0.2.0-beta.11"
28
+ "@usebetterdev/tenant-core": "0.2.0-beta.12"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "pg": ">=8.0.0"
@@ -43,7 +43,7 @@
43
43
  "tsup": "^8.3.5",
44
44
  "typescript": "~5.7.2",
45
45
  "vitest": "^2.1.6",
46
- "@usebetterdev/test-utils": "0.2.0-beta.11"
46
+ "@usebetterdev/test-utils": "0.2.0-beta.12"
47
47
  },
48
48
  "engines": {
49
49
  "node": ">=22"