@tenora/multi-tenant 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,176 @@
1
+ # Tenora
2
+
3
+ A framework-agnostic multi-tenant toolkit for Node.js (Knex + Objection). Tenora handles per-tenant database provisioning, secure credential handling, cached connections, and ready-made CLI commands for migrating and rolling back both base and tenant databases.
4
+
5
+ ## Why Tenora?
6
+ - Works with any HTTP framework (Fastify, Express, Koa, Nest adapters, custom servers).
7
+ - Keeps tenants isolated at the database level (one DB per tenant, optional per-tenant DB user).
8
+ - Zero lock-in: you choose how to resolve tenant IDs and enforce authorization.
9
+ - Batteries included: password generation/encryption helpers and a CLI (`tenora`) for base/tenant migrations.
10
+
11
+ ## Installation
12
+ ```bash
13
+ npm install @tenora/multi-tenant
14
+ # peers: knex, objection, pg (install if not already in your project)
15
+ ```
16
+
17
+ ## Core concepts
18
+ - **Base database**: shared metadata (tenant registry). Tenora connects via a base Knex config and stores tenants in a registry table.
19
+ - **Tenant database**: one Postgres database per tenant. Tenora can create it, create a dedicated DB user, and run tenant migrations/seeds.
20
+ - **Tenant resolver**: your middleware hook that picks the tenant ID per request and attaches a tenant-bound Knex instance.
21
+ - **Cache**: Tenora caches Knex instances per tenant to avoid pool churn; you can destroy them explicitly when needed.
22
+
23
+ ## Quick start (programmatic)
24
+ ```ts
25
+ import { createTenoraFactory, createTenantResolver, generateTenantPassword } from "@tenora/multi-tenant";
26
+
27
+ // 1) Create the factory at startup
28
+ // Option A: rely on tenora.config.js (default) or TENORA_CONFIG
29
+ const manager = createTenoraFactory();
30
+
31
+ // Option B: pass options inline
32
+ // const manager = createTenoraFactory({
33
+ // base: { host, port: 5432, user, password, database: "base" },
34
+ // tenant: { migrationsDir: "migrations/tenants", seedsDir: "seeds/tenants" }, // seeds optional
35
+ // });
36
+
37
+ // 2) Provision a tenant (one-off when signing up)
38
+ const pwd = generateTenantPassword();
39
+ await manager.createTenantDb("tenantA", pwd); // creates DB, user_userA, runs tenant migrations
40
+
41
+ // 3) Per-request hookup (framework-agnostic)
42
+ const resolveTenant = createTenantResolver({
43
+ manager,
44
+ tenantId: (req) => req.params.tenantId ?? req.headers["x-tenant-id"],
45
+ passwordProvider: (tenantId) => lookupPlainOrDecrypt(tenantId), // optional
46
+ authorizer: (tenantId, req) => ensureAccess(req.userId, tenantId), // optional
47
+ // attach is optional; default sets req.tenantId and req.knex
48
+ });
49
+
50
+ await resolveTenant(req);
51
+ // Now use Objection with the tenant-bound Knex:
52
+ await SomeModel.query(req.knex).where(...);
53
+ ```
54
+
55
+ ## Built-in CLI (`tenora`)
56
+ Tenora ships with a CLI for migrations and rollbacks.
57
+
58
+ Commands:
59
+ - `tenora migrate` (alias `migrate:base`) / `tenora rollback` (alias `rollback:base`)
60
+ - `tenora migrate:tenants` / `tenora rollback:tenants`
61
+ - `tenora make:migration <name>` (alias `make:migration:base`) / `tenora make:migration:tenants <name>`
62
+ - `tenora make:seed <name>` (alias `make:seed:base`) / `tenora make:seed:tenants <name>`
63
+ - `tenora seed:run` (alias `seed:run:base`) / `tenora seed:run:tenants`
64
+ - `tenora list` (help)
65
+
66
+ Options:
67
+ - `--create-base`: create the base database (from `base.database`) if it does not exist.
68
+
69
+ Notes:
70
+ - `make:migration:*` requires the corresponding `migrationsDir`.
71
+ - `make:seed:*` and `seed:run*` require the corresponding `seedsDir`.
72
+ - Template output is auto-selected based on the nearest `package.json` (`"type": "module"` → ESM, otherwise CJS).
73
+ - Use `--esm` or `--cjs` to override template output for `make:migration:*` and `make:seed:*`.
74
+ - Migration templates infer common patterns:
75
+ - `create_users` / `create_users_table` → `createTable("users")`
76
+ - `add_email_to_users` → `alterTable("users").addColumn("email")`
77
+ - `remove_email_from_users` / `drop_email_from_users` → `alterTable("users").dropColumn("email")`
78
+
79
+ ### Multiple DBMS
80
+ Set `base.client` to the Knex client you want (e.g., `"pg"`, `"mysql2"`, `"mariadb"`, `"sqlite3"`, `"mssql"`). Tenora uses the
81
+ same client for tenant connections. Use `base.connection` when the driver needs non-standard fields (e.g., `server` for SQL Server
82
+ or `filename` for SQLite).
83
+
84
+ `createTenantDb` and `--create-base` support **Postgres**, **MySQL/MariaDB**, **SQLite**, and **SQL Server**. For other drivers,
85
+ provision the base and tenant databases externally and Tenora will connect to them.
86
+
87
+ ### CLI config (tenora.config.js by default)
88
+ ```js
89
+ // tenora.config.js
90
+ import { defineTenoraConfig, decryptPassword, encryptPassword } from "@tenora/multi-tenant";
91
+
92
+ export default defineTenoraConfig({
93
+ base: {
94
+ client: "pg", // or "mysql2"
95
+ host,
96
+ port: 5432,
97
+ user,
98
+ password,
99
+ database: "base",
100
+ // adminDatabase: "postgres", // optional override for create-base/create-tenant
101
+ // connection: { /* full Knex connection config override (useful for sqlite/mssql) */ },
102
+ migrationsDir: "migrations/base",
103
+ seedsDir: "seeds/base", // optional
104
+ },
105
+ tenant: { migrationsDir: "migrations/tenants", seedsDir: "seeds/tenants" },
106
+ // Optional: customize where tenant records live (default: tenora_tenants)
107
+ registry: { table: "tenora_tenants" },
108
+ encryptPassword: (plain) => encryptPassword(plain, process.env.CIPHER_KEY),
109
+ decryptPassword: (enc) => decryptPassword(enc, process.env.CIPHER_KEY),
110
+ });
111
+ ```
112
+ Run with a custom file: `tenora migrate:tenants --config path/to/file.js`.
113
+ Default lookup order: `tenora.config.js`, `tenora.config.mjs`, `tenora.config.ts` (unless `TENORA_CONFIG` is set).
114
+
115
+ Tip: use `defineTenoraConfig(...)` in your config file to get IDE hints for all options.
116
+ If your config is `.mjs` or `.ts` and you want to load it implicitly in code, use `createTenoraFactoryAsync()`
117
+ or import the config and pass it directly to `createTenoraFactory(...)`.
118
+
119
+ Encryption defaults:
120
+ - If `encryptPassword`/`decryptPassword` are not provided, Tenora will use `process.env.TENORA_KEY` (if set).
121
+ - If no key is present, Tenora stores plaintext passwords in the registry.
122
+
123
+ SQLite notes:
124
+ - For SQLite, set `base.database` to a file path or provide `base.connection.filename`.
125
+ - Tenant DB files default to `<cwd>/<tenantId>.sqlite`; customize with `tenant.databaseDir`, `tenant.databaseSuffix`, or `tenant.databaseName`.
126
+
127
+ ### Tenant registry (auto-migration)
128
+ Tenora stores tenants in a **registry table** in your base DB. The CLI will **auto-generate** a base migration
129
+ the first time you run `tenora migrate` (or `migrate:base`). This gives you a file you can **rename or edit**
130
+ before applying it.
131
+ Make sure `base.migrationsDir` is set so Tenora knows where to write the migration.
132
+
133
+ Defaults (customizable via `registry`):
134
+ - table: `tenora_tenants`
135
+ - columns: `id`, `password`, `encrypted_password`, `created_at`, `updated_at`
136
+
137
+ If you rename the table or columns in the generated migration, update `registry` in your config to match.
138
+ If `encryptPassword` is provided, Tenora stores the encrypted value in `encrypted_password`; otherwise it stores the plain password in `password`.
139
+
140
+ ## API surface
141
+ - `createTenoraFactory(options)` (alias `createKnexFactory`) → `{ getBase, getTenant, createTenantDb, destroyTenant, destroyAll }`
142
+ - `createTenoraFactoryAsync(options)` → same, but can load `.mjs`/`.ts` config files via default lookup
143
+ - `options.base`: base connection (any Knex client) + optional migrations/seeds dirs
144
+ - `options.tenant`: migrationsDir, seedsDir, userPrefix (defaults to `user_`), pool/ssl overrides, SQLite db path options
145
+ - `createTenantResolver({ manager, tenantId, passwordProvider?, authorizer?, attach? })`
146
+ - Returns async `(req) => { tenantId?, knex? }`
147
+ - Default attaches `req.tenantId` and `req.knex`; customize via `attach`
148
+ - Password helpers: `generateTenantPassword()`, `encryptPassword(password, key)`, `decryptPassword(ciphertext, key)`
149
+
150
+ ## Typical lifecycle
151
+ 1) **Bootstrap**: create factory once at app start.
152
+ 2) **Provision**: `createTenantDb(tenantId, password?)` when a new tenant signs up (also writes to registry table).
153
+ 3) **Store creds**: save encrypted tenant DB password in your base DB.
154
+ 4) **Request flow**: middleware runs `createTenantResolver` → attaches `req.knex` for Objection queries.
155
+ 5) **Migrate**: use CLI to keep base and tenant schemas in sync.
156
+ 6) **Shutdown/cleanup**: call `destroyTenant(id)` or `destroyAll()` to close pools.
157
+
158
+ ## Security notes
159
+ - Use per-tenant DB users with strong passwords (generate + encrypt).
160
+ - Keep the AES key (`CIPHER_KEY`) outside source control.
161
+ - Authorize tenant access in the resolver (`authorizer` hook) to prevent cross-tenant leakage.
162
+ - Rotate tenant passwords by recreating the DB user and updating stored (encrypted) password.
163
+
164
+ ## Troubleshooting
165
+ - **“database already exists”**: your registry may have stale tenants; drop or pick a new ID.
166
+ - **“password authentication failed”**: ensure `passwordProvider` returns the plain password for that tenant.
167
+ - **Migrations not running**: verify `tenant.migrationsDir` is correct and reachable from where you invoke the CLI.
168
+ - **Pooling issues**: adjust `tenant.pool` or `base.pool` in the factory options.
169
+
170
+ ## Minimum example config snippet
171
+ ```ts
172
+ const factory = createTenoraFactory(); // uses tenora.config.js or TENORA_CONFIG path
173
+ // or pass inline options as above if you prefer
174
+ ```
175
+
176
+ Tenora stays independent of any specific app domain—use it in any Node.js service that needs clean, per-tenant Postgres isolation with Knex + Objection.
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node