@usebetterdev/tenant-cli 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 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,45 @@
1
+ # @better-tenant/cli
2
+
3
+ CLI for multi-tenancy setup: migrate (tenants table + RLS), add-table, generate, check, seed.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @better-tenant/cli
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ npx better-tenant migrate [--dry-run] [-o <dir>]
15
+ npx better-tenant add-table <tableName> [--dry-run] [-o <dir>]
16
+ npx better-tenant generate [--output <dir>]
17
+ npx better-tenant check
18
+ npx better-tenant seed [--dry-run]
19
+ ```
20
+
21
+ Loads config from `better-tenant.config.ts` (or `.js`) in the project root.
22
+
23
+ ## Programmatic API
24
+
25
+ Subpath exports for use in scripts or custom tooling:
26
+
27
+ ```ts
28
+ import { generateMigrationSql } from "@better-tenant/cli/migrate";
29
+ import { runCheck } from "@better-tenant/cli/check";
30
+ import { runSeed } from "@better-tenant/cli/seed";
31
+ ```
32
+
33
+ ## Future: @usebetterdev/cli
34
+
35
+ Planned consolidation: a single `@usebetterdev/cli` package will support all usebetter products. Tenant commands will be invoked as:
36
+
37
+ ```bash
38
+ npx @usebetterdev/cli tenant migrate
39
+ npx @usebetterdev/cli tenant add-table <tableName>
40
+ npx @usebetterdev/cli tenant generate
41
+ npx @usebetterdev/cli tenant check
42
+ npx @usebetterdev/cli tenant seed
43
+ ```
44
+
45
+ This package (`@better-tenant/cli` / `@usebetterdev/tenant-cli`) will be folded into that unified CLI. For now, use the commands above.
@@ -0,0 +1,14 @@
1
+ import { BetterTenantCliConfig } from './config.js';
2
+
3
+ interface CheckResult {
4
+ check: string;
5
+ passed: boolean;
6
+ message?: string;
7
+ }
8
+ interface CheckOutput {
9
+ passed: boolean;
10
+ results: CheckResult[];
11
+ }
12
+ declare function runCheck(databaseUrl: string, config: BetterTenantCliConfig): Promise<CheckOutput>;
13
+
14
+ export { type CheckOutput, type CheckResult, runCheck };
package/dist/check.js ADDED
@@ -0,0 +1,7 @@
1
+ import {
2
+ runCheck
3
+ } from "./chunk-VJ2YHTHF.js";
4
+ export {
5
+ runCheck
6
+ };
7
+ //# sourceMappingURL=check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,98 @@
1
+ // src/config.ts
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { join } from "path";
4
+ var CONFIG_FILE = "better-tenant.config.json";
5
+ var NO_CONFIG_MESSAGE_PREFIX = "better-tenant: No config found.";
6
+ function defaultTenantTables(suggested) {
7
+ if (suggested.length > 0) return suggested;
8
+ return ["projects"];
9
+ }
10
+ function buildNoConfigMessage(cwd, suggestedTables) {
11
+ const tables = defaultTenantTables(suggestedTables ?? []);
12
+ const configJson = JSON.stringify(
13
+ { tenantTables: tables },
14
+ null,
15
+ 2
16
+ );
17
+ const pkgSnippet = tables.length === 1 ? `"betterTenant": { "tenantTables": ["${tables[0]}"] }` : `"betterTenant": { "tenantTables": ${JSON.stringify(tables)} }`;
18
+ return [
19
+ NO_CONFIG_MESSAGE_PREFIX,
20
+ "",
21
+ "Step 1 \u2014 Create a config file in your project root:",
22
+ "",
23
+ ` touch ${CONFIG_FILE}`,
24
+ "",
25
+ "Step 2 \u2014 Paste this into better-tenant.config.json (adjust tenantTables to your table names):",
26
+ "",
27
+ configJson,
28
+ "",
29
+ 'Alternatively, add a "betterTenant" key to your package.json:',
30
+ "",
31
+ ` ${pkgSnippet}`,
32
+ "",
33
+ `(Looked in: ${cwd})`
34
+ ].join("\n");
35
+ }
36
+ function loadConfig(cwd = process.cwd()) {
37
+ const configPath = join(cwd, CONFIG_FILE);
38
+ if (existsSync(configPath)) {
39
+ return loadJsonConfig(configPath);
40
+ }
41
+ const pkgPath = join(cwd, "package.json");
42
+ const config = loadPackageJsonConfig(pkgPath);
43
+ if (config) return config;
44
+ throw new Error(buildNoConfigMessage(cwd, void 0));
45
+ }
46
+ function loadJsonConfig(path) {
47
+ const content = readFileSync(path, "utf-8");
48
+ let raw;
49
+ try {
50
+ raw = JSON.parse(content);
51
+ } catch (e) {
52
+ throw new Error(
53
+ `better-tenant: Invalid JSON in ${path}: ${e instanceof Error ? e.message : String(e)}`
54
+ );
55
+ }
56
+ return normalizeConfig(raw);
57
+ }
58
+ function loadPackageJsonConfig(pkgPath) {
59
+ if (!existsSync(pkgPath)) return null;
60
+ const content = readFileSync(pkgPath, "utf-8");
61
+ let pkg;
62
+ try {
63
+ pkg = JSON.parse(content);
64
+ } catch {
65
+ return null;
66
+ }
67
+ if (!pkg.betterTenant || typeof pkg.betterTenant !== "object") return null;
68
+ return normalizeConfig(pkg.betterTenant);
69
+ }
70
+ function isConfigObject(x) {
71
+ return x != null && typeof x === "object" && !Array.isArray(x);
72
+ }
73
+ function isStringArray(x) {
74
+ return Array.isArray(x) && x.every((t) => typeof t === "string");
75
+ }
76
+ function normalizeConfig(raw) {
77
+ if (!isConfigObject(raw)) {
78
+ throw new Error("better-tenant: config must be an object");
79
+ }
80
+ const tenantTables = raw.tenantTables;
81
+ if (!isStringArray(tenantTables)) {
82
+ throw new Error(
83
+ "better-tenant: config must have tenantTables (string[])"
84
+ );
85
+ }
86
+ return {
87
+ tenantTables,
88
+ schemaDir: typeof raw.schemaDir === "string" ? raw.schemaDir : void 0,
89
+ migrationsDir: typeof raw.migrationsDir === "string" ? raw.migrationsDir : void 0
90
+ };
91
+ }
92
+
93
+ export {
94
+ NO_CONFIG_MESSAGE_PREFIX,
95
+ buildNoConfigMessage,
96
+ loadConfig
97
+ };
98
+ //# sourceMappingURL=chunk-64RQTBD5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/**\n * CLI config shape. Matches core's BetterTenantConfig subset needed for migrate/generate/check.\n */\nexport interface BetterTenantCliConfig {\n tenantTables: string[];\n schemaDir?: string;\n migrationsDir?: string;\n}\n\nconst CONFIG_FILE = \"better-tenant.config.json\";\n\n/** Prefix of the \"no config\" error message. Use to detect and enrich the error in commands that have a DB URL. */\nexport const NO_CONFIG_MESSAGE_PREFIX = \"better-tenant: No config found.\";\n\nfunction defaultTenantTables(suggested: string[]): string[] {\n if (suggested.length > 0) return suggested;\n return [\"projects\"];\n}\n\n/**\n * Build the \"no config\" error message. When suggestedTables is provided (e.g. from getSuggestedTables),\n * the snippet uses those table names instead of the default.\n */\nexport function buildNoConfigMessage(\n cwd: string,\n suggestedTables?: string[],\n): string {\n const tables = defaultTenantTables(suggestedTables ?? []);\n const configJson = JSON.stringify(\n { tenantTables: tables },\n null,\n 2,\n );\n const pkgSnippet =\n tables.length === 1\n ? `\"betterTenant\": { \"tenantTables\": [\"${tables[0]}\"] }`\n : `\"betterTenant\": { \"tenantTables\": ${JSON.stringify(tables)} }`;\n\n return [\n NO_CONFIG_MESSAGE_PREFIX,\n \"\",\n \"Step 1 — Create a config file in your project root:\",\n \"\",\n ` touch ${CONFIG_FILE}`,\n \"\",\n \"Step 2 — Paste this into better-tenant.config.json (adjust tenantTables to your table names):\",\n \"\",\n configJson,\n \"\",\n \"Alternatively, add a \\\"betterTenant\\\" key to your package.json:\",\n \"\",\n ` ${pkgSnippet}`,\n \"\",\n `(Looked in: ${cwd})`,\n ].join(\"\\n\");\n}\n\n/**\n * Discover and load config from cwd.\n * 1. Look for better-tenant.config.json in cwd\n * 2. Fall back to package.json \"betterTenant\" key\n */\nexport function loadConfig(cwd: string = process.cwd()): BetterTenantCliConfig {\n const configPath = join(cwd, CONFIG_FILE);\n if (existsSync(configPath)) {\n return loadJsonConfig(configPath);\n }\n\n const pkgPath = join(cwd, \"package.json\");\n const config = loadPackageJsonConfig(pkgPath);\n if (config) return config;\n\n throw new Error(buildNoConfigMessage(cwd, undefined));\n}\n\nfunction loadJsonConfig(path: string): BetterTenantCliConfig {\n const content = readFileSync(path, \"utf-8\");\n let raw: unknown;\n try {\n raw = JSON.parse(content);\n } catch (e) {\n throw new Error(\n `better-tenant: Invalid JSON in ${path}: ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n return normalizeConfig(raw);\n}\n\nfunction loadPackageJsonConfig(pkgPath: string): BetterTenantCliConfig | null {\n if (!existsSync(pkgPath)) return null;\n\n const content = readFileSync(pkgPath, \"utf-8\");\n let pkg: { betterTenant?: unknown };\n try {\n pkg = JSON.parse(content);\n } catch {\n return null;\n }\n if (!pkg.betterTenant || typeof pkg.betterTenant !== \"object\") return null;\n\n return normalizeConfig(pkg.betterTenant);\n}\n\nfunction isConfigObject(x: unknown): x is Record<string, unknown> {\n return x != null && typeof x === \"object\" && !Array.isArray(x);\n}\n\nfunction isStringArray(x: unknown): x is string[] {\n return Array.isArray(x) && x.every((t) => typeof t === \"string\");\n}\n\nfunction normalizeConfig(raw: unknown): BetterTenantCliConfig {\n if (!isConfigObject(raw)) {\n throw new Error(\"better-tenant: config must be an object\");\n }\n\n const tenantTables = raw.tenantTables;\n if (!isStringArray(tenantTables)) {\n throw new Error(\n \"better-tenant: config must have tenantTables (string[])\",\n );\n }\n\n return {\n tenantTables,\n schemaDir: typeof raw.schemaDir === \"string\" ? raw.schemaDir : undefined,\n migrationsDir:\n typeof raw.migrationsDir === \"string\" ? raw.migrationsDir : undefined,\n };\n}\n"],"mappings":";AAAA,SAAS,YAAY,oBAAoB;AACzC,SAAS,YAAY;AAWrB,IAAM,cAAc;AAGb,IAAM,2BAA2B;AAExC,SAAS,oBAAoB,WAA+B;AAC1D,MAAI,UAAU,SAAS,EAAG,QAAO;AACjC,SAAO,CAAC,UAAU;AACpB;AAMO,SAAS,qBACd,KACA,iBACQ;AACR,QAAM,SAAS,oBAAoB,mBAAmB,CAAC,CAAC;AACxD,QAAM,aAAa,KAAK;AAAA,IACtB,EAAE,cAAc,OAAO;AAAA,IACvB;AAAA,IACA;AAAA,EACF;AACA,QAAM,aACJ,OAAO,WAAW,IACd,uCAAuC,OAAO,CAAC,CAAC,SAChD,qCAAqC,KAAK,UAAU,MAAM,CAAC;AAEjE,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW,WAAW;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,KAAK,UAAU;AAAA,IACf;AAAA,IACA,eAAe,GAAG;AAAA,EACpB,EAAE,KAAK,IAAI;AACb;AAOO,SAAS,WAAW,MAAc,QAAQ,IAAI,GAA0B;AAC7E,QAAM,aAAa,KAAK,KAAK,WAAW;AACxC,MAAI,WAAW,UAAU,GAAG;AAC1B,WAAO,eAAe,UAAU;AAAA,EAClC;AAEA,QAAM,UAAU,KAAK,KAAK,cAAc;AACxC,QAAM,SAAS,sBAAsB,OAAO;AAC5C,MAAI,OAAQ,QAAO;AAEnB,QAAM,IAAI,MAAM,qBAAqB,KAAK,MAAS,CAAC;AACtD;AAEA,SAAS,eAAe,MAAqC;AAC3D,QAAM,UAAU,aAAa,MAAM,OAAO;AAC1C,MAAI;AACJ,MAAI;AACF,UAAM,KAAK,MAAM,OAAO;AAAA,EAC1B,SAAS,GAAG;AACV,UAAM,IAAI;AAAA,MACR,kCAAkC,IAAI,KAAK,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,IACvF;AAAA,EACF;AACA,SAAO,gBAAgB,GAAG;AAC5B;AAEA,SAAS,sBAAsB,SAA+C;AAC5E,MAAI,CAAC,WAAW,OAAO,EAAG,QAAO;AAEjC,QAAM,UAAU,aAAa,SAAS,OAAO;AAC7C,MAAI;AACJ,MAAI;AACF,UAAM,KAAK,MAAM,OAAO;AAAA,EAC1B,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,CAAC,IAAI,gBAAgB,OAAO,IAAI,iBAAiB,SAAU,QAAO;AAEtE,SAAO,gBAAgB,IAAI,YAAY;AACzC;AAEA,SAAS,eAAe,GAA0C;AAChE,SAAO,KAAK,QAAQ,OAAO,MAAM,YAAY,CAAC,MAAM,QAAQ,CAAC;AAC/D;AAEA,SAAS,cAAc,GAA2B;AAChD,SAAO,MAAM,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,OAAO,MAAM,QAAQ;AACjE;AAEA,SAAS,gBAAgB,KAAqC;AAC5D,MAAI,CAAC,eAAe,GAAG,GAAG;AACxB,UAAM,IAAI,MAAM,yCAAyC;AAAA,EAC3D;AAEA,QAAM,eAAe,IAAI;AACzB,MAAI,CAAC,cAAc,YAAY,GAAG;AAChC,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA,WAAW,OAAO,IAAI,cAAc,WAAW,IAAI,YAAY;AAAA,IAC/D,eACE,OAAO,IAAI,kBAAkB,WAAW,IAAI,gBAAgB;AAAA,EAChE;AACF;","names":[]}
@@ -0,0 +1,40 @@
1
+ // src/seed.ts
2
+ import { Pool } from "pg";
3
+ function slugFromName(name) {
4
+ return name.toLowerCase().trim().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
5
+ }
6
+ async function runSeed(databaseUrl, options) {
7
+ const slug = options.slug ?? slugFromName(options.name);
8
+ if (!slug) {
9
+ throw new Error("seed: slug is required (provide --slug or use a name that yields a non-empty slug)");
10
+ }
11
+ const pool = new Pool({ connectionString: databaseUrl });
12
+ const client = await pool.connect();
13
+ try {
14
+ await client.query("BEGIN");
15
+ await client.query("SET LOCAL app.bypass_rls = 'true'");
16
+ const r = await client.query(
17
+ `INSERT INTO tenants (name, slug) VALUES ($1, $2)
18
+ RETURNING id::text AS id, name, slug`,
19
+ [options.name, slug]
20
+ );
21
+ await client.query("COMMIT");
22
+ const row = r.rows[0];
23
+ if (!row) {
24
+ throw new Error("seed: insert did not return row");
25
+ }
26
+ return { id: row.id, name: row.name, slug: row.slug };
27
+ } catch (err) {
28
+ await client.query("ROLLBACK");
29
+ throw err;
30
+ } finally {
31
+ client.release();
32
+ await pool.end();
33
+ }
34
+ }
35
+
36
+ export {
37
+ slugFromName,
38
+ runSeed
39
+ };
40
+ //# sourceMappingURL=chunk-LXS6CXJ3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/seed.ts"],"sourcesContent":["import { Pool } from \"pg\";\n\nexport interface SeedOptions {\n name: string;\n slug?: string;\n}\n\n/**\n * Derive a URL-safe slug from name (e.g. \"Test Org\" -> \"test-org\").\n */\nexport function slugFromName(name: string): string {\n return name\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, \"-\")\n .replace(/[^a-z0-9-]/g, \"\");\n}\n\nexport interface SeedResult {\n id: string;\n name: string;\n slug: string;\n}\n\n/**\n * Insert one tenant using bypass_rls so RLS allows the insert (no superuser).\n * Uses SET LOCAL app.bypass_rls = 'true' in a transaction.\n */\nexport async function runSeed(\n databaseUrl: string,\n options: SeedOptions,\n): Promise<SeedResult> {\n const slug = options.slug ?? slugFromName(options.name);\n if (!slug) {\n throw new Error(\"seed: slug is required (provide --slug or use a name that yields a non-empty slug)\");\n }\n\n const pool = new Pool({ connectionString: databaseUrl });\n const client = await pool.connect();\n\n try {\n await client.query(\"BEGIN\");\n await client.query(\"SET LOCAL app.bypass_rls = 'true'\");\n\n const r = await client.query(\n `INSERT INTO tenants (name, slug) VALUES ($1, $2)\n RETURNING id::text AS id, name, slug`,\n [options.name, slug],\n );\n\n await client.query(\"COMMIT\");\n\n const row = r.rows[0] as { id: string; name: string; slug: string } | undefined;\n if (!row) {\n throw new Error(\"seed: insert did not return row\");\n }\n return { id: row.id, name: row.name, slug: row.slug };\n } catch (err) {\n await client.query(\"ROLLBACK\");\n throw err;\n } finally {\n client.release();\n await pool.end();\n }\n}\n"],"mappings":";AAAA,SAAS,YAAY;AAUd,SAAS,aAAa,MAAsB;AACjD,SAAO,KACJ,YAAY,EACZ,KAAK,EACL,QAAQ,QAAQ,GAAG,EACnB,QAAQ,eAAe,EAAE;AAC9B;AAYA,eAAsB,QACpB,aACA,SACqB;AACrB,QAAM,OAAO,QAAQ,QAAQ,aAAa,QAAQ,IAAI;AACtD,MAAI,CAAC,MAAM;AACT,UAAM,IAAI,MAAM,oFAAoF;AAAA,EACtG;AAEA,QAAM,OAAO,IAAI,KAAK,EAAE,kBAAkB,YAAY,CAAC;AACvD,QAAM,SAAS,MAAM,KAAK,QAAQ;AAElC,MAAI;AACF,UAAM,OAAO,MAAM,OAAO;AAC1B,UAAM,OAAO,MAAM,mCAAmC;AAEtD,UAAM,IAAI,MAAM,OAAO;AAAA,MACrB;AAAA;AAAA,MAEA,CAAC,QAAQ,MAAM,IAAI;AAAA,IACrB;AAEA,UAAM,OAAO,MAAM,QAAQ;AAE3B,UAAM,MAAM,EAAE,KAAK,CAAC;AACpB,QAAI,CAAC,KAAK;AACR,YAAM,IAAI,MAAM,iCAAiC;AAAA,IACnD;AACA,WAAO,EAAE,IAAI,IAAI,IAAI,MAAM,IAAI,MAAM,MAAM,IAAI,KAAK;AAAA,EACtD,SAAS,KAAK;AACZ,UAAM,OAAO,MAAM,UAAU;AAC7B,UAAM;AAAA,EACR,UAAE;AACA,WAAO,QAAQ;AACf,UAAM,KAAK,IAAI;AAAA,EACjB;AACF;","names":[]}
@@ -0,0 +1,133 @@
1
+ // src/check.ts
2
+ import { Pool } from "pg";
3
+ var TENANTS_COLUMNS = ["id", "name", "slug", "created_at"];
4
+ async function checkTenantsTable(pool) {
5
+ const results = [];
6
+ const r = await pool.query(
7
+ `
8
+ SELECT column_name FROM information_schema.columns
9
+ WHERE table_schema = 'public' AND table_name = 'tenants'
10
+ ORDER BY ordinal_position
11
+ `
12
+ );
13
+ const cols = r.rows.map((row) => row.column_name);
14
+ if (cols.length === 0) {
15
+ results.push({ check: "tenants table exists", passed: false, message: "tenants table not found" });
16
+ return results;
17
+ }
18
+ results.push({ check: "tenants table exists", passed: true });
19
+ for (const col of TENANTS_COLUMNS) {
20
+ const ok = cols.includes(col);
21
+ results.push({
22
+ check: `tenants.${col} column`,
23
+ passed: ok,
24
+ message: ok ? void 0 : `tenants table missing column: ${col}`
25
+ });
26
+ }
27
+ return results;
28
+ }
29
+ async function checkTenantTable(pool, tableName) {
30
+ const results = [];
31
+ const prefix = `table ${tableName}`;
32
+ const tableExists = await pool.query(
33
+ `SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = $1`,
34
+ [tableName]
35
+ );
36
+ if (tableExists.rows.length === 0) {
37
+ results.push({ check: `${prefix} exists`, passed: false, message: `table ${tableName} not found` });
38
+ return results;
39
+ }
40
+ results.push({ check: `${prefix} exists`, passed: true });
41
+ const cols = await pool.query(
42
+ `SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1`,
43
+ [tableName]
44
+ );
45
+ const colNames = cols.rows.map((r) => r.column_name);
46
+ const hasTenantId = colNames.includes("tenant_id");
47
+ results.push({
48
+ check: `${prefix}.tenant_id`,
49
+ passed: hasTenantId,
50
+ message: hasTenantId ? void 0 : `column tenant_id not found`
51
+ });
52
+ const rls = await pool.query(
53
+ `SELECT relrowsecurity, relforcerowsecurity FROM pg_class c
54
+ JOIN pg_namespace n ON c.relnamespace = n.oid
55
+ WHERE n.nspname = 'public' AND c.relname = $1`,
56
+ [tableName]
57
+ );
58
+ const rlsRow = rls.rows[0];
59
+ const rlsOn = rlsRow?.relrowsecurity === true;
60
+ const forceRls = rlsRow?.relforcerowsecurity === true;
61
+ results.push({
62
+ check: `${prefix} RLS enabled`,
63
+ passed: rlsOn,
64
+ message: rlsOn ? void 0 : "ROW LEVEL SECURITY not enabled"
65
+ });
66
+ results.push({
67
+ check: `${prefix} FORCE ROW LEVEL SECURITY`,
68
+ passed: forceRls,
69
+ message: forceRls ? void 0 : "FORCE ROW LEVEL SECURITY not enabled"
70
+ });
71
+ const policies = await pool.query(
72
+ `SELECT qual, with_check FROM pg_policies WHERE schemaname = 'public' AND tablename = $1`,
73
+ [tableName]
74
+ );
75
+ const hasPolicy = policies.rows.length > 0;
76
+ const policyRow = policies.rows[0];
77
+ const hasUsing = hasPolicy && policyRow?.qual != null && policyRow.qual.length > 0;
78
+ const hasWithCheck = hasPolicy && policyRow?.with_check != null && policyRow.with_check.length > 0;
79
+ const hasBypass = hasPolicy && ((policyRow?.qual?.includes("bypass_rls") ?? false) || (policyRow?.with_check?.includes("bypass_rls") ?? false));
80
+ results.push({
81
+ check: `${prefix} policy exists`,
82
+ passed: hasPolicy,
83
+ message: hasPolicy ? void 0 : "no RLS policy found"
84
+ });
85
+ results.push({
86
+ check: `${prefix} policy USING clause`,
87
+ passed: hasUsing,
88
+ message: hasUsing ? void 0 : "policy missing USING expression"
89
+ });
90
+ results.push({
91
+ check: `${prefix} policy WITH CHECK clause`,
92
+ passed: hasWithCheck,
93
+ message: hasWithCheck ? void 0 : "policy missing WITH CHECK expression"
94
+ });
95
+ results.push({
96
+ check: `${prefix} bypass_rls in policy`,
97
+ passed: hasBypass,
98
+ message: hasBypass ? void 0 : "policy should allow bypass_rls for runAsSystem"
99
+ });
100
+ const trigger = await pool.query(
101
+ `SELECT 1 FROM pg_trigger t
102
+ JOIN pg_class c ON t.tgrelid = c.oid
103
+ JOIN pg_namespace n ON c.relnamespace = n.oid
104
+ WHERE n.nspname = 'public' AND c.relname = $1 AND t.tgname = 'set_tenant_id_trigger'`,
105
+ [tableName]
106
+ );
107
+ const hasTrigger = trigger.rows.length > 0;
108
+ results.push({
109
+ check: `${prefix} set_tenant_id trigger`,
110
+ passed: hasTrigger,
111
+ message: hasTrigger ? void 0 : "trigger set_tenant_id_trigger not found"
112
+ });
113
+ return results;
114
+ }
115
+ async function runCheck(databaseUrl, config) {
116
+ const pool = new Pool({ connectionString: databaseUrl });
117
+ const results = [];
118
+ try {
119
+ results.push(...await checkTenantsTable(pool));
120
+ for (const table of config.tenantTables) {
121
+ results.push(...await checkTenantTable(pool, table));
122
+ }
123
+ } finally {
124
+ await pool.end();
125
+ }
126
+ const passed = results.every((r) => r.passed);
127
+ return { passed, results };
128
+ }
129
+
130
+ export {
131
+ runCheck
132
+ };
133
+ //# sourceMappingURL=chunk-VJ2YHTHF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/check.ts"],"sourcesContent":["import { Pool } from \"pg\";\nimport type { BetterTenantCliConfig } from \"./config.js\";\n\nexport interface CheckResult {\n check: string;\n passed: boolean;\n message?: string;\n}\n\nconst TENANTS_COLUMNS = [\"id\", \"name\", \"slug\", \"created_at\"];\n\nasync function checkTenantsTable(pool: Pool): Promise<CheckResult[]> {\n const results: CheckResult[] = [];\n const r = await pool.query(\n `\n SELECT column_name FROM information_schema.columns\n WHERE table_schema = 'public' AND table_name = 'tenants'\n ORDER BY ordinal_position\n `,\n );\n const cols = (r.rows as { column_name: string }[]).map((row) => row.column_name);\n\n if (cols.length === 0) {\n results.push({ check: \"tenants table exists\", passed: false, message: \"tenants table not found\" });\n return results;\n }\n results.push({ check: \"tenants table exists\", passed: true });\n\n for (const col of TENANTS_COLUMNS) {\n const ok = cols.includes(col);\n results.push({\n check: `tenants.${col} column`,\n passed: ok,\n message: ok ? undefined : `tenants table missing column: ${col}`,\n });\n }\n return results;\n}\n\nasync function checkTenantTable(\n pool: Pool,\n tableName: string,\n): Promise<CheckResult[]> {\n const results: CheckResult[] = [];\n const prefix = `table ${tableName}`;\n\n // Table exists?\n const tableExists = await pool.query(\n `SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = $1`,\n [tableName],\n );\n if (tableExists.rows.length === 0) {\n results.push({ check: `${prefix} exists`, passed: false, message: `table ${tableName} not found` });\n return results;\n }\n results.push({ check: `${prefix} exists`, passed: true });\n\n // tenant_id column\n const cols = await pool.query(\n `SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1`,\n [tableName],\n );\n const colNames = (cols.rows as { column_name: string }[]).map((r) => r.column_name);\n const hasTenantId = colNames.includes(\"tenant_id\");\n results.push({\n check: `${prefix}.tenant_id`,\n passed: hasTenantId,\n message: hasTenantId ? undefined : `column tenant_id not found`,\n });\n\n // RLS enabled\n const rls = await pool.query(\n `SELECT relrowsecurity, relforcerowsecurity FROM pg_class c\n JOIN pg_namespace n ON c.relnamespace = n.oid\n WHERE n.nspname = 'public' AND c.relname = $1`,\n [tableName],\n );\n const rlsRow = rls.rows[0] as { relrowsecurity: boolean; relforcerowsecurity: boolean } | undefined;\n const rlsOn = rlsRow?.relrowsecurity === true;\n const forceRls = rlsRow?.relforcerowsecurity === true;\n results.push({\n check: `${prefix} RLS enabled`,\n passed: rlsOn,\n message: rlsOn ? undefined : \"ROW LEVEL SECURITY not enabled\",\n });\n results.push({\n check: `${prefix} FORCE ROW LEVEL SECURITY`,\n passed: forceRls,\n message: forceRls ? undefined : \"FORCE ROW LEVEL SECURITY not enabled\",\n });\n\n // Policy with USING and WITH CHECK\n const policies = await pool.query(\n `SELECT qual, with_check FROM pg_policies WHERE schemaname = 'public' AND tablename = $1`,\n [tableName],\n );\n const hasPolicy = policies.rows.length > 0;\n const policyRow = policies.rows[0] as { qual: string | null; with_check: string | null } | undefined;\n const hasUsing = hasPolicy && policyRow?.qual != null && policyRow.qual.length > 0;\n const hasWithCheck = hasPolicy && policyRow?.with_check != null && policyRow.with_check.length > 0;\n const hasBypass = hasPolicy && (\n (policyRow?.qual?.includes(\"bypass_rls\") ?? false) ||\n (policyRow?.with_check?.includes(\"bypass_rls\") ?? false)\n );\n\n results.push({\n check: `${prefix} policy exists`,\n passed: hasPolicy,\n message: hasPolicy ? undefined : \"no RLS policy found\",\n });\n results.push({\n check: `${prefix} policy USING clause`,\n passed: hasUsing,\n message: hasUsing ? undefined : \"policy missing USING expression\",\n });\n results.push({\n check: `${prefix} policy WITH CHECK clause`,\n passed: hasWithCheck,\n message: hasWithCheck ? undefined : \"policy missing WITH CHECK expression\",\n });\n results.push({\n check: `${prefix} bypass_rls in policy`,\n passed: hasBypass,\n message: hasBypass ? undefined : \"policy should allow bypass_rls for runAsSystem\",\n });\n\n // Trigger\n const trigger = await pool.query(\n `SELECT 1 FROM pg_trigger t\n JOIN pg_class c ON t.tgrelid = c.oid\n JOIN pg_namespace n ON c.relnamespace = n.oid\n WHERE n.nspname = 'public' AND c.relname = $1 AND t.tgname = 'set_tenant_id_trigger'`,\n [tableName],\n );\n const hasTrigger = trigger.rows.length > 0;\n results.push({\n check: `${prefix} set_tenant_id trigger`,\n passed: hasTrigger,\n message: hasTrigger ? undefined : \"trigger set_tenant_id_trigger not found\",\n });\n\n return results;\n}\n\nexport interface CheckOutput {\n passed: boolean;\n results: CheckResult[];\n}\n\nexport async function runCheck(\n databaseUrl: string,\n config: BetterTenantCliConfig,\n): Promise<CheckOutput> {\n const pool = new Pool({ connectionString: databaseUrl });\n const results: CheckResult[] = [];\n\n try {\n results.push(...(await checkTenantsTable(pool)));\n for (const table of config.tenantTables) {\n results.push(...(await checkTenantTable(pool, table)));\n }\n } finally {\n await pool.end();\n }\n\n const passed = results.every((r) => r.passed);\n return { passed, results };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,45 @@
1
+ // src/migrate.ts
2
+ import { readFileSync } from "fs";
3
+ import { join, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+ var __dirname = dirname(fileURLToPath(import.meta.url));
6
+ function loadSql(name) {
7
+ return readFileSync(join(__dirname, "sql", name), "utf-8");
8
+ }
9
+ function quoteIdent(name) {
10
+ return `"${name.replace(/"/g, '""')}"`;
11
+ }
12
+ function tableRlsSql(tableName) {
13
+ const tableQuoted = quoteIdent(tableName);
14
+ const policyName = `rls_tenant_${tableName.replace(/[^a-zA-Z0-9_]/g, "_")}`;
15
+ return loadSql("table_rls.sql").replace(/\{\{TABLE_QUOTED\}\}/g, tableQuoted).replace(/\{\{POLICY_NAME\}\}/g, policyName).replace(/\{\{TABLE\}\}/g, tableName);
16
+ }
17
+ function generateMigrationSql(config) {
18
+ const parts = [
19
+ "-- Better Tenant migration",
20
+ "-- Apply with: psql $DATABASE_URL -f this_file.sql",
21
+ "",
22
+ loadSql("tenants.sql"),
23
+ loadSql("set_tenant_id_function.sql")
24
+ ];
25
+ for (const table of config.tenantTables) {
26
+ parts.push(tableRlsSql(table));
27
+ }
28
+ return parts.join("\n");
29
+ }
30
+ function generateAddTableSql(tableName) {
31
+ const parts = [
32
+ `-- Better Tenant: add-table ${tableName}`,
33
+ "-- Assumes tenants table exists. Run migrate first if needed.",
34
+ "",
35
+ loadSql("set_tenant_id_function.sql"),
36
+ tableRlsSql(tableName)
37
+ ];
38
+ return parts.join("\n");
39
+ }
40
+
41
+ export {
42
+ generateMigrationSql,
43
+ generateAddTableSql
44
+ };
45
+ //# sourceMappingURL=chunk-VZCKMMJZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/migrate.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { BetterTenantCliConfig } from \"./config.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\nfunction loadSql(name: string): string {\n return readFileSync(join(__dirname, \"sql\", name), \"utf-8\");\n}\n\nfunction quoteIdent(name: string): string {\n return `\"${name.replace(/\"/g, '\"\"')}\"`;\n}\n\nfunction tableRlsSql(tableName: string): string {\n const tableQuoted = quoteIdent(tableName);\n const policyName = `rls_tenant_${tableName.replace(/[^a-zA-Z0-9_]/g, \"_\")}`;\n return loadSql(\"table_rls.sql\")\n .replace(/\\{\\{TABLE_QUOTED\\}\\}/g, tableQuoted)\n .replace(/\\{\\{POLICY_NAME\\}\\}/g, policyName)\n .replace(/\\{\\{TABLE\\}\\}/g, tableName);\n}\n\nexport function generateMigrationSql(config: BetterTenantCliConfig): string {\n const parts: string[] = [\n \"-- Better Tenant migration\",\n \"-- Apply with: psql $DATABASE_URL -f this_file.sql\",\n \"\",\n loadSql(\"tenants.sql\"),\n loadSql(\"set_tenant_id_function.sql\"),\n ];\n for (const table of config.tenantTables) {\n parts.push(tableRlsSql(table));\n }\n return parts.join(\"\\n\");\n}\n\n/**\n * Generate SQL to add tenant_id, RLS, policy, and trigger for a single table.\n * Idempotent: uses IF NOT EXISTS for column, DROP IF EXISTS for policy/trigger.\n * Assumes tenants table and set_tenant_id() function exist (run migrate first).\n */\nexport function generateAddTableSql(tableName: string): string {\n const parts: string[] = [\n `-- Better Tenant: add-table ${tableName}`,\n \"-- Assumes tenants table exists. Run migrate first if needed.\",\n \"\",\n loadSql(\"set_tenant_id_function.sql\"),\n tableRlsSql(tableName),\n ];\n return parts.join(\"\\n\");\n}\n"],"mappings":";AAAA,SAAS,oBAAoB;AAC7B,SAAS,MAAM,eAAe;AAC9B,SAAS,qBAAqB;AAG9B,IAAM,YAAY,QAAQ,cAAc,YAAY,GAAG,CAAC;AAExD,SAAS,QAAQ,MAAsB;AACrC,SAAO,aAAa,KAAK,WAAW,OAAO,IAAI,GAAG,OAAO;AAC3D;AAEA,SAAS,WAAW,MAAsB;AACxC,SAAO,IAAI,KAAK,QAAQ,MAAM,IAAI,CAAC;AACrC;AAEA,SAAS,YAAY,WAA2B;AAC9C,QAAM,cAAc,WAAW,SAAS;AACxC,QAAM,aAAa,cAAc,UAAU,QAAQ,kBAAkB,GAAG,CAAC;AACzE,SAAO,QAAQ,eAAe,EAC3B,QAAQ,yBAAyB,WAAW,EAC5C,QAAQ,wBAAwB,UAAU,EAC1C,QAAQ,kBAAkB,SAAS;AACxC;AAEO,SAAS,qBAAqB,QAAuC;AAC1E,QAAM,QAAkB;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ,aAAa;AAAA,IACrB,QAAQ,4BAA4B;AAAA,EACtC;AACA,aAAW,SAAS,OAAO,cAAc;AACvC,UAAM,KAAK,YAAY,KAAK,CAAC;AAAA,EAC/B;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;AAOO,SAAS,oBAAoB,WAA2B;AAC7D,QAAM,QAAkB;AAAA,IACtB,+BAA+B,SAAS;AAAA,IACxC;AAAA,IACA;AAAA,IACA,QAAQ,4BAA4B;AAAA,IACpC,YAAY,SAAS;AAAA,EACvB;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;","names":[]}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+
4
+ declare const program: Command;
5
+
6
+ export { program };
package/dist/cli.js ADDED
@@ -0,0 +1,322 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ runCheck
4
+ } from "./chunk-VJ2YHTHF.js";
5
+ import {
6
+ NO_CONFIG_MESSAGE_PREFIX,
7
+ buildNoConfigMessage,
8
+ loadConfig
9
+ } from "./chunk-64RQTBD5.js";
10
+ import {
11
+ generateAddTableSql,
12
+ generateMigrationSql
13
+ } from "./chunk-VZCKMMJZ.js";
14
+ import {
15
+ runSeed
16
+ } from "./chunk-LXS6CXJ3.js";
17
+
18
+ // src/cli.ts
19
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
20
+ import { dirname, join } from "path";
21
+ import { Command } from "commander";
22
+ import { sendCliTelemetry } from "@usebetterdev/tenant-core";
23
+
24
+ // src/suggested-tables.ts
25
+ import { Pool } from "pg";
26
+ var TABLE_PRIORITY = [
27
+ "users",
28
+ "projects",
29
+ "tasks",
30
+ "organizations",
31
+ "members",
32
+ "workspaces",
33
+ "teams",
34
+ "issues",
35
+ "posts",
36
+ "products",
37
+ "orders",
38
+ "accounts",
39
+ "invitations"
40
+ ];
41
+ var SUGGESTED_LIMIT = 5;
42
+ function sortTablesByPriority(tables) {
43
+ const order = new Map(TABLE_PRIORITY.map((t, i) => [t, i]));
44
+ return [...tables].sort((a, b) => {
45
+ const ia = order.get(a) ?? TABLE_PRIORITY.length;
46
+ const ib = order.get(b) ?? TABLE_PRIORITY.length;
47
+ if (ia !== ib) return ia - ib;
48
+ return a.localeCompare(b);
49
+ });
50
+ }
51
+ async function getSuggestedTables(databaseUrl) {
52
+ const pool = new Pool({ connectionString: databaseUrl });
53
+ try {
54
+ const r = await pool.query(
55
+ `SELECT table_name FROM information_schema.tables
56
+ WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
57
+ AND table_name != 'tenants'
58
+ ORDER BY table_name`
59
+ );
60
+ const names = (r.rows ?? []).map((row) => row.table_name);
61
+ const sorted = sortTablesByPriority(names);
62
+ return sorted.slice(0, SUGGESTED_LIMIT);
63
+ } catch {
64
+ return [];
65
+ } finally {
66
+ await pool.end();
67
+ }
68
+ }
69
+
70
+ // src/generate.ts
71
+ function toTableVar(tableName) {
72
+ const base = tableName.replace(/[^a-zA-Z0-9]/g, "_");
73
+ return `${base}Table`;
74
+ }
75
+ function generateDrizzleSchema(config) {
76
+ const lines = [
77
+ "// Better Tenant: Drizzle schema snippet",
78
+ "// Add tenantId column and relations to each tenant-scoped table.",
79
+ "// The .default(sql`...`) tells Drizzle that tenant_id is auto-populated",
80
+ "// by the set_tenant_id trigger, so you can omit it from .insert().values().",
81
+ "// Merge into your schema. Import tenantsTable from your tenants definition.",
82
+ "",
83
+ "import { sql, relations } from 'drizzle-orm';",
84
+ "import { uuid } from 'drizzle-orm/pg-core';",
85
+ "import { tenantsTable } from './tenants'; // adjust import path",
86
+ ""
87
+ ];
88
+ for (const tableName of config.tenantTables) {
89
+ const tableVar = toTableVar(tableName);
90
+ const relationsName = `${tableName.replace(/[^a-zA-Z0-9]/g, "_")}Relations`;
91
+ lines.push(`// --- Table: ${tableName} ---`);
92
+ lines.push(`// Add to ${tableVar} pgTable definition:`);
93
+ lines.push(
94
+ "// tenantId: uuid(\"tenant_id\").notNull().default(sql`(current_setting('app.current_tenant', true))::uuid`),"
95
+ );
96
+ lines.push("");
97
+ lines.push(`// Relations for ${tableName}:`);
98
+ lines.push(
99
+ `export const ${relationsName} = relations(${tableVar}, ({ one }) => ({`
100
+ );
101
+ lines.push(` tenant: one(tenantsTable),`);
102
+ lines.push(`}));`);
103
+ lines.push("");
104
+ }
105
+ return lines.join("\n").trimEnd();
106
+ }
107
+
108
+ // src/cli.ts
109
+ function formatDate() {
110
+ const d = /* @__PURE__ */ new Date();
111
+ const y = d.getFullYear();
112
+ const m = String(d.getMonth() + 1).padStart(2, "0");
113
+ const day = String(d.getDate()).padStart(2, "0");
114
+ return `${y}${m}${day}`;
115
+ }
116
+ var program = new Command();
117
+ program.name("better-tenant").description("Multi-tenancy for Postgres").version("0.1.0");
118
+ program.command("migrate").description("Generate SQL migration for tenants table and RLS").option("--dry-run", "Print SQL to stdout").option("-o, --output <dir>", "Write migration file to directory").action(async (opts) => {
119
+ try {
120
+ const cwd = process.cwd();
121
+ const config = loadConfig(cwd);
122
+ const sql = generateMigrationSql(config);
123
+ if (opts.dryRun) {
124
+ process.stdout.write(sql);
125
+ sendCliTelemetry("cli_migrate", "no_changes", config);
126
+ return;
127
+ }
128
+ if (opts.output) {
129
+ mkdirSync(opts.output, { recursive: true });
130
+ const filename = `${formatDate()}_better_tenant.sql`;
131
+ const path = join(opts.output, filename);
132
+ const existed = existsSync(path);
133
+ writeFileSync(path, sql, "utf-8");
134
+ console.log(`Wrote ${path}`);
135
+ sendCliTelemetry(
136
+ "cli_migrate",
137
+ existed ? "overwritten" : "migrated",
138
+ config
139
+ );
140
+ return;
141
+ }
142
+ process.stdout.write(sql);
143
+ sendCliTelemetry("cli_migrate", "no_changes", config);
144
+ } catch (err) {
145
+ try {
146
+ const config = loadConfig(process.cwd());
147
+ const sent = sendCliTelemetry("cli_migrate", "aborted", config, {
148
+ wait: true
149
+ });
150
+ await (sent ?? Promise.resolve());
151
+ } catch {
152
+ }
153
+ console.error(err instanceof Error ? err.message : String(err));
154
+ process.exit(1);
155
+ }
156
+ });
157
+ program.command("add-table <tableName>").description(
158
+ "Generate SQL to add tenant_id, RLS, policy, and trigger for one table"
159
+ ).option("--dry-run", "Print SQL to stdout").option("-o, --output <dir>", "Write migration file to directory").action(
160
+ async (tableName, opts) => {
161
+ try {
162
+ if (!tableName || typeof tableName !== "string") {
163
+ console.error("add-table requires a table name");
164
+ process.exit(1);
165
+ }
166
+ const config = loadConfig(process.cwd());
167
+ const sql = generateAddTableSql(tableName);
168
+ if (opts.dryRun) {
169
+ process.stdout.write(sql);
170
+ sendCliTelemetry("cli_migrate", "no_changes", config);
171
+ return;
172
+ }
173
+ if (opts.output) {
174
+ mkdirSync(opts.output, { recursive: true });
175
+ const safeName = tableName.replace(/[^a-zA-Z0-9_]/g, "_");
176
+ const filename = `${formatDate()}_add_table_${safeName}.sql`;
177
+ const path = join(opts.output, filename);
178
+ const existed = existsSync(path);
179
+ writeFileSync(path, sql, "utf-8");
180
+ console.log(`Wrote ${path}`);
181
+ sendCliTelemetry(
182
+ "cli_migrate",
183
+ existed ? "overwritten" : "migrated",
184
+ config
185
+ );
186
+ return;
187
+ }
188
+ process.stdout.write(sql);
189
+ sendCliTelemetry("cli_migrate", "no_changes", config);
190
+ } catch (err) {
191
+ try {
192
+ const config = loadConfig(process.cwd());
193
+ const sent = sendCliTelemetry("cli_migrate", "aborted", config, {
194
+ wait: true
195
+ });
196
+ await (sent ?? Promise.resolve());
197
+ } catch {
198
+ }
199
+ console.error(err instanceof Error ? err.message : String(err));
200
+ process.exit(1);
201
+ }
202
+ }
203
+ );
204
+ program.command("generate").description("Generate Drizzle schema snippet (tenant_id + relations)").option("--dry-run", "Print schema snippet to stdout").option("-o, --output <path>", "Write to file (e.g. schema/better-tenant.ts)").action(async (opts) => {
205
+ try {
206
+ const cwd = process.cwd();
207
+ const config = loadConfig(cwd);
208
+ const schema = generateDrizzleSchema(config);
209
+ if (opts.dryRun) {
210
+ process.stdout.write(schema);
211
+ sendCliTelemetry("cli_generate", "no_changes", config);
212
+ return;
213
+ }
214
+ if (opts.output) {
215
+ const dir = dirname(opts.output);
216
+ mkdirSync(dir, { recursive: true });
217
+ const existed = existsSync(opts.output);
218
+ writeFileSync(opts.output, schema, "utf-8");
219
+ console.log(`Wrote ${opts.output}`);
220
+ sendCliTelemetry(
221
+ "cli_generate",
222
+ existed ? "overwritten" : "generated",
223
+ config
224
+ );
225
+ return;
226
+ }
227
+ process.stdout.write(schema);
228
+ sendCliTelemetry("cli_generate", "no_changes", config);
229
+ } catch (err) {
230
+ try {
231
+ const config = loadConfig(process.cwd());
232
+ const sent = sendCliTelemetry("cli_generate", "aborted", config, {
233
+ wait: true
234
+ });
235
+ await (sent ?? Promise.resolve());
236
+ } catch {
237
+ }
238
+ console.error(err instanceof Error ? err.message : String(err));
239
+ process.exit(1);
240
+ }
241
+ });
242
+ program.command("check").description("Verify tenants table, RLS, policies, and triggers in database").option("--database-url <url>", "Database URL (default: DATABASE_URL env)").action(async (opts) => {
243
+ const url = opts.databaseUrl ?? process.env.DATABASE_URL;
244
+ if (!url || typeof url !== "string") {
245
+ console.error(
246
+ "check requires --database-url or DATABASE_URL environment variable"
247
+ );
248
+ process.exit(1);
249
+ }
250
+ const cwd = process.cwd();
251
+ try {
252
+ const config = loadConfig(cwd);
253
+ const { passed, results } = await runCheck(url, config);
254
+ for (const r of results) {
255
+ const icon = r.passed ? "\u2713" : "\u2717";
256
+ const msg = r.message ? ` ${r.message}` : "";
257
+ console.log(`${icon} ${r.check}${msg}`);
258
+ }
259
+ sendCliTelemetry("cli_check", passed ? "passed" : "failed", config);
260
+ if (!passed) {
261
+ process.exit(1);
262
+ }
263
+ } catch (err) {
264
+ const message = err instanceof Error ? err.message : String(err);
265
+ if (message.startsWith(NO_CONFIG_MESSAGE_PREFIX) && url) {
266
+ const suggestedTables = await getSuggestedTables(url);
267
+ const enrichedMessage = buildNoConfigMessage(cwd, suggestedTables);
268
+ console.error(enrichedMessage);
269
+ process.exit(1);
270
+ }
271
+ try {
272
+ const config = loadConfig(process.cwd());
273
+ const sent = sendCliTelemetry("cli_check", "aborted", config, {
274
+ wait: true
275
+ });
276
+ await (sent ?? Promise.resolve());
277
+ } catch {
278
+ }
279
+ console.error(message);
280
+ process.exit(1);
281
+ }
282
+ });
283
+ program.command("seed").description("Insert one tenant (uses bypass_rls; no superuser)").requiredOption("--name <name>", "Tenant display name").option("--slug <slug>", "URL-safe slug (default: derived from name)").option("--database-url <url>", "Database URL (default: DATABASE_URL env)").action(
284
+ async (opts) => {
285
+ try {
286
+ const url = opts.databaseUrl ?? process.env.DATABASE_URL;
287
+ if (!url || typeof url !== "string") {
288
+ console.error(
289
+ "seed requires --database-url or DATABASE_URL environment variable"
290
+ );
291
+ process.exit(1);
292
+ }
293
+ const config = loadConfig(process.cwd());
294
+ const result = await runSeed(url, {
295
+ name: opts.name,
296
+ slug: opts.slug
297
+ });
298
+ sendCliTelemetry("cli_seed", "created", config);
299
+ console.log(`Created tenant: ${result.name} (${result.slug})`);
300
+ console.log(result.id);
301
+ } catch (err) {
302
+ try {
303
+ const config = loadConfig(process.cwd());
304
+ const sent = sendCliTelemetry("cli_seed", "aborted", config, {
305
+ wait: true
306
+ });
307
+ await (sent ?? Promise.resolve());
308
+ } catch {
309
+ }
310
+ console.error(err instanceof Error ? err.message : String(err));
311
+ process.exit(1);
312
+ }
313
+ }
314
+ );
315
+ var isMain = typeof process !== "undefined" && process.argv[1] !== void 0 && (process.argv[1].endsWith("cli.js") || process.argv[1].endsWith("cli.ts"));
316
+ if (isMain) {
317
+ program.parse();
318
+ }
319
+ export {
320
+ program
321
+ };
322
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts","../src/suggested-tables.ts","../src/generate.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { Command } from \"commander\";\nimport { sendCliTelemetry } from \"@usebetterdev/tenant-core\";\nimport {\n loadConfig,\n buildNoConfigMessage,\n NO_CONFIG_MESSAGE_PREFIX,\n} from \"./config.js\";\nimport { getSuggestedTables } from \"./suggested-tables.js\";\nimport { runCheck } from \"./check.js\";\nimport { runSeed } from \"./seed.js\";\nimport { generateDrizzleSchema } from \"./generate.js\";\nimport { generateAddTableSql, generateMigrationSql } from \"./migrate.js\";\n\nfunction formatDate(): string {\n const d = new Date();\n const y = d.getFullYear();\n const m = String(d.getMonth() + 1).padStart(2, \"0\");\n const day = String(d.getDate()).padStart(2, \"0\");\n return `${y}${m}${day}`;\n}\n\nconst program = new Command();\n\nprogram\n .name(\"better-tenant\")\n .description(\"Multi-tenancy for Postgres\")\n .version(\"0.1.0\");\n\nprogram\n .command(\"migrate\")\n .description(\"Generate SQL migration for tenants table and RLS\")\n .option(\"--dry-run\", \"Print SQL to stdout\")\n .option(\"-o, --output <dir>\", \"Write migration file to directory\")\n .action(async (opts: { dryRun?: boolean; output?: string }) => {\n try {\n const cwd = process.cwd();\n const config = loadConfig(cwd);\n const sql = generateMigrationSql(config);\n\n if (opts.dryRun) {\n process.stdout.write(sql);\n sendCliTelemetry(\"cli_migrate\", \"no_changes\", config);\n return;\n }\n\n if (opts.output) {\n mkdirSync(opts.output, { recursive: true });\n const filename = `${formatDate()}_better_tenant.sql`;\n const path = join(opts.output, filename);\n const existed = existsSync(path);\n writeFileSync(path, sql, \"utf-8\");\n console.log(`Wrote ${path}`);\n sendCliTelemetry(\n \"cli_migrate\",\n existed ? \"overwritten\" : \"migrated\",\n config,\n );\n return;\n }\n\n process.stdout.write(sql);\n sendCliTelemetry(\"cli_migrate\", \"no_changes\", config);\n } catch (err) {\n try {\n const config = loadConfig(process.cwd());\n const sent = sendCliTelemetry(\"cli_migrate\", \"aborted\", config, {\n wait: true,\n });\n await (sent ?? Promise.resolve());\n } catch {\n /* config load failed, skip telemetry */\n }\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n }\n });\n\nprogram\n .command(\"add-table <tableName>\")\n .description(\n \"Generate SQL to add tenant_id, RLS, policy, and trigger for one table\",\n )\n .option(\"--dry-run\", \"Print SQL to stdout\")\n .option(\"-o, --output <dir>\", \"Write migration file to directory\")\n .action(\n async (tableName: string, opts: { dryRun?: boolean; output?: string }) => {\n try {\n if (!tableName || typeof tableName !== \"string\") {\n console.error(\"add-table requires a table name\");\n process.exit(1);\n }\n const config = loadConfig(process.cwd());\n const sql = generateAddTableSql(tableName);\n\n if (opts.dryRun) {\n process.stdout.write(sql);\n sendCliTelemetry(\"cli_migrate\", \"no_changes\", config);\n return;\n }\n\n if (opts.output) {\n mkdirSync(opts.output, { recursive: true });\n const safeName = tableName.replace(/[^a-zA-Z0-9_]/g, \"_\");\n const filename = `${formatDate()}_add_table_${safeName}.sql`;\n const path = join(opts.output, filename);\n const existed = existsSync(path);\n writeFileSync(path, sql, \"utf-8\");\n console.log(`Wrote ${path}`);\n sendCliTelemetry(\n \"cli_migrate\",\n existed ? \"overwritten\" : \"migrated\",\n config,\n );\n return;\n }\n\n process.stdout.write(sql);\n sendCliTelemetry(\"cli_migrate\", \"no_changes\", config);\n } catch (err) {\n try {\n const config = loadConfig(process.cwd());\n const sent = sendCliTelemetry(\"cli_migrate\", \"aborted\", config, {\n wait: true,\n });\n await (sent ?? Promise.resolve());\n } catch {\n /* skip telemetry */\n }\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n }\n },\n );\n\nprogram\n .command(\"generate\")\n .description(\"Generate Drizzle schema snippet (tenant_id + relations)\")\n .option(\"--dry-run\", \"Print schema snippet to stdout\")\n .option(\"-o, --output <path>\", \"Write to file (e.g. schema/better-tenant.ts)\")\n .action(async (opts: { dryRun?: boolean; output?: string }) => {\n try {\n const cwd = process.cwd();\n const config = loadConfig(cwd);\n const schema = generateDrizzleSchema(config);\n\n if (opts.dryRun) {\n process.stdout.write(schema);\n sendCliTelemetry(\"cli_generate\", \"no_changes\", config);\n return;\n }\n\n if (opts.output) {\n const dir = dirname(opts.output);\n mkdirSync(dir, { recursive: true });\n const existed = existsSync(opts.output);\n writeFileSync(opts.output, schema, \"utf-8\");\n console.log(`Wrote ${opts.output}`);\n sendCliTelemetry(\n \"cli_generate\",\n existed ? \"overwritten\" : \"generated\",\n config,\n );\n return;\n }\n\n process.stdout.write(schema);\n sendCliTelemetry(\"cli_generate\", \"no_changes\", config);\n } catch (err) {\n try {\n const config = loadConfig(process.cwd());\n const sent = sendCliTelemetry(\"cli_generate\", \"aborted\", config, {\n wait: true,\n });\n await (sent ?? Promise.resolve());\n } catch {\n /* skip telemetry */\n }\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n }\n });\n\nprogram\n .command(\"check\")\n .description(\"Verify tenants table, RLS, policies, and triggers in database\")\n .option(\"--database-url <url>\", \"Database URL (default: DATABASE_URL env)\")\n .action(async (opts: { databaseUrl?: string }) => {\n const url = opts.databaseUrl ?? process.env.DATABASE_URL;\n if (!url || typeof url !== \"string\") {\n console.error(\n \"check requires --database-url or DATABASE_URL environment variable\",\n );\n process.exit(1);\n }\n const cwd = process.cwd();\n try {\n const config = loadConfig(cwd);\n const { passed, results } = await runCheck(url, config);\n\n for (const r of results) {\n const icon = r.passed ? \"✓\" : \"✗\";\n const msg = r.message ? ` ${r.message}` : \"\";\n console.log(`${icon} ${r.check}${msg}`);\n }\n sendCliTelemetry(\"cli_check\", passed ? \"passed\" : \"failed\", config);\n if (!passed) {\n process.exit(1);\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n if (\n message.startsWith(NO_CONFIG_MESSAGE_PREFIX) &&\n url\n ) {\n const suggestedTables = await getSuggestedTables(url);\n const enrichedMessage = buildNoConfigMessage(cwd, suggestedTables);\n console.error(enrichedMessage);\n process.exit(1);\n }\n try {\n const config = loadConfig(process.cwd());\n const sent = sendCliTelemetry(\"cli_check\", \"aborted\", config, {\n wait: true,\n });\n await (sent ?? Promise.resolve());\n } catch {\n /* skip telemetry */\n }\n console.error(message);\n process.exit(1);\n }\n });\n\nprogram\n .command(\"seed\")\n .description(\"Insert one tenant (uses bypass_rls; no superuser)\")\n .requiredOption(\"--name <name>\", \"Tenant display name\")\n .option(\"--slug <slug>\", \"URL-safe slug (default: derived from name)\")\n .option(\"--database-url <url>\", \"Database URL (default: DATABASE_URL env)\")\n .action(\n async (opts: { name: string; slug?: string; databaseUrl?: string }) => {\n try {\n const url = opts.databaseUrl ?? process.env.DATABASE_URL;\n if (!url || typeof url !== \"string\") {\n console.error(\n \"seed requires --database-url or DATABASE_URL environment variable\",\n );\n process.exit(1);\n }\n const config = loadConfig(process.cwd());\n const result = await runSeed(url, {\n name: opts.name,\n slug: opts.slug,\n });\n sendCliTelemetry(\"cli_seed\", \"created\", config);\n console.log(`Created tenant: ${result.name} (${result.slug})`);\n console.log(result.id);\n } catch (err) {\n try {\n const config = loadConfig(process.cwd());\n const sent = sendCliTelemetry(\"cli_seed\", \"aborted\", config, {\n wait: true,\n });\n await (sent ?? Promise.resolve());\n } catch {\n /* skip telemetry */\n }\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n }\n },\n );\n\nconst isMain =\n typeof process !== \"undefined\" &&\n process.argv[1] !== undefined &&\n (process.argv[1].endsWith(\"cli.js\") || process.argv[1].endsWith(\"cli.ts\"));\nif (isMain) {\n program.parse();\n}\n\nexport { program };\n","import { Pool } from \"pg\";\n\n/** Preferred table names (first match wins); used to order suggestions from the DB. */\nexport const TABLE_PRIORITY = [\n \"users\",\n \"projects\",\n \"tasks\",\n \"organizations\",\n \"members\",\n \"workspaces\",\n \"teams\",\n \"issues\",\n \"posts\",\n \"products\",\n \"orders\",\n \"accounts\",\n \"invitations\",\n];\n\nexport const SUGGESTED_LIMIT = 5;\n\n/**\n * Sort table names: priority list first (by TABLE_PRIORITY order), then the rest alphabetically.\n */\nexport function sortTablesByPriority(tables: string[]): string[] {\n const order = new Map(TABLE_PRIORITY.map((t, i) => [t, i]));\n return [...tables].sort((a, b) => {\n const ia = order.get(a) ?? TABLE_PRIORITY.length;\n const ib = order.get(b) ?? TABLE_PRIORITY.length;\n if (ia !== ib) return ia - ib;\n return a.localeCompare(b);\n });\n}\n\n/**\n * Connect to the database and return up to SUGGESTED_LIMIT table names from public schema,\n * prioritized by common names (users, projects, tasks, etc.). Excludes \"tenants\".\n * Returns [] on any connection/query error.\n */\nexport async function getSuggestedTables(\n databaseUrl: string,\n): Promise<string[]> {\n const pool = new Pool({ connectionString: databaseUrl });\n try {\n const r = await pool.query<{ table_name: string }>(\n `SELECT table_name FROM information_schema.tables\n WHERE table_schema = 'public' AND table_type = 'BASE TABLE'\n AND table_name != 'tenants'\n ORDER BY table_name`,\n );\n const names = (r.rows ?? []).map((row) => row.table_name);\n const sorted = sortTablesByPriority(names);\n return sorted.slice(0, SUGGESTED_LIMIT);\n } catch {\n return [];\n } finally {\n await pool.end();\n }\n}\n","import type { BetterTenantCliConfig } from \"./config.js\";\n\n/**\n * Convert table name (e.g. \"projects\") to a typical Drizzle table variable name (e.g. \"projectsTable\").\n */\nfunction toTableVar(tableName: string): string {\n const base = tableName.replace(/[^a-zA-Z0-9]/g, \"_\");\n return `${base}Table`;\n}\n\n/**\n * Generate a Drizzle schema snippet for tenant-scoped tables.\n * Outputs tenant_id column definition and relations for each table.\n * User merges this into their schema file.\n */\nexport function generateDrizzleSchema(config: BetterTenantCliConfig): string {\n const lines: string[] = [\n \"// Better Tenant: Drizzle schema snippet\",\n \"// Add tenantId column and relations to each tenant-scoped table.\",\n \"// The .default(sql`...`) tells Drizzle that tenant_id is auto-populated\",\n \"// by the set_tenant_id trigger, so you can omit it from .insert().values().\",\n \"// Merge into your schema. Import tenantsTable from your tenants definition.\",\n \"\",\n \"import { sql, relations } from 'drizzle-orm';\",\n \"import { uuid } from 'drizzle-orm/pg-core';\",\n \"import { tenantsTable } from './tenants'; // adjust import path\",\n \"\",\n ];\n\n for (const tableName of config.tenantTables) {\n const tableVar = toTableVar(tableName);\n const relationsName = `${tableName.replace(/[^a-zA-Z0-9]/g, \"_\")}Relations`;\n lines.push(`// --- Table: ${tableName} ---`);\n lines.push(`// Add to ${tableVar} pgTable definition:`);\n lines.push(\n \"// tenantId: uuid(\\\"tenant_id\\\").notNull().default(sql`(current_setting('app.current_tenant', true))::uuid`),\",\n );\n lines.push(\"\");\n lines.push(`// Relations for ${tableName}:`);\n lines.push(\n `export const ${relationsName} = relations(${tableVar}, ({ one }) => ({`,\n );\n lines.push(` tenant: one(tenantsTable),`);\n lines.push(`}));`);\n lines.push(\"\");\n }\n\n return lines.join(\"\\n\").trimEnd();\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * CLI config shape. Matches core's BetterTenantConfig subset needed for migrate/generate/check.
3
+ */
4
+ interface BetterTenantCliConfig {
5
+ tenantTables: string[];
6
+ schemaDir?: string;
7
+ migrationsDir?: string;
8
+ }
9
+ /** Prefix of the "no config" error message. Use to detect and enrich the error in commands that have a DB URL. */
10
+ declare const NO_CONFIG_MESSAGE_PREFIX = "better-tenant: No config found.";
11
+ /**
12
+ * Build the "no config" error message. When suggestedTables is provided (e.g. from getSuggestedTables),
13
+ * the snippet uses those table names instead of the default.
14
+ */
15
+ declare function buildNoConfigMessage(cwd: string, suggestedTables?: string[]): string;
16
+ /**
17
+ * Discover and load config from cwd.
18
+ * 1. Look for better-tenant.config.json in cwd
19
+ * 2. Fall back to package.json "betterTenant" key
20
+ */
21
+ declare function loadConfig(cwd?: string): BetterTenantCliConfig;
22
+
23
+ export { type BetterTenantCliConfig, NO_CONFIG_MESSAGE_PREFIX, buildNoConfigMessage, loadConfig };
package/dist/config.js ADDED
@@ -0,0 +1,11 @@
1
+ import {
2
+ NO_CONFIG_MESSAGE_PREFIX,
3
+ buildNoConfigMessage,
4
+ loadConfig
5
+ } from "./chunk-64RQTBD5.js";
6
+ export {
7
+ NO_CONFIG_MESSAGE_PREFIX,
8
+ buildNoConfigMessage,
9
+ loadConfig
10
+ };
11
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,11 @@
1
+ import { BetterTenantCliConfig } from './config.js';
2
+
3
+ declare function generateMigrationSql(config: BetterTenantCliConfig): string;
4
+ /**
5
+ * Generate SQL to add tenant_id, RLS, policy, and trigger for a single table.
6
+ * Idempotent: uses IF NOT EXISTS for column, DROP IF EXISTS for policy/trigger.
7
+ * Assumes tenants table and set_tenant_id() function exist (run migrate first).
8
+ */
9
+ declare function generateAddTableSql(tableName: string): string;
10
+
11
+ export { generateAddTableSql, generateMigrationSql };
@@ -0,0 +1,9 @@
1
+ import {
2
+ generateAddTableSql,
3
+ generateMigrationSql
4
+ } from "./chunk-VZCKMMJZ.js";
5
+ export {
6
+ generateAddTableSql,
7
+ generateMigrationSql
8
+ };
9
+ //# sourceMappingURL=migrate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/dist/seed.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ interface SeedOptions {
2
+ name: string;
3
+ slug?: string;
4
+ }
5
+ /**
6
+ * Derive a URL-safe slug from name (e.g. "Test Org" -> "test-org").
7
+ */
8
+ declare function slugFromName(name: string): string;
9
+ interface SeedResult {
10
+ id: string;
11
+ name: string;
12
+ slug: string;
13
+ }
14
+ /**
15
+ * Insert one tenant using bypass_rls so RLS allows the insert (no superuser).
16
+ * Uses SET LOCAL app.bypass_rls = 'true' in a transaction.
17
+ */
18
+ declare function runSeed(databaseUrl: string, options: SeedOptions): Promise<SeedResult>;
19
+
20
+ export { type SeedOptions, type SeedResult, runSeed, slugFromName };
package/dist/seed.js ADDED
@@ -0,0 +1,9 @@
1
+ import {
2
+ runSeed,
3
+ slugFromName
4
+ } from "./chunk-LXS6CXJ3.js";
5
+ export {
6
+ runSeed,
7
+ slugFromName
8
+ };
9
+ //# sourceMappingURL=seed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,10 @@
1
+ -- Better Tenant: trigger function for auto-populating tenant_id
2
+ CREATE OR REPLACE FUNCTION set_tenant_id()
3
+ RETURNS TRIGGER AS $$
4
+ BEGIN
5
+ IF NEW.tenant_id IS NULL AND current_setting('app.current_tenant', true) IS NOT NULL THEN
6
+ NEW.tenant_id := current_setting('app.current_tenant', true)::uuid;
7
+ END IF;
8
+ RETURN NEW;
9
+ END;
10
+ $$ LANGUAGE plpgsql;
@@ -0,0 +1,21 @@
1
+ -- Better Tenant: RLS for {{TABLE}}
2
+ ALTER TABLE {{TABLE_QUOTED}} ADD COLUMN IF NOT EXISTS tenant_id UUID NOT NULL REFERENCES tenants(id);
3
+ ALTER TABLE {{TABLE_QUOTED}} ENABLE ROW LEVEL SECURITY;
4
+ ALTER TABLE {{TABLE_QUOTED}} FORCE ROW LEVEL SECURITY;
5
+ DROP POLICY IF EXISTS "{{POLICY_NAME}}" ON {{TABLE_QUOTED}};
6
+ CREATE POLICY "{{POLICY_NAME}}" ON {{TABLE_QUOTED}}
7
+ FOR ALL
8
+ USING (
9
+ (tenant_id)::text = current_setting('app.current_tenant', true)
10
+ OR current_setting('app.bypass_rls', true) = 'true'
11
+ )
12
+ WITH CHECK (
13
+ (tenant_id)::text = current_setting('app.current_tenant', true)
14
+ OR current_setting('app.bypass_rls', true) = 'true'
15
+ );
16
+
17
+ DROP TRIGGER IF EXISTS set_tenant_id_trigger ON {{TABLE_QUOTED}};
18
+ CREATE TRIGGER set_tenant_id_trigger
19
+ BEFORE INSERT ON {{TABLE_QUOTED}}
20
+ FOR EACH ROW
21
+ EXECUTE PROCEDURE set_tenant_id();
@@ -0,0 +1,7 @@
1
+ -- Better Tenant: tenants table
2
+ CREATE TABLE IF NOT EXISTS tenants (
3
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
4
+ name TEXT NOT NULL,
5
+ slug TEXT NOT NULL UNIQUE,
6
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
7
+ );
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@usebetterdev/tenant-cli",
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/cli.js",
13
+ "exports": {
14
+ ".": "./dist/cli.js",
15
+ "./migrate": {
16
+ "import": "./dist/migrate.js",
17
+ "types": "./dist/migrate.d.ts"
18
+ },
19
+ "./check": {
20
+ "import": "./dist/check.js",
21
+ "types": "./dist/check.d.ts"
22
+ },
23
+ "./seed": {
24
+ "import": "./dist/seed.js",
25
+ "types": "./dist/seed.d.ts"
26
+ },
27
+ "./config": {
28
+ "import": "./dist/config.js",
29
+ "types": "./dist/config.d.ts"
30
+ }
31
+ },
32
+ "bin": {
33
+ "better-tenant": "./dist/cli.js"
34
+ },
35
+ "files": [
36
+ "dist"
37
+ ],
38
+ "dependencies": {
39
+ "commander": "^12.1.0",
40
+ "pg": "^8.13.0",
41
+ "@usebetterdev/tenant-core": "0.1.0"
42
+ },
43
+ "devDependencies": {
44
+ "@testcontainers/postgresql": "^11.11.0",
45
+ "@types/node": "^22.10.0",
46
+ "@types/pg": "^8.11.0",
47
+ "pg": "^8.13.0",
48
+ "tsup": "^8.3.5",
49
+ "typescript": "~5.7.2",
50
+ "vitest": "^2.1.6"
51
+ },
52
+ "engines": {
53
+ "node": ">=22"
54
+ },
55
+ "scripts": {
56
+ "build": "tsup && mkdir -p dist/sql && cp src/sql/*.sql dist/sql/",
57
+ "lint": "oxlint",
58
+ "test": "vitest run",
59
+ "test:integration": "vitest run -c vitest.integration.config.ts",
60
+ "typecheck": "tsc --noEmit"
61
+ }
62
+ }