@usebetterdev/tenant-drizzle 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/dist/index.cjs +132 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +119 -0
- package/dist/index.d.ts +119 -0
- package/dist/index.js +103 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 usebetter
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# @better-tenant/drizzle
|
|
2
|
+
|
|
3
|
+
Drizzle adapter for [@usebetterdev/tenant-core](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
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @better-tenant/core @better-tenant/drizzle drizzle-orm pg
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { drizzle } from "drizzle-orm/node-postgres";
|
|
15
|
+
import { betterTenant } from "better-tenant";
|
|
16
|
+
import {
|
|
17
|
+
drizzleAdapter,
|
|
18
|
+
createGetTenantRepository,
|
|
19
|
+
tenantsTable,
|
|
20
|
+
} from "better-tenant/drizzle";
|
|
21
|
+
|
|
22
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
23
|
+
const db = drizzle(pool);
|
|
24
|
+
|
|
25
|
+
const adapter = drizzleAdapter(db);
|
|
26
|
+
const tenant = betterTenant({
|
|
27
|
+
adapter,
|
|
28
|
+
tenantResolver: { header: "x-tenant-id" },
|
|
29
|
+
tenantTables: ["projects"],
|
|
30
|
+
getTenantRepository: createGetTenantRepository(tenantsTable),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// tenant.api.createTenant, updateTenant, listTenants, deleteTenant now work (via runAsSystem).
|
|
34
|
+
// 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).
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
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`.
|
|
39
|
+
|
|
40
|
+
## Behaviour
|
|
41
|
+
|
|
42
|
+
- **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.
|
|
43
|
+
- **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).
|
|
44
|
+
- **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).
|
|
45
|
+
- The handle passed to `fn` is the same type as your Drizzle db but transaction-scoped (full Drizzle API on `tx`).
|
|
46
|
+
|
|
47
|
+
## Testing
|
|
48
|
+
|
|
49
|
+
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
|
+
|
|
51
|
+
**Run integration tests:** From the repo root, run `pnpm run test:integration` or `pnpm run test --filter @better-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
|
+
|
|
53
|
+
## Peer dependency
|
|
54
|
+
|
|
55
|
+
Requires `pg` (node-postgres). Install it in your app when using this adapter.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
createGetTenantRepository: () => createGetTenantRepository,
|
|
24
|
+
drizzleAdapter: () => drizzleAdapter,
|
|
25
|
+
tenantsTable: () => tenantsTable
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/adapter.ts
|
|
30
|
+
var import_drizzle_orm = require("drizzle-orm");
|
|
31
|
+
function drizzleAdapter(database) {
|
|
32
|
+
return {
|
|
33
|
+
async runWithTenant(tenantId, fn) {
|
|
34
|
+
return database.transaction(async (tx) => {
|
|
35
|
+
await tx.execute(
|
|
36
|
+
import_drizzle_orm.sql`SELECT set_config('app.current_tenant', ${tenantId}, true)`
|
|
37
|
+
);
|
|
38
|
+
return fn(tx);
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
async runAsSystem(fn) {
|
|
42
|
+
return database.transaction(async (tx) => {
|
|
43
|
+
await tx.execute(
|
|
44
|
+
import_drizzle_orm.sql`SELECT set_config('app.bypass_rls', 'true', true)`
|
|
45
|
+
);
|
|
46
|
+
return fn(tx);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/schema.ts
|
|
53
|
+
var import_pg_core = require("drizzle-orm/pg-core");
|
|
54
|
+
var tenantsTable = (0, import_pg_core.pgTable)("tenants", {
|
|
55
|
+
id: (0, import_pg_core.uuid)("id").primaryKey().defaultRandom(),
|
|
56
|
+
name: (0, import_pg_core.text)("name").notNull(),
|
|
57
|
+
slug: (0, import_pg_core.text)("slug").notNull().unique(),
|
|
58
|
+
createdAt: (0, import_pg_core.timestamp)("created_at", { withTimezone: true }).defaultNow().notNull()
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// src/repository.ts
|
|
62
|
+
var import_drizzle_orm2 = require("drizzle-orm");
|
|
63
|
+
function rowToTenant(row) {
|
|
64
|
+
return {
|
|
65
|
+
id: String(row.id),
|
|
66
|
+
name: String(row.name),
|
|
67
|
+
slug: String(row.slug),
|
|
68
|
+
createdAt: row.createdAt ?? row.created_at,
|
|
69
|
+
...row
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function createGetTenantRepository(table) {
|
|
73
|
+
return (database) => {
|
|
74
|
+
const db = database;
|
|
75
|
+
return {
|
|
76
|
+
async create(data) {
|
|
77
|
+
const rows = await db.insert(table).values({
|
|
78
|
+
name: data.name,
|
|
79
|
+
slug: data.slug
|
|
80
|
+
}).returning();
|
|
81
|
+
const row = rows[0];
|
|
82
|
+
if (!row || typeof row !== "object") {
|
|
83
|
+
throw new Error("better-tenant: createTenant failed to return row");
|
|
84
|
+
}
|
|
85
|
+
return rowToTenant(row);
|
|
86
|
+
},
|
|
87
|
+
async update(tenantId, data) {
|
|
88
|
+
const set = {};
|
|
89
|
+
if (data.name !== void 0) set.name = data.name;
|
|
90
|
+
if (data.slug !== void 0) set.slug = data.slug;
|
|
91
|
+
if (Object.keys(set).length === 0) {
|
|
92
|
+
const rows2 = await db.select().from(table).where((0, import_drizzle_orm2.eq)(table.id, tenantId)).limit(1);
|
|
93
|
+
const row2 = Array.isArray(rows2) ? rows2[0] : void 0;
|
|
94
|
+
if (!row2 || typeof row2 !== "object") {
|
|
95
|
+
throw new Error("better-tenant: tenant not found");
|
|
96
|
+
}
|
|
97
|
+
return rowToTenant(row2);
|
|
98
|
+
}
|
|
99
|
+
const rows = await db.update(table).set(set).where((0, import_drizzle_orm2.eq)(table.id, tenantId)).returning();
|
|
100
|
+
const row = rows[0];
|
|
101
|
+
if (!row || typeof row !== "object") {
|
|
102
|
+
throw new Error("better-tenant: tenant not found");
|
|
103
|
+
}
|
|
104
|
+
return rowToTenant(row);
|
|
105
|
+
},
|
|
106
|
+
async getById(tenantId) {
|
|
107
|
+
const rows = await db.select().from(table).where((0, import_drizzle_orm2.eq)(table.id, tenantId)).limit(1);
|
|
108
|
+
const row = Array.isArray(rows) ? rows[0] : void 0;
|
|
109
|
+
if (!row || typeof row !== "object") return null;
|
|
110
|
+
return rowToTenant(row);
|
|
111
|
+
},
|
|
112
|
+
async list(options = {}) {
|
|
113
|
+
const limit = Math.min(options.limit ?? 50, 50);
|
|
114
|
+
const offset = Math.max(0, options.offset ?? 0);
|
|
115
|
+
const rows = await db.select().from(table).limit(limit).offset(offset);
|
|
116
|
+
return (Array.isArray(rows) ? rows : []).map(
|
|
117
|
+
(r) => rowToTenant(r)
|
|
118
|
+
);
|
|
119
|
+
},
|
|
120
|
+
async delete(tenantId) {
|
|
121
|
+
await db.delete(table).where((0, import_drizzle_orm2.eq)(table.id, tenantId));
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
127
|
+
0 && (module.exports = {
|
|
128
|
+
createGetTenantRepository,
|
|
129
|
+
drizzleAdapter,
|
|
130
|
+
tenantsTable
|
|
131
|
+
});
|
|
132
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +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 {\n TenantAdapter,\n TenantScopedDatabase,\n SystemDatabase,\n} from \"@usebetterdev/tenant-core\";\n\n/**\n * Minimal shape for a Drizzle pg database: must support transaction()\n * and the transaction runner must support execute(sql).\n */\nexport interface DrizzlePgDatabase {\n transaction<T>(\n callback: (tx: DrizzlePgTransaction) => Promise<T>,\n ): Promise<T>;\n}\n\nexport interface DrizzlePgTransaction {\n execute(query: ReturnType<typeof sql> | unknown): Promise<unknown>;\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(database: DrizzlePgDatabase): TenantAdapter {\n return {\n async runWithTenant<T>(\n tenantId: string,\n fn: (database: TenantScopedDatabase) => 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 as TenantScopedDatabase);\n });\n },\n\n async runAsSystem<T>(\n fn: (database: SystemDatabase) => 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 as SystemDatabase);\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 {\n SystemDatabase,\n Tenant,\n TenantRepository,\n} 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: SystemDatabase) => TenantRepository {\n return (database: SystemDatabase) => {\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\") return null;\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
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { sql } from 'drizzle-orm';
|
|
2
|
+
import { TenantAdapter, SystemDatabase, TenantRepository } from '@usebetterdev/tenant-core';
|
|
3
|
+
import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Minimal shape for a Drizzle pg database: must support transaction()
|
|
7
|
+
* and the transaction runner must support execute(sql).
|
|
8
|
+
*/
|
|
9
|
+
interface DrizzlePgDatabase {
|
|
10
|
+
transaction<T>(callback: (tx: DrizzlePgTransaction) => Promise<T>): Promise<T>;
|
|
11
|
+
}
|
|
12
|
+
interface DrizzlePgTransaction {
|
|
13
|
+
execute(query: ReturnType<typeof sql> | unknown): Promise<unknown>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Creates a TenantAdapter for Drizzle (PostgreSQL).
|
|
17
|
+
*
|
|
18
|
+
* Uses a transaction per runWithTenant: BEGIN → SET LOCAL app.current_tenant = tenantId → fn(tx) → COMMIT.
|
|
19
|
+
* The handle passed to fn is the transaction runner; use only this handle for tenant-scoped queries so RLS applies.
|
|
20
|
+
*
|
|
21
|
+
* runAsSystem runs in a transaction with SET LOCAL app.bypass_rls = true so RLS policies that allow
|
|
22
|
+
* when current_setting('app.bypass_rls', true) = 'true' permit access (CLI generates this bypass policy).
|
|
23
|
+
*
|
|
24
|
+
* @param database - Drizzle pg database (from drizzle(pool) or similar)
|
|
25
|
+
* @returns TenantAdapter implementation
|
|
26
|
+
*/
|
|
27
|
+
declare function drizzleAdapter(database: DrizzlePgDatabase): TenantAdapter;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Standard tenants table schema matching the CLI-generated shape.
|
|
31
|
+
* Columns: id (UUID), name, slug, created_at.
|
|
32
|
+
* Use this table or provide your own with the same column contract for createGetTenantRepository.
|
|
33
|
+
*/
|
|
34
|
+
declare const tenantsTable: drizzle_orm_pg_core.PgTableWithColumns<{
|
|
35
|
+
name: "tenants";
|
|
36
|
+
schema: undefined;
|
|
37
|
+
columns: {
|
|
38
|
+
id: drizzle_orm_pg_core.PgColumn<{
|
|
39
|
+
name: "id";
|
|
40
|
+
tableName: "tenants";
|
|
41
|
+
dataType: "string";
|
|
42
|
+
columnType: "PgUUID";
|
|
43
|
+
data: string;
|
|
44
|
+
driverParam: string;
|
|
45
|
+
notNull: true;
|
|
46
|
+
hasDefault: true;
|
|
47
|
+
isPrimaryKey: true;
|
|
48
|
+
isAutoincrement: false;
|
|
49
|
+
hasRuntimeDefault: false;
|
|
50
|
+
enumValues: undefined;
|
|
51
|
+
baseColumn: never;
|
|
52
|
+
identity: undefined;
|
|
53
|
+
generated: undefined;
|
|
54
|
+
}, {}, {}>;
|
|
55
|
+
name: drizzle_orm_pg_core.PgColumn<{
|
|
56
|
+
name: "name";
|
|
57
|
+
tableName: "tenants";
|
|
58
|
+
dataType: "string";
|
|
59
|
+
columnType: "PgText";
|
|
60
|
+
data: string;
|
|
61
|
+
driverParam: string;
|
|
62
|
+
notNull: true;
|
|
63
|
+
hasDefault: false;
|
|
64
|
+
isPrimaryKey: false;
|
|
65
|
+
isAutoincrement: false;
|
|
66
|
+
hasRuntimeDefault: false;
|
|
67
|
+
enumValues: [string, ...string[]];
|
|
68
|
+
baseColumn: never;
|
|
69
|
+
identity: undefined;
|
|
70
|
+
generated: undefined;
|
|
71
|
+
}, {}, {}>;
|
|
72
|
+
slug: drizzle_orm_pg_core.PgColumn<{
|
|
73
|
+
name: "slug";
|
|
74
|
+
tableName: "tenants";
|
|
75
|
+
dataType: "string";
|
|
76
|
+
columnType: "PgText";
|
|
77
|
+
data: string;
|
|
78
|
+
driverParam: string;
|
|
79
|
+
notNull: true;
|
|
80
|
+
hasDefault: false;
|
|
81
|
+
isPrimaryKey: false;
|
|
82
|
+
isAutoincrement: false;
|
|
83
|
+
hasRuntimeDefault: false;
|
|
84
|
+
enumValues: [string, ...string[]];
|
|
85
|
+
baseColumn: never;
|
|
86
|
+
identity: undefined;
|
|
87
|
+
generated: undefined;
|
|
88
|
+
}, {}, {}>;
|
|
89
|
+
createdAt: drizzle_orm_pg_core.PgColumn<{
|
|
90
|
+
name: "created_at";
|
|
91
|
+
tableName: "tenants";
|
|
92
|
+
dataType: "date";
|
|
93
|
+
columnType: "PgTimestamp";
|
|
94
|
+
data: Date;
|
|
95
|
+
driverParam: string;
|
|
96
|
+
notNull: true;
|
|
97
|
+
hasDefault: true;
|
|
98
|
+
isPrimaryKey: false;
|
|
99
|
+
isAutoincrement: false;
|
|
100
|
+
hasRuntimeDefault: false;
|
|
101
|
+
enumValues: undefined;
|
|
102
|
+
baseColumn: never;
|
|
103
|
+
identity: undefined;
|
|
104
|
+
generated: undefined;
|
|
105
|
+
}, {}, {}>;
|
|
106
|
+
};
|
|
107
|
+
dialect: "pg";
|
|
108
|
+
}>;
|
|
109
|
+
|
|
110
|
+
/** Table shape expected by createGetTenantRepository: id, name, slug, createdAt (or created_at). */
|
|
111
|
+
type TenantsTableLike = typeof tenantsTable;
|
|
112
|
+
/**
|
|
113
|
+
* Creates getTenantRepository for use with betterTenant({ getTenantRepository }).
|
|
114
|
+
* Pass the tenants table (e.g. tenantsTable from this package or your own with id, name, slug, created_at).
|
|
115
|
+
* The returned function is called by core with the system database handle (from adapter.runAsSystem).
|
|
116
|
+
*/
|
|
117
|
+
declare function createGetTenantRepository(table: TenantsTableLike): (database: SystemDatabase) => TenantRepository;
|
|
118
|
+
|
|
119
|
+
export { type TenantsTableLike, createGetTenantRepository, drizzleAdapter, tenantsTable };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { sql } from 'drizzle-orm';
|
|
2
|
+
import { TenantAdapter, SystemDatabase, TenantRepository } from '@usebetterdev/tenant-core';
|
|
3
|
+
import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Minimal shape for a Drizzle pg database: must support transaction()
|
|
7
|
+
* and the transaction runner must support execute(sql).
|
|
8
|
+
*/
|
|
9
|
+
interface DrizzlePgDatabase {
|
|
10
|
+
transaction<T>(callback: (tx: DrizzlePgTransaction) => Promise<T>): Promise<T>;
|
|
11
|
+
}
|
|
12
|
+
interface DrizzlePgTransaction {
|
|
13
|
+
execute(query: ReturnType<typeof sql> | unknown): Promise<unknown>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Creates a TenantAdapter for Drizzle (PostgreSQL).
|
|
17
|
+
*
|
|
18
|
+
* Uses a transaction per runWithTenant: BEGIN → SET LOCAL app.current_tenant = tenantId → fn(tx) → COMMIT.
|
|
19
|
+
* The handle passed to fn is the transaction runner; use only this handle for tenant-scoped queries so RLS applies.
|
|
20
|
+
*
|
|
21
|
+
* runAsSystem runs in a transaction with SET LOCAL app.bypass_rls = true so RLS policies that allow
|
|
22
|
+
* when current_setting('app.bypass_rls', true) = 'true' permit access (CLI generates this bypass policy).
|
|
23
|
+
*
|
|
24
|
+
* @param database - Drizzle pg database (from drizzle(pool) or similar)
|
|
25
|
+
* @returns TenantAdapter implementation
|
|
26
|
+
*/
|
|
27
|
+
declare function drizzleAdapter(database: DrizzlePgDatabase): TenantAdapter;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Standard tenants table schema matching the CLI-generated shape.
|
|
31
|
+
* Columns: id (UUID), name, slug, created_at.
|
|
32
|
+
* Use this table or provide your own with the same column contract for createGetTenantRepository.
|
|
33
|
+
*/
|
|
34
|
+
declare const tenantsTable: drizzle_orm_pg_core.PgTableWithColumns<{
|
|
35
|
+
name: "tenants";
|
|
36
|
+
schema: undefined;
|
|
37
|
+
columns: {
|
|
38
|
+
id: drizzle_orm_pg_core.PgColumn<{
|
|
39
|
+
name: "id";
|
|
40
|
+
tableName: "tenants";
|
|
41
|
+
dataType: "string";
|
|
42
|
+
columnType: "PgUUID";
|
|
43
|
+
data: string;
|
|
44
|
+
driverParam: string;
|
|
45
|
+
notNull: true;
|
|
46
|
+
hasDefault: true;
|
|
47
|
+
isPrimaryKey: true;
|
|
48
|
+
isAutoincrement: false;
|
|
49
|
+
hasRuntimeDefault: false;
|
|
50
|
+
enumValues: undefined;
|
|
51
|
+
baseColumn: never;
|
|
52
|
+
identity: undefined;
|
|
53
|
+
generated: undefined;
|
|
54
|
+
}, {}, {}>;
|
|
55
|
+
name: drizzle_orm_pg_core.PgColumn<{
|
|
56
|
+
name: "name";
|
|
57
|
+
tableName: "tenants";
|
|
58
|
+
dataType: "string";
|
|
59
|
+
columnType: "PgText";
|
|
60
|
+
data: string;
|
|
61
|
+
driverParam: string;
|
|
62
|
+
notNull: true;
|
|
63
|
+
hasDefault: false;
|
|
64
|
+
isPrimaryKey: false;
|
|
65
|
+
isAutoincrement: false;
|
|
66
|
+
hasRuntimeDefault: false;
|
|
67
|
+
enumValues: [string, ...string[]];
|
|
68
|
+
baseColumn: never;
|
|
69
|
+
identity: undefined;
|
|
70
|
+
generated: undefined;
|
|
71
|
+
}, {}, {}>;
|
|
72
|
+
slug: drizzle_orm_pg_core.PgColumn<{
|
|
73
|
+
name: "slug";
|
|
74
|
+
tableName: "tenants";
|
|
75
|
+
dataType: "string";
|
|
76
|
+
columnType: "PgText";
|
|
77
|
+
data: string;
|
|
78
|
+
driverParam: string;
|
|
79
|
+
notNull: true;
|
|
80
|
+
hasDefault: false;
|
|
81
|
+
isPrimaryKey: false;
|
|
82
|
+
isAutoincrement: false;
|
|
83
|
+
hasRuntimeDefault: false;
|
|
84
|
+
enumValues: [string, ...string[]];
|
|
85
|
+
baseColumn: never;
|
|
86
|
+
identity: undefined;
|
|
87
|
+
generated: undefined;
|
|
88
|
+
}, {}, {}>;
|
|
89
|
+
createdAt: drizzle_orm_pg_core.PgColumn<{
|
|
90
|
+
name: "created_at";
|
|
91
|
+
tableName: "tenants";
|
|
92
|
+
dataType: "date";
|
|
93
|
+
columnType: "PgTimestamp";
|
|
94
|
+
data: Date;
|
|
95
|
+
driverParam: string;
|
|
96
|
+
notNull: true;
|
|
97
|
+
hasDefault: true;
|
|
98
|
+
isPrimaryKey: false;
|
|
99
|
+
isAutoincrement: false;
|
|
100
|
+
hasRuntimeDefault: false;
|
|
101
|
+
enumValues: undefined;
|
|
102
|
+
baseColumn: never;
|
|
103
|
+
identity: undefined;
|
|
104
|
+
generated: undefined;
|
|
105
|
+
}, {}, {}>;
|
|
106
|
+
};
|
|
107
|
+
dialect: "pg";
|
|
108
|
+
}>;
|
|
109
|
+
|
|
110
|
+
/** Table shape expected by createGetTenantRepository: id, name, slug, createdAt (or created_at). */
|
|
111
|
+
type TenantsTableLike = typeof tenantsTable;
|
|
112
|
+
/**
|
|
113
|
+
* Creates getTenantRepository for use with betterTenant({ getTenantRepository }).
|
|
114
|
+
* Pass the tenants table (e.g. tenantsTable from this package or your own with id, name, slug, created_at).
|
|
115
|
+
* The returned function is called by core with the system database handle (from adapter.runAsSystem).
|
|
116
|
+
*/
|
|
117
|
+
declare function createGetTenantRepository(table: TenantsTableLike): (database: SystemDatabase) => TenantRepository;
|
|
118
|
+
|
|
119
|
+
export { type TenantsTableLike, createGetTenantRepository, drizzleAdapter, tenantsTable };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// src/adapter.ts
|
|
2
|
+
import { sql } from "drizzle-orm";
|
|
3
|
+
function drizzleAdapter(database) {
|
|
4
|
+
return {
|
|
5
|
+
async runWithTenant(tenantId, fn) {
|
|
6
|
+
return database.transaction(async (tx) => {
|
|
7
|
+
await tx.execute(
|
|
8
|
+
sql`SELECT set_config('app.current_tenant', ${tenantId}, true)`
|
|
9
|
+
);
|
|
10
|
+
return fn(tx);
|
|
11
|
+
});
|
|
12
|
+
},
|
|
13
|
+
async runAsSystem(fn) {
|
|
14
|
+
return database.transaction(async (tx) => {
|
|
15
|
+
await tx.execute(
|
|
16
|
+
sql`SELECT set_config('app.bypass_rls', 'true', true)`
|
|
17
|
+
);
|
|
18
|
+
return fn(tx);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/schema.ts
|
|
25
|
+
import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
|
26
|
+
var tenantsTable = pgTable("tenants", {
|
|
27
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
28
|
+
name: text("name").notNull(),
|
|
29
|
+
slug: text("slug").notNull().unique(),
|
|
30
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull()
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// src/repository.ts
|
|
34
|
+
import { eq } from "drizzle-orm";
|
|
35
|
+
function rowToTenant(row) {
|
|
36
|
+
return {
|
|
37
|
+
id: String(row.id),
|
|
38
|
+
name: String(row.name),
|
|
39
|
+
slug: String(row.slug),
|
|
40
|
+
createdAt: row.createdAt ?? row.created_at,
|
|
41
|
+
...row
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function createGetTenantRepository(table) {
|
|
45
|
+
return (database) => {
|
|
46
|
+
const db = database;
|
|
47
|
+
return {
|
|
48
|
+
async create(data) {
|
|
49
|
+
const rows = await db.insert(table).values({
|
|
50
|
+
name: data.name,
|
|
51
|
+
slug: data.slug
|
|
52
|
+
}).returning();
|
|
53
|
+
const row = rows[0];
|
|
54
|
+
if (!row || typeof row !== "object") {
|
|
55
|
+
throw new Error("better-tenant: createTenant failed to return row");
|
|
56
|
+
}
|
|
57
|
+
return rowToTenant(row);
|
|
58
|
+
},
|
|
59
|
+
async update(tenantId, data) {
|
|
60
|
+
const set = {};
|
|
61
|
+
if (data.name !== void 0) set.name = data.name;
|
|
62
|
+
if (data.slug !== void 0) set.slug = data.slug;
|
|
63
|
+
if (Object.keys(set).length === 0) {
|
|
64
|
+
const rows2 = await db.select().from(table).where(eq(table.id, tenantId)).limit(1);
|
|
65
|
+
const row2 = Array.isArray(rows2) ? rows2[0] : void 0;
|
|
66
|
+
if (!row2 || typeof row2 !== "object") {
|
|
67
|
+
throw new Error("better-tenant: tenant not found");
|
|
68
|
+
}
|
|
69
|
+
return rowToTenant(row2);
|
|
70
|
+
}
|
|
71
|
+
const rows = await db.update(table).set(set).where(eq(table.id, tenantId)).returning();
|
|
72
|
+
const row = rows[0];
|
|
73
|
+
if (!row || typeof row !== "object") {
|
|
74
|
+
throw new Error("better-tenant: tenant not found");
|
|
75
|
+
}
|
|
76
|
+
return rowToTenant(row);
|
|
77
|
+
},
|
|
78
|
+
async getById(tenantId) {
|
|
79
|
+
const rows = await db.select().from(table).where(eq(table.id, tenantId)).limit(1);
|
|
80
|
+
const row = Array.isArray(rows) ? rows[0] : void 0;
|
|
81
|
+
if (!row || typeof row !== "object") return null;
|
|
82
|
+
return rowToTenant(row);
|
|
83
|
+
},
|
|
84
|
+
async list(options = {}) {
|
|
85
|
+
const limit = Math.min(options.limit ?? 50, 50);
|
|
86
|
+
const offset = Math.max(0, options.offset ?? 0);
|
|
87
|
+
const rows = await db.select().from(table).limit(limit).offset(offset);
|
|
88
|
+
return (Array.isArray(rows) ? rows : []).map(
|
|
89
|
+
(r) => rowToTenant(r)
|
|
90
|
+
);
|
|
91
|
+
},
|
|
92
|
+
async delete(tenantId) {
|
|
93
|
+
await db.delete(table).where(eq(table.id, tenantId));
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export {
|
|
99
|
+
createGetTenantRepository,
|
|
100
|
+
drizzleAdapter,
|
|
101
|
+
tenantsTable
|
|
102
|
+
};
|
|
103
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapter.ts","../src/schema.ts","../src/repository.ts"],"sourcesContent":["import { sql } from \"drizzle-orm\";\nimport type {\n TenantAdapter,\n TenantScopedDatabase,\n SystemDatabase,\n} from \"@usebetterdev/tenant-core\";\n\n/**\n * Minimal shape for a Drizzle pg database: must support transaction()\n * and the transaction runner must support execute(sql).\n */\nexport interface DrizzlePgDatabase {\n transaction<T>(\n callback: (tx: DrizzlePgTransaction) => Promise<T>,\n ): Promise<T>;\n}\n\nexport interface DrizzlePgTransaction {\n execute(query: ReturnType<typeof sql> | unknown): Promise<unknown>;\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(database: DrizzlePgDatabase): TenantAdapter {\n return {\n async runWithTenant<T>(\n tenantId: string,\n fn: (database: TenantScopedDatabase) => 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 as TenantScopedDatabase);\n });\n },\n\n async runAsSystem<T>(\n fn: (database: SystemDatabase) => 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 as SystemDatabase);\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 {\n SystemDatabase,\n Tenant,\n TenantRepository,\n} 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: SystemDatabase) => TenantRepository {\n return (database: SystemDatabase) => {\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\") return null;\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"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@usebetterdev/tenant-drizzle",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"repository": "github:usebetter-dev/usebetter",
|
|
5
|
+
"bugs": "https://github.com/usebetter-dev/usebetter/issues",
|
|
6
|
+
"homepage": "https://github.com/usebetter-dev/usebetter#readme",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public",
|
|
9
|
+
"registry": "https://registry.npmjs.org/"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./dist/index.cjs",
|
|
13
|
+
"module": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js",
|
|
19
|
+
"require": "./dist/index.cjs"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"drizzle-orm": "^0.36.0",
|
|
28
|
+
"@usebetterdev/tenant-core": "0.1.0"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"pg": ">=8.0.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependenciesMeta": {
|
|
34
|
+
"pg": {
|
|
35
|
+
"optional": false
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@testcontainers/postgresql": "^11.11.0",
|
|
40
|
+
"@types/node": "^22.10.0",
|
|
41
|
+
"@types/pg": "^8.11.0",
|
|
42
|
+
"pg": "^8.13.0",
|
|
43
|
+
"tsup": "^8.3.5",
|
|
44
|
+
"typescript": "~5.7.2",
|
|
45
|
+
"vitest": "^2.1.6",
|
|
46
|
+
"@usebetterdev/test-utils": "0.1.0"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=22"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsup",
|
|
53
|
+
"lint": "oxlint",
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"test:integration": "vitest run",
|
|
56
|
+
"typecheck": "tsc --noEmit"
|
|
57
|
+
}
|
|
58
|
+
}
|