@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 +176 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1013 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +118 -0
- package/dist/index.js +468 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
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
|