@usebetterdev/tenant-drizzle 0.1.0 → 0.2.0-beta.9
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 +7 -8
- package/dist/index.cjs +3 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -9
- package/dist/index.d.ts +13 -9
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @usebetterdev/tenant-drizzle
|
|
2
2
|
|
|
3
|
-
Drizzle adapter for [@usebetterdev/tenant
|
|
3
|
+
Drizzle adapter for [@usebetterdev/tenant](https://github.com/usebetter-dev/usebetter). Runs each tenant scope in a transaction with `SET LOCAL app.current_tenant = '<uuid>'` so Postgres RLS applies.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
pnpm add @
|
|
8
|
+
pnpm add @usebetterdev/tenant drizzle-orm pg
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
13
|
```ts
|
|
14
14
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
15
|
-
import { betterTenant } from "
|
|
15
|
+
import { betterTenant } from "@usebetterdev/tenant";
|
|
16
16
|
import {
|
|
17
17
|
drizzleAdapter,
|
|
18
18
|
createGetTenantRepository,
|
|
19
19
|
tenantsTable,
|
|
20
|
-
} from "
|
|
20
|
+
} from "@usebetterdev/tenant/drizzle";
|
|
21
21
|
|
|
22
22
|
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
23
23
|
const db = drizzle(pool);
|
|
@@ -26,13 +26,12 @@ const adapter = drizzleAdapter(db);
|
|
|
26
26
|
const tenant = betterTenant({
|
|
27
27
|
adapter,
|
|
28
28
|
tenantResolver: { header: "x-tenant-id" },
|
|
29
|
-
tenantTables: ["projects"],
|
|
30
29
|
getTenantRepository: createGetTenantRepository(tenantsTable),
|
|
31
30
|
});
|
|
32
31
|
|
|
33
32
|
// tenant.api.createTenant, updateTenant, listTenants, deleteTenant now work (via runAsSystem).
|
|
34
33
|
// In middleware: resolve tenantId, then runWithTenantAndDatabase(tenantId, adapter, next).
|
|
35
|
-
// In handlers: getDatabase() returns the transaction handle; getContext().tenant is the loaded Tenant (set loadTenant: false to skip loading).
|
|
34
|
+
// In handlers: tenant.getDatabase() returns the transaction handle; tenant.getContext().tenant is the loaded Tenant (set loadTenant: false to skip loading).
|
|
36
35
|
```
|
|
37
36
|
|
|
38
37
|
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`.
|
|
@@ -48,7 +47,7 @@ Table shape for tenants must match the CLI-generated schema: `id` (UUID), `name`
|
|
|
48
47
|
|
|
49
48
|
Integration tests in `src/adapter.integration.test.ts` use [@testcontainers/postgresql](https://www.npmjs.com/package/@testcontainers/postgresql): a Postgres container is started automatically when you run tests (Docker required). If Docker is unavailable, the tests are skipped. You can instead set `DATABASE_URL` to run the same tests against an existing database.
|
|
50
49
|
|
|
51
|
-
**Run integration tests:** From the repo root, run `pnpm run test:integration` or `pnpm run test --filter @
|
|
50
|
+
**Run integration tests:** From the repo root, run `pnpm run test:integration` or `pnpm run test --filter @usebetterdev/tenant-drizzle`. Running `pnpm run test` from inside `packages/core` only runs core unit tests; run from root to run all packages (including drizzle integration).
|
|
52
51
|
|
|
53
52
|
## Peer dependency
|
|
54
53
|
|
package/dist/index.cjs
CHANGED
|
@@ -106,7 +106,9 @@ function createGetTenantRepository(table) {
|
|
|
106
106
|
async getById(tenantId) {
|
|
107
107
|
const rows = await db.select().from(table).where((0, import_drizzle_orm2.eq)(table.id, tenantId)).limit(1);
|
|
108
108
|
const row = Array.isArray(rows) ? rows[0] : void 0;
|
|
109
|
-
if (!row || typeof row !== "object")
|
|
109
|
+
if (!row || typeof row !== "object") {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
110
112
|
return rowToTenant(row);
|
|
111
113
|
},
|
|
112
114
|
async list(options = {}) {
|
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 { tenantsTable } from \"./schema.js\";\nexport {\n createGetTenantRepository,\n type TenantsTableLike,\n} from \"./repository.js\";\n","import { sql } from \"drizzle-orm\";\nimport type {
|
|
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"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { sql } from 'drizzle-orm';
|
|
2
|
-
import { TenantAdapter,
|
|
2
|
+
import { TenantAdapter, TenantRepository } from '@usebetterdev/tenant-core';
|
|
3
3
|
import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Minimal shape for a Drizzle pg
|
|
7
|
-
*
|
|
6
|
+
* Minimal shape for a Drizzle pg transaction: must support execute(sql).
|
|
7
|
+
* The actual Drizzle transaction extends this with insert, select, etc.
|
|
8
8
|
*/
|
|
9
|
-
interface DrizzlePgDatabase {
|
|
10
|
-
transaction<T>(callback: (tx: DrizzlePgTransaction) => Promise<T>): Promise<T>;
|
|
11
|
-
}
|
|
12
9
|
interface DrizzlePgTransaction {
|
|
13
10
|
execute(query: ReturnType<typeof sql> | unknown): Promise<unknown>;
|
|
14
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Minimal shape for a Drizzle pg database: must support transaction().
|
|
14
|
+
* Generic over TTx so the full transaction type (with insert, select, etc.) is preserved.
|
|
15
|
+
*/
|
|
16
|
+
interface DrizzlePgDatabase<TTx extends DrizzlePgTransaction = DrizzlePgTransaction> {
|
|
17
|
+
transaction<T>(callback: (tx: TTx) => Promise<T>): Promise<T>;
|
|
18
|
+
}
|
|
15
19
|
/**
|
|
16
20
|
* Creates a TenantAdapter for Drizzle (PostgreSQL).
|
|
17
21
|
*
|
|
@@ -24,7 +28,7 @@ interface DrizzlePgTransaction {
|
|
|
24
28
|
* @param database - Drizzle pg database (from drizzle(pool) or similar)
|
|
25
29
|
* @returns TenantAdapter implementation
|
|
26
30
|
*/
|
|
27
|
-
declare function drizzleAdapter(database: DrizzlePgDatabase): TenantAdapter
|
|
31
|
+
declare function drizzleAdapter<TTx extends DrizzlePgTransaction>(database: DrizzlePgDatabase<TTx>): TenantAdapter<TTx, TTx>;
|
|
28
32
|
|
|
29
33
|
/**
|
|
30
34
|
* Standard tenants table schema matching the CLI-generated shape.
|
|
@@ -114,6 +118,6 @@ type TenantsTableLike = typeof tenantsTable;
|
|
|
114
118
|
* Pass the tenants table (e.g. tenantsTable from this package or your own with id, name, slug, created_at).
|
|
115
119
|
* The returned function is called by core with the system database handle (from adapter.runAsSystem).
|
|
116
120
|
*/
|
|
117
|
-
declare function createGetTenantRepository(table: TenantsTableLike): (database:
|
|
121
|
+
declare function createGetTenantRepository(table: TenantsTableLike): (database: unknown) => TenantRepository;
|
|
118
122
|
|
|
119
|
-
export { type TenantsTableLike, createGetTenantRepository, drizzleAdapter, tenantsTable };
|
|
123
|
+
export { type DrizzlePgDatabase, type DrizzlePgTransaction, type TenantsTableLike, createGetTenantRepository, drizzleAdapter, tenantsTable };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { sql } from 'drizzle-orm';
|
|
2
|
-
import { TenantAdapter,
|
|
2
|
+
import { TenantAdapter, TenantRepository } from '@usebetterdev/tenant-core';
|
|
3
3
|
import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Minimal shape for a Drizzle pg
|
|
7
|
-
*
|
|
6
|
+
* Minimal shape for a Drizzle pg transaction: must support execute(sql).
|
|
7
|
+
* The actual Drizzle transaction extends this with insert, select, etc.
|
|
8
8
|
*/
|
|
9
|
-
interface DrizzlePgDatabase {
|
|
10
|
-
transaction<T>(callback: (tx: DrizzlePgTransaction) => Promise<T>): Promise<T>;
|
|
11
|
-
}
|
|
12
9
|
interface DrizzlePgTransaction {
|
|
13
10
|
execute(query: ReturnType<typeof sql> | unknown): Promise<unknown>;
|
|
14
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Minimal shape for a Drizzle pg database: must support transaction().
|
|
14
|
+
* Generic over TTx so the full transaction type (with insert, select, etc.) is preserved.
|
|
15
|
+
*/
|
|
16
|
+
interface DrizzlePgDatabase<TTx extends DrizzlePgTransaction = DrizzlePgTransaction> {
|
|
17
|
+
transaction<T>(callback: (tx: TTx) => Promise<T>): Promise<T>;
|
|
18
|
+
}
|
|
15
19
|
/**
|
|
16
20
|
* Creates a TenantAdapter for Drizzle (PostgreSQL).
|
|
17
21
|
*
|
|
@@ -24,7 +28,7 @@ interface DrizzlePgTransaction {
|
|
|
24
28
|
* @param database - Drizzle pg database (from drizzle(pool) or similar)
|
|
25
29
|
* @returns TenantAdapter implementation
|
|
26
30
|
*/
|
|
27
|
-
declare function drizzleAdapter(database: DrizzlePgDatabase): TenantAdapter
|
|
31
|
+
declare function drizzleAdapter<TTx extends DrizzlePgTransaction>(database: DrizzlePgDatabase<TTx>): TenantAdapter<TTx, TTx>;
|
|
28
32
|
|
|
29
33
|
/**
|
|
30
34
|
* Standard tenants table schema matching the CLI-generated shape.
|
|
@@ -114,6 +118,6 @@ type TenantsTableLike = typeof tenantsTable;
|
|
|
114
118
|
* Pass the tenants table (e.g. tenantsTable from this package or your own with id, name, slug, created_at).
|
|
115
119
|
* The returned function is called by core with the system database handle (from adapter.runAsSystem).
|
|
116
120
|
*/
|
|
117
|
-
declare function createGetTenantRepository(table: TenantsTableLike): (database:
|
|
121
|
+
declare function createGetTenantRepository(table: TenantsTableLike): (database: unknown) => TenantRepository;
|
|
118
122
|
|
|
119
|
-
export { type TenantsTableLike, createGetTenantRepository, drizzleAdapter, tenantsTable };
|
|
123
|
+
export { type DrizzlePgDatabase, type DrizzlePgTransaction, type TenantsTableLike, createGetTenantRepository, drizzleAdapter, tenantsTable };
|
package/dist/index.js
CHANGED
|
@@ -78,7 +78,9 @@ function createGetTenantRepository(table) {
|
|
|
78
78
|
async getById(tenantId) {
|
|
79
79
|
const rows = await db.select().from(table).where(eq(table.id, tenantId)).limit(1);
|
|
80
80
|
const row = Array.isArray(rows) ? rows[0] : void 0;
|
|
81
|
-
if (!row || typeof row !== "object")
|
|
81
|
+
if (!row || typeof row !== "object") {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
82
84
|
return rowToTenant(row);
|
|
83
85
|
},
|
|
84
86
|
async list(options = {}) {
|
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 {
|
|
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":";AAAA,SAAS,WAAW;AAiCb,SAAS,eACd,UACyB;AACzB,SAAO;AAAA,IACL,MAAM,cACJ,UACA,IACY;AACZ,aAAO,SAAS,YAAY,OAAO,OAAO;AACxC,cAAM,GAAG;AAAA,UACP,8CAA8C,QAAQ;AAAA,QACxD;AACA,eAAO,GAAG,EAAE;AAAA,MACd,CAAC;AAAA,IACH;AAAA,IAEA,MAAM,YACJ,IACY;AACZ,aAAO,SAAS,YAAY,OAAO,OAAO;AACxC,cAAM,GAAG;AAAA,UACP;AAAA,QACF;AACA,eAAO,GAAG,EAAE;AAAA,MACd,CAAC;AAAA,IACH;AAAA,EACF;AACF;;;AC5DA,SAAS,SAAS,MAAM,WAAW,YAAY;AAOxC,IAAM,eAAe,QAAQ,WAAW;AAAA,EAC7C,IAAI,KAAK,IAAI,EAAE,WAAW,EAAE,cAAc;AAAA,EAC1C,MAAM,KAAK,MAAM,EAAE,QAAQ;AAAA,EAC3B,MAAM,KAAK,MAAM,EAAE,QAAQ,EAAE,OAAO;AAAA,EACpC,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,EAAE,WAAW,EAAE,QAAQ;AAClF,CAAC;;;ACZD,SAAS,UAAU;AAOnB,SAAS,YAAY,KAAsC;AACzD,SAAO;AAAA,IACL,IAAI,OAAO,IAAI,EAAE;AAAA,IACjB,MAAM,OAAO,IAAI,IAAI;AAAA,IACrB,MAAM,OAAO,IAAI,IAAI;AAAA,IACrB,WAAY,IAAI,aAAa,IAAI;AAAA,IACjC,GAAG;AAAA,EACL;AACF;AAOO,SAAS,0BACd,OACyC;AACzC,SAAO,CAAC,aAAsB;AAC5B,UAAM,KAAK;AAsBX,WAAO;AAAA,MACL,MAAM,OAAO,MAAM;AACjB,cAAM,OAAO,MAAM,GAChB,OAAO,KAAK,EACZ,OAAO;AAAA,UACN,MAAM,KAAK;AAAA,UACX,MAAM,KAAK;AAAA,QACb,CAAC,EACA,UAAU;AACb,cAAM,MAAM,KAAK,CAAC;AAClB,YAAI,CAAC,OAAO,OAAO,QAAQ,UAAU;AACnC,gBAAM,IAAI,MAAM,kDAAkD;AAAA,QACpE;AACA,eAAO,YAAY,GAA8B;AAAA,MACnD;AAAA,MAEA,MAAM,OAAO,UAAU,MAAM;AAC3B,cAAM,MAA+C,CAAC;AACtD,YAAI,KAAK,SAAS,OAAW,KAAI,OAAO,KAAK;AAC7C,YAAI,KAAK,SAAS,OAAW,KAAI,OAAO,KAAK;AAC7C,YAAI,OAAO,KAAK,GAAG,EAAE,WAAW,GAAG;AACjC,gBAAMA,QAAO,MAAM,GAChB,OAAO,EACP,KAAK,KAAK,EACV,MAAM,GAAG,MAAM,IAAI,QAAQ,CAAC,EAC5B,MAAM,CAAC;AACV,gBAAMC,OAAM,MAAM,QAAQD,KAAI,IAAIA,MAAK,CAAC,IAAI;AAC5C,cAAI,CAACC,QAAO,OAAOA,SAAQ,UAAU;AACnC,kBAAM,IAAI,MAAM,iCAAiC;AAAA,UACnD;AACA,iBAAO,YAAYA,IAA8B;AAAA,QACnD;AACA,cAAM,OAAO,MAAM,GAChB,OAAO,KAAK,EACZ,IAAI,GAAG,EACP,MAAM,GAAG,MAAM,IAAI,QAAQ,CAAC,EAC5B,UAAU;AACb,cAAM,MAAM,KAAK,CAAC;AAClB,YAAI,CAAC,OAAO,OAAO,QAAQ,UAAU;AACnC,gBAAM,IAAI,MAAM,iCAAiC;AAAA,QACnD;AACA,eAAO,YAAY,GAA8B;AAAA,MACnD;AAAA,MAEA,MAAM,QAAQ,UAAkB;AAC9B,cAAM,OAAO,MAAM,GAChB,OAAO,EACP,KAAK,KAAK,EACV,MAAM,GAAG,MAAM,IAAI,QAAQ,CAAC,EAC5B,MAAM,CAAC;AACV,cAAM,MAAM,MAAM,QAAQ,IAAI,IAAI,KAAK,CAAC,IAAI;AAC5C,YAAI,CAAC,OAAO,OAAO,QAAQ,UAAU;AACnC,iBAAO;AAAA,QACT;AAEA,eAAO,YAAY,GAA8B;AAAA,MACnD;AAAA,MAEA,MAAM,KAAK,UAAU,CAAC,GAAG;AACvB,cAAM,QAAQ,KAAK,IAAI,QAAQ,SAAS,IAAI,EAAE;AAC9C,cAAM,SAAS,KAAK,IAAI,GAAG,QAAQ,UAAU,CAAC;AAC9C,cAAM,OAAO,MAAM,GAAG,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,KAAK,EAAE,OAAO,MAAM;AACrE,gBAAQ,MAAM,QAAQ,IAAI,IAAI,OAAO,CAAC,GAAG;AAAA,UAAI,CAAC,MAC5C,YAAY,CAA4B;AAAA,QAC1C;AAAA,MACF;AAAA,MAEA,MAAM,OAAO,UAAU;AACrB,cAAM,GAAG,OAAO,KAAK,EAAE,MAAM,GAAG,MAAM,IAAI,QAAQ,CAAC;AAAA,MACrD;AAAA,IACF;AAAA,EACF;AACF;","names":["rows","row"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usebetterdev/tenant-drizzle",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-beta.9",
|
|
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.
|
|
28
|
+
"@usebetterdev/tenant-core": "0.2.0-beta.9"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"pg": ">=8.0.0"
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"build": "tsup",
|
|
53
53
|
"lint": "oxlint",
|
|
54
54
|
"test": "vitest run",
|
|
55
|
-
"test:integration": "vitest run",
|
|
55
|
+
"test:integration": "vitest run -c vitest.integration.config.ts",
|
|
56
56
|
"typecheck": "tsc --noEmit"
|
|
57
57
|
}
|
|
58
58
|
}
|