@usebetterdev/tenant-prisma 0.2.1-beta.1 → 0.3.0-beta.3

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
@@ -5,17 +5,19 @@ Prisma adapter for [@usebetterdev/tenant](https://github.com/usebetter-dev/usebe
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- pnpm add @usebetterdev/tenant @prisma/client
8
+ pnpm add @usebetterdev/tenant @prisma/client @prisma/adapter-pg
9
9
  ```
10
10
 
11
11
  ## Usage
12
12
 
13
13
  ```ts
14
- import { PrismaClient } from "@prisma/client";
14
+ import { PrismaClient } from "./generated/prisma/client.js";
15
+ import { PrismaPg } from "@prisma/adapter-pg";
15
16
  import { betterTenant } from "@usebetterdev/tenant";
16
17
  import { prismaDatabase } from "@usebetterdev/tenant/prisma";
17
18
 
18
- const prisma = new PrismaClient();
19
+ const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
20
+ const prisma = new PrismaClient({ adapter });
19
21
 
20
22
  const tenant = betterTenant({
21
23
  database: prismaDatabase(prisma),
@@ -52,6 +54,11 @@ Integration tests use [@testcontainers/postgresql](https://www.npmjs.com/package
52
54
 
53
55
  **Run integration tests:** From the repo root, run `pnpm run test:integration` or `pnpm run test --filter @usebetterdev/tenant-prisma`.
54
56
 
55
- ## Peer dependency
57
+ ## Peer dependencies
56
58
 
57
- Requires `@prisma/client` (>= 5.0.0). Install it in your app when using this adapter.
59
+ Requires Prisma 7+:
60
+
61
+ - `@prisma/client` (>= 7.0.0)
62
+ - `@prisma/adapter-pg` (>= 7.0.0)
63
+
64
+ Prisma 7 uses the `prisma-client` generator with a required `output` path. Adjust the `PrismaClient` import to match your `output` configuration in `schema.prisma`.
package/dist/index.cjs CHANGED
@@ -52,8 +52,7 @@ function rowToTenant(row) {
52
52
  id: String(row.id),
53
53
  name: String(row.name),
54
54
  slug: String(row.slug),
55
- createdAt,
56
- ...row
55
+ createdAt
57
56
  };
58
57
  }
59
58
  function assertSafeIdentifier(name) {
@@ -69,8 +68,10 @@ function createGetTenantRepository(tableName = "tenants") {
69
68
  const tx = database;
70
69
  return {
71
70
  async create(data) {
71
+ const id = crypto.randomUUID();
72
72
  const rows = await tx.$queryRawUnsafe(
73
- `INSERT INTO ${tableName} (name, slug) VALUES ($1, $2) RETURNING *`,
73
+ `INSERT INTO ${tableName} (id, name, slug) VALUES ($1::uuid, $2, $3) RETURNING *`,
74
+ id,
74
75
  data.name,
75
76
  data.slug
76
77
  );
@@ -151,6 +152,13 @@ function createGetTenantRepository(tableName = "tenants") {
151
152
  `DELETE FROM ${tableName} WHERE id = $1::uuid`,
152
153
  tenantId
153
154
  );
155
+ },
156
+ async count() {
157
+ const rows = await tx.$queryRawUnsafe(
158
+ `SELECT count(*)::int AS count FROM ${tableName}`
159
+ );
160
+ const row = rows[0];
161
+ return Number(row?.count ?? 0);
154
162
  }
155
163
  };
156
164
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/adapter.ts","../src/repository.ts","../src/database.ts"],"sourcesContent":["export { prismaDatabase } from \"./database.js\";\nexport type { PrismaClientLike, PrismaTransactionClient } from \"./types.js\";\n","import type { TenantAdapter } from \"@usebetterdev/tenant-core\";\nimport type { PrismaClientLike, PrismaTransactionClient } from \"./types.js\";\n\n/**\n * Creates a TenantAdapter for Prisma (PostgreSQL).\n *\n * Uses an interactive transaction per runWithTenant:\n * BEGIN → SET LOCAL app.current_tenant = tenantId → fn(tx) → COMMIT.\n *\n * The handle passed to fn is the Prisma transaction client; use only this\n * handle for tenant-scoped queries so RLS applies.\n *\n * runAsSystem runs in a transaction with SET LOCAL app.bypass_rls = 'true'\n * so RLS policies that allow when current_setting('app.bypass_rls', true) = 'true'\n * permit access (CLI generates this bypass policy).\n *\n * @param prisma - PrismaClient instance (or anything matching PrismaClientLike)\n * @returns TenantAdapter implementation\n */\nexport function prismaAdapter(\n prisma: PrismaClientLike,\n): TenantAdapter<PrismaTransactionClient, PrismaTransactionClient> {\n return {\n async runWithTenant<T>(\n tenantId: string,\n fn: (database: PrismaTransactionClient) => Promise<T>,\n ): Promise<T> {\n return prisma.$transaction(async (tx: PrismaTransactionClient) => {\n await tx.$executeRaw`SELECT set_config('app.current_tenant', ${tenantId}, true)`;\n return fn(tx);\n });\n },\n\n async runAsSystem<T>(\n fn: (database: PrismaTransactionClient) => Promise<T>,\n ): Promise<T> {\n return prisma.$transaction(async (tx: PrismaTransactionClient) => {\n await tx.$executeRaw`SELECT set_config('app.bypass_rls', 'true', true)`;\n return fn(tx);\n });\n },\n };\n}\n","import type { Tenant, TenantRepository } from \"@usebetterdev/tenant-core\";\nimport type { PrismaTransactionClient } from \"./types.js\";\n\n/**\n * Converts a raw DB row to the Tenant shape expected by core.\n */\nfunction rowToTenant(row: Record<string, unknown>): Tenant {\n const createdAt = row.created_at ?? row.createdAt;\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 * Validate table name to prevent SQL injection. Only allows [a-zA-Z_][a-zA-Z0-9_]*.\n */\nfunction assertSafeIdentifier(name: string): void {\n if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {\n throw new Error(\n `better-tenant: invalid table name \"${name}\" — must be [a-zA-Z_][a-zA-Z0-9_]*`,\n );\n }\n}\n\n/**\n * Creates getTenantRepository for use with betterTenant({ getTenantRepository }).\n *\n * Uses raw SQL ($queryRawUnsafe / $executeRawUnsafe) on the Prisma transaction\n * client so the adapter works without requiring a generated `Tenant` model in\n * the user's Prisma schema. The tenants table must match the CLI-generated shape\n * (id UUID, name TEXT, slug TEXT, created_at TIMESTAMPTZ).\n *\n * Table name is validated at creation time to prevent injection — only\n * alphanumeric + underscore identifiers are accepted.\n *\n * @param tableName - SQL table name (default: \"tenants\")\n */\nexport function createGetTenantRepository(\n tableName = \"tenants\",\n): (database: PrismaTransactionClient) => TenantRepository {\n assertSafeIdentifier(tableName);\n\n return (database: PrismaTransactionClient) => {\n const tx = database;\n\n return {\n async create(data) {\n const rows = await tx.$queryRawUnsafe<Record<string, unknown>[]>(\n `INSERT INTO ${tableName} (name, slug) VALUES ($1, $2) RETURNING *`,\n data.name,\n data.slug,\n );\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);\n },\n\n async getById(tenantId: string) {\n const rows = await tx.$queryRawUnsafe<Record<string, unknown>[]>(\n `SELECT * FROM ${tableName} WHERE id = $1::uuid LIMIT 1`,\n tenantId,\n );\n const row = rows[0];\n if (!row || typeof row !== \"object\") {\n return null;\n }\n return rowToTenant(row);\n },\n\n async getBySlug(slug: string) {\n const rows = await tx.$queryRawUnsafe<Record<string, unknown>[]>(\n `SELECT * FROM ${tableName} WHERE slug = $1 LIMIT 1`,\n slug,\n );\n const row = rows[0];\n if (!row || typeof row !== \"object\") {\n return null;\n }\n return rowToTenant(row);\n },\n\n async update(tenantId, data) {\n const setClauses: string[] = [];\n const values: unknown[] = [];\n\n if (data.name !== undefined) {\n values.push(data.name);\n setClauses.push(`name = $${String(values.length)}`);\n }\n if (data.slug !== undefined) {\n values.push(data.slug);\n setClauses.push(`slug = $${String(values.length)}`);\n }\n\n if (setClauses.length === 0) {\n const rows = await tx.$queryRawUnsafe<Record<string, unknown>[]>(\n `SELECT * FROM ${tableName} WHERE id = $1::uuid LIMIT 1`,\n tenantId,\n );\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);\n }\n\n values.push(tenantId);\n const query = `UPDATE ${tableName} SET ${setClauses.join(\", \")} WHERE id = $${String(values.length)}::uuid RETURNING *`;\n\n const rows = await tx.$queryRawUnsafe<Record<string, unknown>[]>(\n query,\n ...values,\n );\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);\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 tx.$queryRawUnsafe<Record<string, unknown>[]>(\n `SELECT * FROM ${tableName} ORDER BY created_at ASC LIMIT $1 OFFSET $2`,\n limit,\n offset,\n );\n return rows.map(rowToTenant);\n },\n\n async delete(tenantId) {\n await tx.$executeRawUnsafe(\n `DELETE FROM ${tableName} WHERE id = $1::uuid`,\n tenantId,\n );\n },\n };\n };\n}\n","import type { DatabaseProvider } from \"@usebetterdev/tenant-core\";\nimport { prismaAdapter } from \"./adapter.js\";\nimport type { PrismaClientLike, PrismaTransactionClient } from \"./types.js\";\nimport { createGetTenantRepository } from \"./repository.js\";\n\n/**\n * Creates a DatabaseProvider for Prisma (PostgreSQL).\n *\n * Bundles prismaAdapter + createGetTenantRepository into a single config value\n * for use with `betterTenant({ database: prismaDatabase(prisma) })`.\n *\n * @param prisma - PrismaClient instance (or anything matching PrismaClientLike)\n * @param options - Optional: custom table name (default: \"tenants\")\n */\nexport function prismaDatabase(\n prisma: PrismaClientLike,\n options?: { tableName?: string },\n): DatabaseProvider<PrismaTransactionClient, PrismaTransactionClient> {\n return {\n adapter: prismaAdapter(prisma),\n getTenantRepository: createGetTenantRepository(options?.tableName),\n };\n}\n"],"mappings":"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","names":["rows","row"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/adapter.ts","../src/repository.ts","../src/database.ts"],"sourcesContent":["export { prismaDatabase } from \"./database.js\";\nexport type {\n PrismaClientLike,\n PrismaTransactionClient,\n WithOptionalTenant,\n} from \"./types.js\";\n","import type { TenantAdapter } from \"@usebetterdev/tenant-core\";\nimport type { PrismaClientLike, PrismaTransactionClient } from \"./types.js\";\n\n/**\n * Creates a TenantAdapter for Prisma (PostgreSQL).\n *\n * Uses an interactive transaction per runWithTenant:\n * BEGIN → SET LOCAL app.current_tenant = tenantId → fn(tx) → COMMIT.\n *\n * The handle passed to fn is the Prisma transaction client; use only this\n * handle for tenant-scoped queries so RLS applies.\n *\n * runAsSystem runs in a transaction with SET LOCAL app.bypass_rls = 'true'\n * so RLS policies that allow when current_setting('app.bypass_rls', true) = 'true'\n * permit access (CLI generates this bypass policy).\n *\n * Generic `TxClient` flows from `PrismaClientLike<TxClient>` so\n * `getDatabase()` returns the user's full transaction client type.\n *\n * @param prisma - PrismaClient instance (or anything matching PrismaClientLike)\n * @returns TenantAdapter implementation\n */\nexport function prismaAdapter<\n TxClient extends PrismaTransactionClient = PrismaTransactionClient,\n>(\n prisma: PrismaClientLike<TxClient>,\n): TenantAdapter<TxClient, TxClient> {\n return {\n async runWithTenant<T>(\n tenantId: string,\n fn: (database: TxClient) => Promise<T>,\n ): Promise<T> {\n return prisma.$transaction(async (tx) => {\n await tx.$executeRaw`SELECT set_config('app.current_tenant', ${tenantId}, true)`;\n return fn(tx);\n });\n },\n\n async runAsSystem<T>(\n fn: (database: TxClient) => Promise<T>,\n ): Promise<T> {\n return prisma.$transaction(async (tx) => {\n await tx.$executeRaw`SELECT set_config('app.bypass_rls', 'true', true)`;\n return fn(tx);\n });\n },\n };\n}\n","import type { Tenant, TenantRepository } from \"@usebetterdev/tenant-core\";\nimport type { PrismaTransactionClient } from \"./types.js\";\n\n/**\n * Converts a raw DB row to the Tenant shape expected by core.\n */\nfunction rowToTenant(row: Record<string, unknown>): Tenant {\n const createdAt = row.created_at ?? row.createdAt;\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 };\n}\n\n/**\n * Validate table name to prevent SQL injection. Only allows [a-zA-Z_][a-zA-Z0-9_]*.\n */\nfunction assertSafeIdentifier(name: string): void {\n if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {\n throw new Error(\n `better-tenant: invalid table name \"${name}\" — must be [a-zA-Z_][a-zA-Z0-9_]*`,\n );\n }\n}\n\n/**\n * Creates getTenantRepository for use with betterTenant({ getTenantRepository }).\n *\n * Uses raw SQL ($queryRawUnsafe / $executeRawUnsafe) on the Prisma transaction\n * client so the adapter works without requiring a generated `Tenant` model in\n * the user's Prisma schema. The tenants table must match the CLI-generated shape\n * (id UUID, name TEXT, slug TEXT, created_at TIMESTAMPTZ).\n *\n * Table name is validated at creation time to prevent injection — only\n * alphanumeric + underscore identifiers are accepted.\n *\n * @param tableName - SQL table name (default: \"tenants\")\n */\nexport function createGetTenantRepository(\n tableName = \"tenants\",\n): (database: PrismaTransactionClient) => TenantRepository {\n assertSafeIdentifier(tableName);\n\n return (database: PrismaTransactionClient) => {\n const tx = database;\n\n return {\n async create(data) {\n const id = crypto.randomUUID();\n const rows = await tx.$queryRawUnsafe<Record<string, unknown>[]>(\n `INSERT INTO ${tableName} (id, name, slug) VALUES ($1::uuid, $2, $3) RETURNING *`,\n id,\n data.name,\n data.slug,\n );\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);\n },\n\n async getById(tenantId: string) {\n const rows = await tx.$queryRawUnsafe<Record<string, unknown>[]>(\n `SELECT * FROM ${tableName} WHERE id = $1::uuid LIMIT 1`,\n tenantId,\n );\n const row = rows[0];\n if (!row || typeof row !== \"object\") {\n return null;\n }\n return rowToTenant(row);\n },\n\n async getBySlug(slug: string) {\n const rows = await tx.$queryRawUnsafe<Record<string, unknown>[]>(\n `SELECT * FROM ${tableName} WHERE slug = $1 LIMIT 1`,\n slug,\n );\n const row = rows[0];\n if (!row || typeof row !== \"object\") {\n return null;\n }\n return rowToTenant(row);\n },\n\n async update(tenantId, data) {\n const setClauses: string[] = [];\n const values: unknown[] = [];\n\n if (data.name !== undefined) {\n values.push(data.name);\n setClauses.push(`name = $${String(values.length)}`);\n }\n if (data.slug !== undefined) {\n values.push(data.slug);\n setClauses.push(`slug = $${String(values.length)}`);\n }\n\n if (setClauses.length === 0) {\n const rows = await tx.$queryRawUnsafe<Record<string, unknown>[]>(\n `SELECT * FROM ${tableName} WHERE id = $1::uuid LIMIT 1`,\n tenantId,\n );\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);\n }\n\n values.push(tenantId);\n const query = `UPDATE ${tableName} SET ${setClauses.join(\", \")} WHERE id = $${String(values.length)}::uuid RETURNING *`;\n\n const rows = await tx.$queryRawUnsafe<Record<string, unknown>[]>(\n query,\n ...values,\n );\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);\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 tx.$queryRawUnsafe<Record<string, unknown>[]>(\n `SELECT * FROM ${tableName} ORDER BY created_at ASC LIMIT $1 OFFSET $2`,\n limit,\n offset,\n );\n return rows.map(rowToTenant);\n },\n\n async delete(tenantId) {\n await tx.$executeRawUnsafe(\n `DELETE FROM ${tableName} WHERE id = $1::uuid`,\n tenantId,\n );\n },\n\n async count() {\n const rows = await tx.$queryRawUnsafe<{ count: bigint | number | string }[]>(\n `SELECT count(*)::int AS count FROM ${tableName}`,\n );\n const row = rows[0];\n return Number(row?.count ?? 0);\n },\n };\n };\n}\n","import type { DatabaseProvider } from \"@usebetterdev/tenant-core\";\nimport { prismaAdapter } from \"./adapter.js\";\nimport type { PrismaClientLike, PrismaTransactionClient } from \"./types.js\";\nimport { createGetTenantRepository } from \"./repository.js\";\n\n/**\n * Creates a DatabaseProvider for Prisma (PostgreSQL).\n *\n * Bundles prismaAdapter + createGetTenantRepository into a single config value\n * for use with `betterTenant({ database: prismaDatabase(prisma) })`.\n *\n * Generic `TxClient` is inferred from the PrismaClient's `$transaction` callback,\n * so `getDatabase()` returns the user's full transaction client type with model\n * methods (e.g. `db.project.findMany()`) instead of just raw query methods.\n *\n * @param prisma - PrismaClient instance (or anything matching PrismaClientLike)\n * @param options - Optional: custom table name (default: \"tenants\")\n */\nexport function prismaDatabase<\n TxClient extends PrismaTransactionClient = PrismaTransactionClient,\n>(\n prisma: PrismaClientLike<TxClient>,\n options?: { tableName?: string },\n): DatabaseProvider<TxClient, TxClient> {\n return {\n adapter: prismaAdapter(prisma),\n getTenantRepository: createGetTenantRepository(options?.tableName),\n };\n}\n"],"mappings":"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","names":["rows","row"]}
package/dist/index.d.cts CHANGED
@@ -16,13 +16,38 @@ type PrismaTransactionClient = {
16
16
  * Minimal PrismaClient shape: must support interactive `$transaction`.
17
17
  *
18
18
  * Matches `new PrismaClient()` — no generated model types required at build time.
19
+ *
20
+ * Generic `TxClient` allows the transaction client type to flow through
21
+ * so `getDatabase()` returns the user's full Prisma transaction client
22
+ * (with model methods like `db.project.findMany()`) instead of `unknown`.
19
23
  */
20
- type PrismaClientLike = PrismaTransactionClient & {
21
- $transaction<T>(fn: (tx: PrismaTransactionClient) => Promise<T>, options?: {
24
+ type PrismaClientLike<TxClient extends PrismaTransactionClient = PrismaTransactionClient> = PrismaTransactionClient & {
25
+ $transaction<T>(fn: (tx: TxClient) => Promise<T>, options?: {
22
26
  maxWait?: number;
23
27
  timeout?: number;
28
+ isolationLevel?: string;
24
29
  }): Promise<T>;
25
30
  };
31
+ /**
32
+ * Makes `tenantId` optional in a Prisma create/update input type.
33
+ *
34
+ * When a model has a `@relation` to a Tenant model, Prisma generates `tenantId`
35
+ * as required in `UncheckedCreateInput` regardless of `@default(dbgenerated(...))`.
36
+ * Wrap your data type with this utility to safely omit `tenantId` — the
37
+ * `set_tenant_id()` trigger fills it from the session variable at INSERT time.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * import type { WithOptionalTenant } from "@usebetterdev/tenant/prisma";
42
+ * import type { ProjectUncheckedCreateInput } from "./generated/prisma/models/Project.js";
43
+ *
44
+ * const data: WithOptionalTenant<ProjectUncheckedCreateInput> = { name: "Alpha" };
45
+ * await db.project.create({ data: data as ProjectUncheckedCreateInput });
46
+ * ```
47
+ */
48
+ type WithOptionalTenant<T> = Omit<T, "tenantId" | "tenant"> & {
49
+ tenantId?: string;
50
+ };
26
51
 
27
52
  /**
28
53
  * Creates a DatabaseProvider for Prisma (PostgreSQL).
@@ -30,11 +55,15 @@ type PrismaClientLike = PrismaTransactionClient & {
30
55
  * Bundles prismaAdapter + createGetTenantRepository into a single config value
31
56
  * for use with `betterTenant({ database: prismaDatabase(prisma) })`.
32
57
  *
58
+ * Generic `TxClient` is inferred from the PrismaClient's `$transaction` callback,
59
+ * so `getDatabase()` returns the user's full transaction client type with model
60
+ * methods (e.g. `db.project.findMany()`) instead of just raw query methods.
61
+ *
33
62
  * @param prisma - PrismaClient instance (or anything matching PrismaClientLike)
34
63
  * @param options - Optional: custom table name (default: "tenants")
35
64
  */
36
- declare function prismaDatabase(prisma: PrismaClientLike, options?: {
65
+ declare function prismaDatabase<TxClient extends PrismaTransactionClient = PrismaTransactionClient>(prisma: PrismaClientLike<TxClient>, options?: {
37
66
  tableName?: string;
38
- }): DatabaseProvider<PrismaTransactionClient, PrismaTransactionClient>;
67
+ }): DatabaseProvider<TxClient, TxClient>;
39
68
 
40
- export { type PrismaClientLike, type PrismaTransactionClient, prismaDatabase };
69
+ export { type PrismaClientLike, type PrismaTransactionClient, type WithOptionalTenant, prismaDatabase };
package/dist/index.d.ts CHANGED
@@ -16,13 +16,38 @@ type PrismaTransactionClient = {
16
16
  * Minimal PrismaClient shape: must support interactive `$transaction`.
17
17
  *
18
18
  * Matches `new PrismaClient()` — no generated model types required at build time.
19
+ *
20
+ * Generic `TxClient` allows the transaction client type to flow through
21
+ * so `getDatabase()` returns the user's full Prisma transaction client
22
+ * (with model methods like `db.project.findMany()`) instead of `unknown`.
19
23
  */
20
- type PrismaClientLike = PrismaTransactionClient & {
21
- $transaction<T>(fn: (tx: PrismaTransactionClient) => Promise<T>, options?: {
24
+ type PrismaClientLike<TxClient extends PrismaTransactionClient = PrismaTransactionClient> = PrismaTransactionClient & {
25
+ $transaction<T>(fn: (tx: TxClient) => Promise<T>, options?: {
22
26
  maxWait?: number;
23
27
  timeout?: number;
28
+ isolationLevel?: string;
24
29
  }): Promise<T>;
25
30
  };
31
+ /**
32
+ * Makes `tenantId` optional in a Prisma create/update input type.
33
+ *
34
+ * When a model has a `@relation` to a Tenant model, Prisma generates `tenantId`
35
+ * as required in `UncheckedCreateInput` regardless of `@default(dbgenerated(...))`.
36
+ * Wrap your data type with this utility to safely omit `tenantId` — the
37
+ * `set_tenant_id()` trigger fills it from the session variable at INSERT time.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * import type { WithOptionalTenant } from "@usebetterdev/tenant/prisma";
42
+ * import type { ProjectUncheckedCreateInput } from "./generated/prisma/models/Project.js";
43
+ *
44
+ * const data: WithOptionalTenant<ProjectUncheckedCreateInput> = { name: "Alpha" };
45
+ * await db.project.create({ data: data as ProjectUncheckedCreateInput });
46
+ * ```
47
+ */
48
+ type WithOptionalTenant<T> = Omit<T, "tenantId" | "tenant"> & {
49
+ tenantId?: string;
50
+ };
26
51
 
27
52
  /**
28
53
  * Creates a DatabaseProvider for Prisma (PostgreSQL).
@@ -30,11 +55,15 @@ type PrismaClientLike = PrismaTransactionClient & {
30
55
  * Bundles prismaAdapter + createGetTenantRepository into a single config value
31
56
  * for use with `betterTenant({ database: prismaDatabase(prisma) })`.
32
57
  *
58
+ * Generic `TxClient` is inferred from the PrismaClient's `$transaction` callback,
59
+ * so `getDatabase()` returns the user's full transaction client type with model
60
+ * methods (e.g. `db.project.findMany()`) instead of just raw query methods.
61
+ *
33
62
  * @param prisma - PrismaClient instance (or anything matching PrismaClientLike)
34
63
  * @param options - Optional: custom table name (default: "tenants")
35
64
  */
36
- declare function prismaDatabase(prisma: PrismaClientLike, options?: {
65
+ declare function prismaDatabase<TxClient extends PrismaTransactionClient = PrismaTransactionClient>(prisma: PrismaClientLike<TxClient>, options?: {
37
66
  tableName?: string;
38
- }): DatabaseProvider<PrismaTransactionClient, PrismaTransactionClient>;
67
+ }): DatabaseProvider<TxClient, TxClient>;
39
68
 
40
- export { type PrismaClientLike, type PrismaTransactionClient, prismaDatabase };
69
+ export { type PrismaClientLike, type PrismaTransactionClient, type WithOptionalTenant, prismaDatabase };
package/dist/index.js CHANGED
@@ -26,8 +26,7 @@ function rowToTenant(row) {
26
26
  id: String(row.id),
27
27
  name: String(row.name),
28
28
  slug: String(row.slug),
29
- createdAt,
30
- ...row
29
+ createdAt
31
30
  };
32
31
  }
33
32
  function assertSafeIdentifier(name) {
@@ -43,8 +42,10 @@ function createGetTenantRepository(tableName = "tenants") {
43
42
  const tx = database;
44
43
  return {
45
44
  async create(data) {
45
+ const id = crypto.randomUUID();
46
46
  const rows = await tx.$queryRawUnsafe(
47
- `INSERT INTO ${tableName} (name, slug) VALUES ($1, $2) RETURNING *`,
47
+ `INSERT INTO ${tableName} (id, name, slug) VALUES ($1::uuid, $2, $3) RETURNING *`,
48
+ id,
48
49
  data.name,
49
50
  data.slug
50
51
  );
@@ -125,6 +126,13 @@ function createGetTenantRepository(tableName = "tenants") {
125
126
  `DELETE FROM ${tableName} WHERE id = $1::uuid`,
126
127
  tenantId
127
128
  );
129
+ },
130
+ async count() {
131
+ const rows = await tx.$queryRawUnsafe(
132
+ `SELECT count(*)::int AS count FROM ${tableName}`
133
+ );
134
+ const row = rows[0];
135
+ return Number(row?.count ?? 0);
128
136
  }
129
137
  };
130
138
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/adapter.ts","../src/repository.ts","../src/database.ts"],"sourcesContent":["import type { TenantAdapter } from \"@usebetterdev/tenant-core\";\nimport type { PrismaClientLike, PrismaTransactionClient } from \"./types.js\";\n\n/**\n * Creates a TenantAdapter for Prisma (PostgreSQL).\n *\n * Uses an interactive transaction per runWithTenant:\n * BEGIN → SET LOCAL app.current_tenant = tenantId → fn(tx) → COMMIT.\n *\n * The handle passed to fn is the Prisma transaction client; use only this\n * handle for tenant-scoped queries so RLS applies.\n *\n * runAsSystem runs in a transaction with SET LOCAL app.bypass_rls = 'true'\n * so RLS policies that allow when current_setting('app.bypass_rls', true) = 'true'\n * permit access (CLI generates this bypass policy).\n *\n * @param prisma - PrismaClient instance (or anything matching PrismaClientLike)\n * @returns TenantAdapter implementation\n */\nexport function prismaAdapter(\n prisma: PrismaClientLike,\n): TenantAdapter<PrismaTransactionClient, PrismaTransactionClient> {\n return {\n async runWithTenant<T>(\n tenantId: string,\n fn: (database: PrismaTransactionClient) => Promise<T>,\n ): Promise<T> {\n return prisma.$transaction(async (tx: PrismaTransactionClient) => {\n await tx.$executeRaw`SELECT set_config('app.current_tenant', ${tenantId}, true)`;\n return fn(tx);\n });\n },\n\n async runAsSystem<T>(\n fn: (database: PrismaTransactionClient) => Promise<T>,\n ): Promise<T> {\n return prisma.$transaction(async (tx: PrismaTransactionClient) => {\n await tx.$executeRaw`SELECT set_config('app.bypass_rls', 'true', true)`;\n return fn(tx);\n });\n },\n };\n}\n","import type { Tenant, TenantRepository } from \"@usebetterdev/tenant-core\";\nimport type { PrismaTransactionClient } from \"./types.js\";\n\n/**\n * Converts a raw DB row to the Tenant shape expected by core.\n */\nfunction rowToTenant(row: Record<string, unknown>): Tenant {\n const createdAt = row.created_at ?? row.createdAt;\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 * Validate table name to prevent SQL injection. Only allows [a-zA-Z_][a-zA-Z0-9_]*.\n */\nfunction assertSafeIdentifier(name: string): void {\n if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {\n throw new Error(\n `better-tenant: invalid table name \"${name}\" — must be [a-zA-Z_][a-zA-Z0-9_]*`,\n );\n }\n}\n\n/**\n * Creates getTenantRepository for use with betterTenant({ getTenantRepository }).\n *\n * Uses raw SQL ($queryRawUnsafe / $executeRawUnsafe) on the Prisma transaction\n * client so the adapter works without requiring a generated `Tenant` model in\n * the user's Prisma schema. The tenants table must match the CLI-generated shape\n * (id UUID, name TEXT, slug TEXT, created_at TIMESTAMPTZ).\n *\n * Table name is validated at creation time to prevent injection — only\n * alphanumeric + underscore identifiers are accepted.\n *\n * @param tableName - SQL table name (default: \"tenants\")\n */\nexport function createGetTenantRepository(\n tableName = \"tenants\",\n): (database: PrismaTransactionClient) => TenantRepository {\n assertSafeIdentifier(tableName);\n\n return (database: PrismaTransactionClient) => {\n const tx = database;\n\n return {\n async create(data) {\n const rows = await tx.$queryRawUnsafe<Record<string, unknown>[]>(\n `INSERT INTO ${tableName} (name, slug) VALUES ($1, $2) RETURNING *`,\n data.name,\n data.slug,\n );\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);\n },\n\n async getById(tenantId: string) {\n const rows = await tx.$queryRawUnsafe<Record<string, unknown>[]>(\n `SELECT * FROM ${tableName} WHERE id = $1::uuid LIMIT 1`,\n tenantId,\n );\n const row = rows[0];\n if (!row || typeof row !== \"object\") {\n return null;\n }\n return rowToTenant(row);\n },\n\n async getBySlug(slug: string) {\n const rows = await tx.$queryRawUnsafe<Record<string, unknown>[]>(\n `SELECT * FROM ${tableName} WHERE slug = $1 LIMIT 1`,\n slug,\n );\n const row = rows[0];\n if (!row || typeof row !== \"object\") {\n return null;\n }\n return rowToTenant(row);\n },\n\n async update(tenantId, data) {\n const setClauses: string[] = [];\n const values: unknown[] = [];\n\n if (data.name !== undefined) {\n values.push(data.name);\n setClauses.push(`name = $${String(values.length)}`);\n }\n if (data.slug !== undefined) {\n values.push(data.slug);\n setClauses.push(`slug = $${String(values.length)}`);\n }\n\n if (setClauses.length === 0) {\n const rows = await tx.$queryRawUnsafe<Record<string, unknown>[]>(\n `SELECT * FROM ${tableName} WHERE id = $1::uuid LIMIT 1`,\n tenantId,\n );\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);\n }\n\n values.push(tenantId);\n const query = `UPDATE ${tableName} SET ${setClauses.join(\", \")} WHERE id = $${String(values.length)}::uuid RETURNING *`;\n\n const rows = await tx.$queryRawUnsafe<Record<string, unknown>[]>(\n query,\n ...values,\n );\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);\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 tx.$queryRawUnsafe<Record<string, unknown>[]>(\n `SELECT * FROM ${tableName} ORDER BY created_at ASC LIMIT $1 OFFSET $2`,\n limit,\n offset,\n );\n return rows.map(rowToTenant);\n },\n\n async delete(tenantId) {\n await tx.$executeRawUnsafe(\n `DELETE FROM ${tableName} WHERE id = $1::uuid`,\n tenantId,\n );\n },\n };\n };\n}\n","import type { DatabaseProvider } from \"@usebetterdev/tenant-core\";\nimport { prismaAdapter } from \"./adapter.js\";\nimport type { PrismaClientLike, PrismaTransactionClient } from \"./types.js\";\nimport { createGetTenantRepository } from \"./repository.js\";\n\n/**\n * Creates a DatabaseProvider for Prisma (PostgreSQL).\n *\n * Bundles prismaAdapter + createGetTenantRepository into a single config value\n * for use with `betterTenant({ database: prismaDatabase(prisma) })`.\n *\n * @param prisma - PrismaClient instance (or anything matching PrismaClientLike)\n * @param options - Optional: custom table name (default: \"tenants\")\n */\nexport function prismaDatabase(\n prisma: PrismaClientLike,\n options?: { tableName?: string },\n): DatabaseProvider<PrismaTransactionClient, PrismaTransactionClient> {\n return {\n adapter: prismaAdapter(prisma),\n getTenantRepository: createGetTenantRepository(options?.tableName),\n };\n}\n"],"mappings":"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","names":["rows","row"]}
1
+ {"version":3,"sources":["../src/adapter.ts","../src/repository.ts","../src/database.ts"],"sourcesContent":["import type { TenantAdapter } from \"@usebetterdev/tenant-core\";\nimport type { PrismaClientLike, PrismaTransactionClient } from \"./types.js\";\n\n/**\n * Creates a TenantAdapter for Prisma (PostgreSQL).\n *\n * Uses an interactive transaction per runWithTenant:\n * BEGIN → SET LOCAL app.current_tenant = tenantId → fn(tx) → COMMIT.\n *\n * The handle passed to fn is the Prisma transaction client; use only this\n * handle for tenant-scoped queries so RLS applies.\n *\n * runAsSystem runs in a transaction with SET LOCAL app.bypass_rls = 'true'\n * so RLS policies that allow when current_setting('app.bypass_rls', true) = 'true'\n * permit access (CLI generates this bypass policy).\n *\n * Generic `TxClient` flows from `PrismaClientLike<TxClient>` so\n * `getDatabase()` returns the user's full transaction client type.\n *\n * @param prisma - PrismaClient instance (or anything matching PrismaClientLike)\n * @returns TenantAdapter implementation\n */\nexport function prismaAdapter<\n TxClient extends PrismaTransactionClient = PrismaTransactionClient,\n>(\n prisma: PrismaClientLike<TxClient>,\n): TenantAdapter<TxClient, TxClient> {\n return {\n async runWithTenant<T>(\n tenantId: string,\n fn: (database: TxClient) => Promise<T>,\n ): Promise<T> {\n return prisma.$transaction(async (tx) => {\n await tx.$executeRaw`SELECT set_config('app.current_tenant', ${tenantId}, true)`;\n return fn(tx);\n });\n },\n\n async runAsSystem<T>(\n fn: (database: TxClient) => Promise<T>,\n ): Promise<T> {\n return prisma.$transaction(async (tx) => {\n await tx.$executeRaw`SELECT set_config('app.bypass_rls', 'true', true)`;\n return fn(tx);\n });\n },\n };\n}\n","import type { Tenant, TenantRepository } from \"@usebetterdev/tenant-core\";\nimport type { PrismaTransactionClient } from \"./types.js\";\n\n/**\n * Converts a raw DB row to the Tenant shape expected by core.\n */\nfunction rowToTenant(row: Record<string, unknown>): Tenant {\n const createdAt = row.created_at ?? row.createdAt;\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 };\n}\n\n/**\n * Validate table name to prevent SQL injection. Only allows [a-zA-Z_][a-zA-Z0-9_]*.\n */\nfunction assertSafeIdentifier(name: string): void {\n if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {\n throw new Error(\n `better-tenant: invalid table name \"${name}\" — must be [a-zA-Z_][a-zA-Z0-9_]*`,\n );\n }\n}\n\n/**\n * Creates getTenantRepository for use with betterTenant({ getTenantRepository }).\n *\n * Uses raw SQL ($queryRawUnsafe / $executeRawUnsafe) on the Prisma transaction\n * client so the adapter works without requiring a generated `Tenant` model in\n * the user's Prisma schema. The tenants table must match the CLI-generated shape\n * (id UUID, name TEXT, slug TEXT, created_at TIMESTAMPTZ).\n *\n * Table name is validated at creation time to prevent injection — only\n * alphanumeric + underscore identifiers are accepted.\n *\n * @param tableName - SQL table name (default: \"tenants\")\n */\nexport function createGetTenantRepository(\n tableName = \"tenants\",\n): (database: PrismaTransactionClient) => TenantRepository {\n assertSafeIdentifier(tableName);\n\n return (database: PrismaTransactionClient) => {\n const tx = database;\n\n return {\n async create(data) {\n const id = crypto.randomUUID();\n const rows = await tx.$queryRawUnsafe<Record<string, unknown>[]>(\n `INSERT INTO ${tableName} (id, name, slug) VALUES ($1::uuid, $2, $3) RETURNING *`,\n id,\n data.name,\n data.slug,\n );\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);\n },\n\n async getById(tenantId: string) {\n const rows = await tx.$queryRawUnsafe<Record<string, unknown>[]>(\n `SELECT * FROM ${tableName} WHERE id = $1::uuid LIMIT 1`,\n tenantId,\n );\n const row = rows[0];\n if (!row || typeof row !== \"object\") {\n return null;\n }\n return rowToTenant(row);\n },\n\n async getBySlug(slug: string) {\n const rows = await tx.$queryRawUnsafe<Record<string, unknown>[]>(\n `SELECT * FROM ${tableName} WHERE slug = $1 LIMIT 1`,\n slug,\n );\n const row = rows[0];\n if (!row || typeof row !== \"object\") {\n return null;\n }\n return rowToTenant(row);\n },\n\n async update(tenantId, data) {\n const setClauses: string[] = [];\n const values: unknown[] = [];\n\n if (data.name !== undefined) {\n values.push(data.name);\n setClauses.push(`name = $${String(values.length)}`);\n }\n if (data.slug !== undefined) {\n values.push(data.slug);\n setClauses.push(`slug = $${String(values.length)}`);\n }\n\n if (setClauses.length === 0) {\n const rows = await tx.$queryRawUnsafe<Record<string, unknown>[]>(\n `SELECT * FROM ${tableName} WHERE id = $1::uuid LIMIT 1`,\n tenantId,\n );\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);\n }\n\n values.push(tenantId);\n const query = `UPDATE ${tableName} SET ${setClauses.join(\", \")} WHERE id = $${String(values.length)}::uuid RETURNING *`;\n\n const rows = await tx.$queryRawUnsafe<Record<string, unknown>[]>(\n query,\n ...values,\n );\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);\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 tx.$queryRawUnsafe<Record<string, unknown>[]>(\n `SELECT * FROM ${tableName} ORDER BY created_at ASC LIMIT $1 OFFSET $2`,\n limit,\n offset,\n );\n return rows.map(rowToTenant);\n },\n\n async delete(tenantId) {\n await tx.$executeRawUnsafe(\n `DELETE FROM ${tableName} WHERE id = $1::uuid`,\n tenantId,\n );\n },\n\n async count() {\n const rows = await tx.$queryRawUnsafe<{ count: bigint | number | string }[]>(\n `SELECT count(*)::int AS count FROM ${tableName}`,\n );\n const row = rows[0];\n return Number(row?.count ?? 0);\n },\n };\n };\n}\n","import type { DatabaseProvider } from \"@usebetterdev/tenant-core\";\nimport { prismaAdapter } from \"./adapter.js\";\nimport type { PrismaClientLike, PrismaTransactionClient } from \"./types.js\";\nimport { createGetTenantRepository } from \"./repository.js\";\n\n/**\n * Creates a DatabaseProvider for Prisma (PostgreSQL).\n *\n * Bundles prismaAdapter + createGetTenantRepository into a single config value\n * for use with `betterTenant({ database: prismaDatabase(prisma) })`.\n *\n * Generic `TxClient` is inferred from the PrismaClient's `$transaction` callback,\n * so `getDatabase()` returns the user's full transaction client type with model\n * methods (e.g. `db.project.findMany()`) instead of just raw query methods.\n *\n * @param prisma - PrismaClient instance (or anything matching PrismaClientLike)\n * @param options - Optional: custom table name (default: \"tenants\")\n */\nexport function prismaDatabase<\n TxClient extends PrismaTransactionClient = PrismaTransactionClient,\n>(\n prisma: PrismaClientLike<TxClient>,\n options?: { tableName?: string },\n): DatabaseProvider<TxClient, TxClient> {\n return {\n adapter: prismaAdapter(prisma),\n getTenantRepository: createGetTenantRepository(options?.tableName),\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-prisma",
3
- "version": "0.2.1-beta.1",
3
+ "version": "0.3.0-beta.3",
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",
@@ -24,14 +24,18 @@
24
24
  "README.md"
25
25
  ],
26
26
  "dependencies": {
27
- "@usebetterdev/tenant-core": "0.2.1-beta.1"
27
+ "@usebetterdev/tenant-core": "0.3.0-beta.3"
28
28
  },
29
29
  "peerDependencies": {
30
- "@prisma/client": ">=5.0.0"
30
+ "@prisma/client": ">=7.0.0",
31
+ "@prisma/adapter-pg": ">=7.0.0"
31
32
  },
32
33
  "peerDependenciesMeta": {
33
34
  "@prisma/client": {
34
35
  "optional": false
36
+ },
37
+ "@prisma/adapter-pg": {
38
+ "optional": false
35
39
  }
36
40
  },
37
41
  "devDependencies": {
@@ -42,7 +46,7 @@
42
46
  "tsup": "^8.3.5",
43
47
  "typescript": "~5.7.2",
44
48
  "vitest": "^2.1.6",
45
- "@usebetterdev/test-utils": "0.2.1-beta.1"
49
+ "@usebetterdev/test-utils": "0.3.0-beta.3"
46
50
  },
47
51
  "engines": {
48
52
  "node": ">=22"