@stratum-hq/create 0.3.1 → 0.4.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/CHANGELOG.md +28 -0
- package/README.md +2 -1
- package/dist/bin.js +3 -0
- package/dist/index.js +712 -156
- package/dist/matrix.js +16 -32
- package/package.json +5 -5
- package/src/bin.ts +7 -0
- package/src/generators/db-setup.ts +94 -31
- package/src/generators/init-sql.ts +50 -3
- package/src/generators/middleware.ts +30 -21
- package/src/generators/package-json.ts +31 -12
- package/src/generators/readme.ts +2 -10
- package/src/generators/tsconfig.ts +48 -0
- package/src/index.ts +29 -42
- package/src/preset-project.ts +10 -28
- package/src/stratum-versions.ts +20 -0
- package/turbo.json +19 -0
package/dist/index.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
1
|
// src/index.ts
|
|
4
2
|
import * as fs2 from "fs";
|
|
5
3
|
import * as path2 from "path";
|
|
6
4
|
import crypto2 from "node:crypto";
|
|
7
5
|
import { execSync as execSync2 } from "child_process";
|
|
8
|
-
import { fileURLToPath } from "url";
|
|
9
6
|
|
|
10
7
|
// src/matrix.ts
|
|
11
8
|
var POSTGRES_STRATEGIES = ["rls", "schema", "database"];
|
|
@@ -37,19 +34,14 @@ var ALL_STRATEGIES = ["rls", "schema", "database", "collection", "table-prefix"]
|
|
|
37
34
|
var ALL_ORMS = ["prisma", "drizzle", "sequelize", "knex", "mongoose", "pg"];
|
|
38
35
|
function isValidPreset(preset) {
|
|
39
36
|
const config = VALID_COMBINATIONS[preset.database];
|
|
40
|
-
if (!config)
|
|
41
|
-
|
|
42
|
-
if (!config.
|
|
43
|
-
|
|
44
|
-
if (!config.orms.includes(preset.orm))
|
|
45
|
-
return false;
|
|
46
|
-
if (!config.frameworks.includes(preset.framework))
|
|
47
|
-
return false;
|
|
37
|
+
if (!config) return false;
|
|
38
|
+
if (!config.strategies.includes(preset.strategy)) return false;
|
|
39
|
+
if (!config.orms.includes(preset.orm)) return false;
|
|
40
|
+
if (!config.frameworks.includes(preset.framework)) return false;
|
|
48
41
|
return true;
|
|
49
42
|
}
|
|
50
43
|
function parsePresetString(s) {
|
|
51
|
-
if (!s || typeof s !== "string")
|
|
52
|
-
return null;
|
|
44
|
+
if (!s || typeof s !== "string") return null;
|
|
53
45
|
const parts = s.toLowerCase().split("-");
|
|
54
46
|
let database;
|
|
55
47
|
let strategy;
|
|
@@ -65,14 +57,10 @@ function parsePresetString(s) {
|
|
|
65
57
|
} else {
|
|
66
58
|
return null;
|
|
67
59
|
}
|
|
68
|
-
if (!ALL_DATABASES.includes(database))
|
|
69
|
-
|
|
70
|
-
if (!
|
|
71
|
-
|
|
72
|
-
if (!ALL_ORMS.includes(orm))
|
|
73
|
-
return null;
|
|
74
|
-
if (!ALL_FRAMEWORKS.includes(framework))
|
|
75
|
-
return null;
|
|
60
|
+
if (!ALL_DATABASES.includes(database)) return null;
|
|
61
|
+
if (!ALL_STRATEGIES.includes(strategy)) return null;
|
|
62
|
+
if (!ALL_ORMS.includes(orm)) return null;
|
|
63
|
+
if (!ALL_FRAMEWORKS.includes(framework)) return null;
|
|
76
64
|
return {
|
|
77
65
|
database,
|
|
78
66
|
strategy,
|
|
@@ -182,6 +170,36 @@ volumes:
|
|
|
182
170
|
}
|
|
183
171
|
|
|
184
172
|
// src/generators/init-sql.ts
|
|
173
|
+
function postgresAppRole(dbName) {
|
|
174
|
+
return `${dbName}_app`;
|
|
175
|
+
}
|
|
176
|
+
var POSTGRES_APP_PASSWORD = "dev_app_password";
|
|
177
|
+
function postgresAppRoleSql(dbName, strategy) {
|
|
178
|
+
const role = postgresAppRole(dbName);
|
|
179
|
+
let strategyGrant = "";
|
|
180
|
+
if (strategy === "schema") {
|
|
181
|
+
strategyGrant = `
|
|
182
|
+
-- schema-per-tenant: the app creates one schema per tenant
|
|
183
|
+
GRANT CREATE ON DATABASE ${dbName} TO ${role};
|
|
184
|
+
`;
|
|
185
|
+
} else if (strategy === "database") {
|
|
186
|
+
strategyGrant = `
|
|
187
|
+
-- database-per-tenant: the app creates one database per tenant
|
|
188
|
+
ALTER ROLE ${role} CREATEDB;
|
|
189
|
+
`;
|
|
190
|
+
}
|
|
191
|
+
return `
|
|
192
|
+
-- Application role. The app connects as ${role} (DATABASE_URL), never as the
|
|
193
|
+
-- bootstrap superuser (DATABASE_ADMIN_URL): a superuser or BYPASSRLS role
|
|
194
|
+
-- ignores every row-level security policy, FORCE included. Use the superuser
|
|
195
|
+
-- only for bootstrap and migrations.
|
|
196
|
+
CREATE ROLE ${role} WITH LOGIN PASSWORD '${POSTGRES_APP_PASSWORD}' NOSUPERUSER NOBYPASSRLS NOCREATEROLE;
|
|
197
|
+
GRANT CONNECT ON DATABASE ${dbName} TO ${role};
|
|
198
|
+
GRANT USAGE, CREATE ON SCHEMA public TO ${role};
|
|
199
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${role};
|
|
200
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO ${role};
|
|
201
|
+
${strategyGrant}`;
|
|
202
|
+
}
|
|
185
203
|
function generatePresetInitSql(projectName, preset) {
|
|
186
204
|
const dbName = projectName.replace(/[^a-z0-9]/gi, "_").toLowerCase();
|
|
187
205
|
switch (preset.database) {
|
|
@@ -201,10 +219,16 @@ function generatePostgresInit(projectName, dbName, strategy) {
|
|
|
201
219
|
-- Add RLS policies to each tenant-scoped table:
|
|
202
220
|
--
|
|
203
221
|
-- ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;
|
|
222
|
+
-- ALTER TABLE your_table FORCE ROW LEVEL SECURITY;
|
|
204
223
|
-- CREATE POLICY tenant_isolation ON your_table
|
|
205
|
-
-- USING (tenant_id = current_setting('app.
|
|
224
|
+
-- USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid);
|
|
206
225
|
--
|
|
207
|
-
--
|
|
226
|
+
-- A pooled connection reads the setting as '' after a tenant transaction ends,
|
|
227
|
+
-- or as NULL before the first one. NULLIF makes both return no rows; a bare
|
|
228
|
+
-- ::uuid cast of '' raises an error.
|
|
229
|
+
-- FORCE makes the policy apply to the table owner too; without it, a table
|
|
230
|
+
-- created by the application role is not isolated for that role.
|
|
231
|
+
-- The Stratum db-adapters package sets app.current_tenant_id automatically.
|
|
208
232
|
`;
|
|
209
233
|
}
|
|
210
234
|
return `-- Initialize ${projectName} database
|
|
@@ -215,7 +239,7 @@ CREATE EXTENSION IF NOT EXISTS "ltree";
|
|
|
215
239
|
-- The ltree extension enables hierarchical tenant trees
|
|
216
240
|
-- uuid-ossp provides uuid_generate_v4() for tenant IDs
|
|
217
241
|
COMMENT ON DATABASE ${dbName} IS 'Multi-tenant database for ${projectName}';
|
|
218
|
-
${rlsBlock}`;
|
|
242
|
+
${postgresAppRoleSql(dbName, strategy)}${rlsBlock}`;
|
|
219
243
|
}
|
|
220
244
|
function generateMysqlInit(projectName, dbName) {
|
|
221
245
|
return `-- Initialize ${projectName} database
|
|
@@ -252,7 +276,6 @@ function generateDbSetup(preset) {
|
|
|
252
276
|
}
|
|
253
277
|
function generatePrismaSetup(preset) {
|
|
254
278
|
const provider = preset.database === "mysql" ? "mysql" : "postgresql";
|
|
255
|
-
const envUrl = preset.database === "mysql" ? "mysql://user:password@localhost:3306/dbname" : "postgres://user:password@localhost:5432/dbname";
|
|
256
279
|
return [
|
|
257
280
|
{
|
|
258
281
|
filename: "prisma/schema.prisma",
|
|
@@ -344,9 +367,10 @@ export function createTenantDb(getTenantId: () => string) {
|
|
|
344
367
|
return drizzleWithTenant(db, getTenantId, pool);
|
|
345
368
|
}
|
|
346
369
|
|
|
347
|
-
// Usage:
|
|
370
|
+
// Usage: run tenant queries inside transaction() so the tenant context and
|
|
371
|
+
// the query share one connection.
|
|
348
372
|
// const tenantDb = createTenantDb(() => currentTenantId);
|
|
349
|
-
// const rows = await tenantDb.select().from(orders);
|
|
373
|
+
// const rows = await tenantDb.transaction((tx) => tx.select().from(orders));
|
|
350
374
|
|
|
351
375
|
export { pool };
|
|
352
376
|
`
|
|
@@ -422,59 +446,106 @@ export default config;
|
|
|
422
446
|
},
|
|
423
447
|
{
|
|
424
448
|
filename: "src/stratum-knex.ts",
|
|
425
|
-
content:
|
|
449
|
+
content: preset.database === "postgres" ? KNEX_POSTGRES_SCOPE : knexColumnScope(preset.database)
|
|
450
|
+
}
|
|
451
|
+
];
|
|
452
|
+
}
|
|
453
|
+
var KNEX_POSTGRES_SCOPE = `// Knex with Stratum tenant-scoped queries
|
|
454
|
+
import createKnex, { type Knex } from "knex";
|
|
455
|
+
import config from "../knexfile.js";
|
|
456
|
+
|
|
457
|
+
const knex = createKnex(config);
|
|
458
|
+
|
|
459
|
+
// Run fn in a transaction that sets app.current_tenant_id, the setting that
|
|
460
|
+
// the RLS policies read. The setting ends with the transaction, so run every
|
|
461
|
+
// tenant query through trx, not through knex.
|
|
462
|
+
export async function withTenantScope<T>(
|
|
463
|
+
tenantId: string,
|
|
464
|
+
fn: (trx: Knex.Transaction) => Promise<T>,
|
|
465
|
+
): Promise<T> {
|
|
466
|
+
return knex.transaction(async (trx) => {
|
|
467
|
+
await trx.raw("SELECT set_config('app.current_tenant_id', ?, true)", [tenantId]);
|
|
468
|
+
return fn(trx);
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Usage:
|
|
473
|
+
// const orders = await withTenantScope(currentTenantId, (trx) => trx("orders").select());
|
|
474
|
+
|
|
475
|
+
export { knex };
|
|
476
|
+
`;
|
|
477
|
+
function knexColumnScope(database) {
|
|
478
|
+
return `// Knex with Stratum tenant-scoped queries
|
|
426
479
|
import Knex from "knex";
|
|
427
480
|
import config from "../knexfile.js";
|
|
428
481
|
|
|
429
482
|
const knex = Knex(config);
|
|
430
483
|
|
|
431
484
|
// Create a tenant-scoped query builder.
|
|
432
|
-
// For RLS strategy, set the session variable before queries.
|
|
433
485
|
export async function withTenantScope(tenantId: string, fn: (db: typeof knex) => Promise<void>) {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
await fn(knex);
|
|
437
|
-
} finally {
|
|
438
|
-
${preset.database === "postgres" ? ` await knex.raw("RESET app.current_tenant");` : ` // Scope cleanup not needed for column-based isolation`}
|
|
439
|
-
}
|
|
486
|
+
// For ${database}, scope queries by tenant_id column
|
|
487
|
+
await fn(knex);
|
|
440
488
|
}
|
|
441
489
|
|
|
442
490
|
export { knex };
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
491
|
+
`;
|
|
492
|
+
}
|
|
493
|
+
function generateMongooseSetup(preset) {
|
|
494
|
+
const tenantAccess = preset.strategy === "collection" ? `// Return the tenant's model for a base collection. The tenant's documents are
|
|
495
|
+
// in the collection {baseCollection}_{tenantSlug}, the name that
|
|
496
|
+
// MongoCollectionAdapter from @stratum-hq/mongodb uses.
|
|
497
|
+
export function getTenantModel<T>(
|
|
498
|
+
baseCollection: string,
|
|
499
|
+
schema: mongoose.Schema<T>,
|
|
500
|
+
tenantSlug: string,
|
|
501
|
+
) {
|
|
502
|
+
const name = \`\${baseCollection}_\${assertSlug(tenantSlug)}\`;
|
|
503
|
+
return mainConnection.models[name] ?? mainConnection.model(name, schema, name);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Usage:
|
|
507
|
+
// const Order = getTenantModel("orders", OrderSchema, "tenant_abc");
|
|
508
|
+
// const orders = await Order.find();` : `// Return a connection to the tenant's own database. The database name is
|
|
509
|
+
// stratum_tenant_{tenantSlug}, the name that MongoDatabaseAdapter from
|
|
510
|
+
// @stratum-hq/mongodb uses. useCache returns the same connection on each call.
|
|
511
|
+
export function getTenantConnection(tenantSlug: string) {
|
|
512
|
+
return mainConnection.useDb(\`stratum_tenant_\${assertSlug(tenantSlug)}\`, { useCache: true });
|
|
446
513
|
}
|
|
447
|
-
|
|
514
|
+
|
|
515
|
+
// Usage:
|
|
516
|
+
// const conn = getTenantConnection("tenant_abc");
|
|
517
|
+
// const Order = conn.model("Order", OrderSchema);
|
|
518
|
+
// const orders = await Order.find();`;
|
|
448
519
|
return [
|
|
449
520
|
{
|
|
450
521
|
filename: "src/stratum-mongoose.ts",
|
|
451
522
|
content: `// Mongoose with Stratum multi-tenant support
|
|
452
523
|
import mongoose from "mongoose";
|
|
453
|
-
import { createTenantConnection } from "@stratum-hq/mongodb";
|
|
454
524
|
|
|
455
525
|
// Main connection (used for tenant metadata)
|
|
456
526
|
const mainConnection = mongoose.createConnection(
|
|
457
527
|
process.env.MONGODB_URI || "mongodb://localhost:27017/main",
|
|
458
528
|
);
|
|
459
529
|
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
530
|
+
// A tenant slug becomes part of a MongoDB name, so it must match the Stratum
|
|
531
|
+
// slug rule: a lowercase letter, then lowercase letters, digits or underscores.
|
|
532
|
+
const SLUG_PATTERN = /^[a-z][a-z0-9_]{0,62}$/;
|
|
533
|
+
|
|
534
|
+
function assertSlug(tenantSlug: string): string {
|
|
535
|
+
if (!SLUG_PATTERN.test(tenantSlug)) {
|
|
536
|
+
throw new Error(\`Invalid tenant slug: "\${tenantSlug}"\`);
|
|
537
|
+
}
|
|
538
|
+
return tenantSlug;
|
|
465
539
|
}
|
|
466
540
|
|
|
541
|
+
${tenantAccess}
|
|
542
|
+
|
|
467
543
|
// Define schemas that work across tenant connections
|
|
468
544
|
export const TenantSchema = new mongoose.Schema({
|
|
469
545
|
name: { type: String, required: true },
|
|
470
546
|
createdAt: { type: Date, default: Date.now },
|
|
471
547
|
});
|
|
472
548
|
|
|
473
|
-
// Usage:
|
|
474
|
-
// const conn = getTenantConnection("tenant-abc");
|
|
475
|
-
// const Order = conn.model("Order", OrderSchema);
|
|
476
|
-
// const orders = await Order.find();
|
|
477
|
-
|
|
478
549
|
export { mainConnection };
|
|
479
550
|
`
|
|
480
551
|
}
|
|
@@ -493,7 +564,11 @@ const pool = mysql.createPool({
|
|
|
493
564
|
});
|
|
494
565
|
|
|
495
566
|
// Execute a query scoped to a tenant by filtering on tenant_id
|
|
496
|
-
export async function tenantQuery(
|
|
567
|
+
export async function tenantQuery(
|
|
568
|
+
tenantId: string,
|
|
569
|
+
sql: string,
|
|
570
|
+
params: (string | number | bigint | boolean | Date | null)[] = [],
|
|
571
|
+
) {
|
|
497
572
|
const [rows] = await pool.execute(sql, [...params, tenantId]);
|
|
498
573
|
return rows;
|
|
499
574
|
}
|
|
@@ -518,9 +593,10 @@ const pool = new Pool({
|
|
|
518
593
|
});
|
|
519
594
|
|
|
520
595
|
// Create a tenant-scoped pool.
|
|
521
|
-
//
|
|
596
|
+
// Each query runs in a transaction that sets app.current_tenant_id, the
|
|
597
|
+
// setting that the RLS policies read.
|
|
522
598
|
export function getTenantPool(tenantId: string) {
|
|
523
|
-
return createTenantPool(pool, tenantId);
|
|
599
|
+
return createTenantPool(pool, () => tenantId);
|
|
524
600
|
}
|
|
525
601
|
|
|
526
602
|
// Usage:
|
|
@@ -561,11 +637,13 @@ const port = Number(process.env.PORT) || 3000;
|
|
|
561
637
|
|
|
562
638
|
app.use(express.json());
|
|
563
639
|
|
|
564
|
-
// Tenant extraction middleware
|
|
640
|
+
// Tenant extraction middleware. The tenant comes from the subdomain the
|
|
641
|
+
// request was routed to. Do not take it from a client-supplied header such as
|
|
642
|
+
// x-tenant-id: any caller can set one and pick another tenant. Once you add
|
|
643
|
+
// authentication, check that the signed-in user belongs to this tenant, or
|
|
644
|
+
// derive the tenant from the verified session or JWT instead.
|
|
565
645
|
app.use((req, _res, next) => {
|
|
566
|
-
const tenantId =
|
|
567
|
-
req.headers["x-tenant-id"] as string ||
|
|
568
|
-
req.hostname.split(".")[0];
|
|
646
|
+
const tenantId = req.hostname.split(".")[0];
|
|
569
647
|
(req as any).tenantId = tenantId;
|
|
570
648
|
next();
|
|
571
649
|
});
|
|
@@ -595,12 +673,14 @@ function generateFastifyMiddleware(projectName) {
|
|
|
595
673
|
const fastify = Fastify({ logger: true });
|
|
596
674
|
const port = Number(process.env.PORT) || 3000;
|
|
597
675
|
|
|
598
|
-
// Tenant extraction
|
|
676
|
+
// Tenant extraction hook. The tenant comes from the subdomain the
|
|
677
|
+
// request was routed to. Do not take it from a client-supplied header such as
|
|
678
|
+
// x-tenant-id: any caller can set one and pick another tenant. Once you add
|
|
679
|
+
// authentication, check that the signed-in user belongs to this tenant, or
|
|
680
|
+
// derive the tenant from the verified session or JWT instead.
|
|
599
681
|
fastify.decorateRequest("tenantId", "");
|
|
600
682
|
fastify.addHook("onRequest", async (request) => {
|
|
601
|
-
const tenantId =
|
|
602
|
-
(request.headers["x-tenant-id"] as string) ||
|
|
603
|
-
(request.hostname?.split(".")[0] ?? "");
|
|
683
|
+
const tenantId = request.hostname?.split(".")[0] ?? "";
|
|
604
684
|
(request as any).tenantId = tenantId;
|
|
605
685
|
});
|
|
606
686
|
|
|
@@ -631,14 +711,15 @@ function generateNextjsMiddleware(projectName) {
|
|
|
631
711
|
import { NextRequest, NextResponse } from "next/server";
|
|
632
712
|
|
|
633
713
|
export function middleware(request: NextRequest) {
|
|
714
|
+
// The tenant comes from the subdomain the request was routed to. Any
|
|
715
|
+
// x-tenant-id the client sent is removed first, so server code that reads
|
|
716
|
+
// x-tenant-id only ever sees the value set here. Once you add
|
|
717
|
+
// authentication, check that the signed-in user belongs to this tenant.
|
|
634
718
|
const hostname = request.headers.get("host") || "";
|
|
635
|
-
const
|
|
636
|
-
const headerTenantId = request.headers.get("x-tenant-id");
|
|
637
|
-
const pathTenantId = request.nextUrl.pathname.match(/^\\/tenant\\/([^/]+)/)?.[1];
|
|
638
|
-
|
|
639
|
-
const tenantId = headerTenantId || pathTenantId || subdomain;
|
|
719
|
+
const tenantId = hostname.split(".")[0];
|
|
640
720
|
|
|
641
721
|
const requestHeaders = new Headers(request.headers);
|
|
722
|
+
requestHeaders.delete("x-tenant-id");
|
|
642
723
|
if (tenantId && tenantId !== "localhost" && tenantId !== "www") {
|
|
643
724
|
requestHeaders.set("x-tenant-id", tenantId);
|
|
644
725
|
}
|
|
@@ -661,7 +742,7 @@ export default function Home() {
|
|
|
661
742
|
<p>Multi-tenant app powered by Stratum.</p>
|
|
662
743
|
<ul>
|
|
663
744
|
<li>Configure tenants via the Stratum control plane</li>
|
|
664
|
-
<li>
|
|
745
|
+
<li>Tenant is resolved from the subdomain in <code>middleware.ts</code></li>
|
|
665
746
|
<li>Use <code>@stratum-hq/lib</code> for tenant resolution</li>
|
|
666
747
|
</ul>
|
|
667
748
|
</main>
|
|
@@ -680,11 +761,13 @@ import { serve } from "@hono/node-server";
|
|
|
680
761
|
|
|
681
762
|
const app = new Hono();
|
|
682
763
|
|
|
683
|
-
// Tenant extraction middleware
|
|
764
|
+
// Tenant extraction middleware. The tenant comes from the subdomain the
|
|
765
|
+
// request was routed to. Do not take it from a client-supplied header such as
|
|
766
|
+
// x-tenant-id: any caller can set one and pick another tenant. Once you add
|
|
767
|
+
// authentication, check that the signed-in user belongs to this tenant, or
|
|
768
|
+
// derive the tenant from the verified session or JWT instead.
|
|
684
769
|
app.use("*", async (c, next) => {
|
|
685
|
-
const tenantId =
|
|
686
|
-
c.req.header("x-tenant-id") ||
|
|
687
|
-
new URL(c.req.url).hostname.split(".")[0];
|
|
770
|
+
const tenantId = new URL(c.req.url).hostname.split(".")[0];
|
|
688
771
|
c.set("tenantId", tenantId);
|
|
689
772
|
await next();
|
|
690
773
|
});
|
|
@@ -763,9 +846,11 @@ export class AppController {
|
|
|
763
846
|
export class TenantGuard implements CanActivate {
|
|
764
847
|
canActivate(context: ExecutionContext): boolean {
|
|
765
848
|
const request = context.switchToHttp().getRequest();
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
849
|
+
// The tenant comes from the subdomain the request was routed to. Do not
|
|
850
|
+
// take it from a client-supplied header such as x-tenant-id: any caller
|
|
851
|
+
// can set one and pick another tenant. Once you add authentication, check
|
|
852
|
+
// that the signed-in user belongs to this tenant here.
|
|
853
|
+
const tenantId = request.hostname?.split(".")[0];
|
|
769
854
|
request.tenantId = tenantId || null;
|
|
770
855
|
return true;
|
|
771
856
|
}
|
|
@@ -789,16 +874,516 @@ console.log("Add tenant resolution logic for your use case");
|
|
|
789
874
|
];
|
|
790
875
|
}
|
|
791
876
|
|
|
877
|
+
// src/generators/tsconfig.ts
|
|
878
|
+
function rootSources(preset) {
|
|
879
|
+
return preset.orm === "knex" ? ["knexfile.ts"] : [];
|
|
880
|
+
}
|
|
881
|
+
function generateTsconfig(framework, extraSources = []) {
|
|
882
|
+
const rootDir = extraSources.length > 0 ? "." : "src";
|
|
883
|
+
return JSON.stringify(
|
|
884
|
+
{
|
|
885
|
+
compilerOptions: {
|
|
886
|
+
target: "ESNext",
|
|
887
|
+
module: "NodeNext",
|
|
888
|
+
moduleResolution: "NodeNext",
|
|
889
|
+
strict: true,
|
|
890
|
+
outDir: "dist",
|
|
891
|
+
rootDir,
|
|
892
|
+
declaration: true,
|
|
893
|
+
skipLibCheck: true,
|
|
894
|
+
esModuleInterop: true,
|
|
895
|
+
...framework === "nestjs" ? { experimentalDecorators: true, emitDecoratorMetadata: true } : {},
|
|
896
|
+
...framework === "nextjs" ? { jsx: "preserve", plugins: [{ name: "next" }] } : {}
|
|
897
|
+
},
|
|
898
|
+
include: ["src", ...extraSources],
|
|
899
|
+
exclude: ["node_modules", "dist"]
|
|
900
|
+
},
|
|
901
|
+
null,
|
|
902
|
+
2
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// ../hono/package.json
|
|
907
|
+
var package_default = {
|
|
908
|
+
name: "@stratum-hq/hono",
|
|
909
|
+
version: "1.1.0",
|
|
910
|
+
description: "Stratum Hono integration \u2014 tenant extraction middleware with ALS context",
|
|
911
|
+
keywords: [
|
|
912
|
+
"multi-tenancy",
|
|
913
|
+
"multi-tenant",
|
|
914
|
+
"saas",
|
|
915
|
+
"tenant",
|
|
916
|
+
"hono",
|
|
917
|
+
"middleware",
|
|
918
|
+
"typescript",
|
|
919
|
+
"stratum"
|
|
920
|
+
],
|
|
921
|
+
type: "module",
|
|
922
|
+
main: "./dist/index.js",
|
|
923
|
+
types: "./dist/index.d.ts",
|
|
924
|
+
exports: {
|
|
925
|
+
".": {
|
|
926
|
+
types: "./dist/index.d.ts",
|
|
927
|
+
default: "./dist/index.js"
|
|
928
|
+
}
|
|
929
|
+
},
|
|
930
|
+
files: [
|
|
931
|
+
"dist",
|
|
932
|
+
"README.md",
|
|
933
|
+
"LICENSE"
|
|
934
|
+
],
|
|
935
|
+
scripts: {
|
|
936
|
+
build: "tsc",
|
|
937
|
+
test: "vitest run",
|
|
938
|
+
typecheck: "tsc --noEmit",
|
|
939
|
+
lint: "eslint .",
|
|
940
|
+
clean: "rm -rf dist"
|
|
941
|
+
},
|
|
942
|
+
publishConfig: {
|
|
943
|
+
access: "public",
|
|
944
|
+
registry: "https://registry.npmjs.org/"
|
|
945
|
+
},
|
|
946
|
+
peerDependencies: {
|
|
947
|
+
"@stratum-hq/core": "^1.0.0",
|
|
948
|
+
"@stratum-hq/sdk": "^1.2.0",
|
|
949
|
+
hono: ">=4.0.0"
|
|
950
|
+
},
|
|
951
|
+
devDependencies: {
|
|
952
|
+
"@stratum-hq/core": "*",
|
|
953
|
+
"@stratum-hq/sdk": "*",
|
|
954
|
+
hono: "^4.13.9",
|
|
955
|
+
typescript: "^5.4.0",
|
|
956
|
+
vitest: "^4.1.11"
|
|
957
|
+
},
|
|
958
|
+
license: "MIT",
|
|
959
|
+
author: "Christian Crank",
|
|
960
|
+
homepage: "https://github.com/stratum-hq/Stratum/tree/main/packages/hono#readme",
|
|
961
|
+
bugs: "https://github.com/stratum-hq/Stratum/issues",
|
|
962
|
+
engines: {
|
|
963
|
+
node: ">=20.0.0"
|
|
964
|
+
},
|
|
965
|
+
repository: {
|
|
966
|
+
type: "git",
|
|
967
|
+
url: "https://github.com/stratum-hq/Stratum.git",
|
|
968
|
+
directory: "packages/hono"
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
// ../db-adapters/package.json
|
|
973
|
+
var package_default2 = {
|
|
974
|
+
name: "@stratum-hq/db-adapters",
|
|
975
|
+
version: "1.2.0",
|
|
976
|
+
description: "Stratum PostgreSQL adapters for tenant-scoped query isolation",
|
|
977
|
+
keywords: [
|
|
978
|
+
"multi-tenancy",
|
|
979
|
+
"multi-tenant",
|
|
980
|
+
"saas",
|
|
981
|
+
"tenant",
|
|
982
|
+
"tenant-isolation",
|
|
983
|
+
"postgresql",
|
|
984
|
+
"postgres",
|
|
985
|
+
"row-level-security",
|
|
986
|
+
"rls",
|
|
987
|
+
"schema-isolation",
|
|
988
|
+
"database",
|
|
989
|
+
"stratum"
|
|
990
|
+
],
|
|
991
|
+
main: "./dist/index.js",
|
|
992
|
+
types: "./dist/index.d.ts",
|
|
993
|
+
exports: {
|
|
994
|
+
".": {
|
|
995
|
+
types: "./dist/index.d.ts",
|
|
996
|
+
default: "./dist/index.js"
|
|
997
|
+
}
|
|
998
|
+
},
|
|
999
|
+
files: [
|
|
1000
|
+
"dist",
|
|
1001
|
+
"README.md",
|
|
1002
|
+
"LICENSE"
|
|
1003
|
+
],
|
|
1004
|
+
scripts: {
|
|
1005
|
+
build: "tsc",
|
|
1006
|
+
test: "vitest run",
|
|
1007
|
+
typecheck: "tsc --noEmit",
|
|
1008
|
+
lint: "eslint .",
|
|
1009
|
+
clean: "rm -rf dist"
|
|
1010
|
+
},
|
|
1011
|
+
publishConfig: {
|
|
1012
|
+
access: "public",
|
|
1013
|
+
registry: "https://registry.npmjs.org/"
|
|
1014
|
+
},
|
|
1015
|
+
dependencies: {
|
|
1016
|
+
"@stratum-hq/core": "^1.4.0",
|
|
1017
|
+
pg: "^8.11.0"
|
|
1018
|
+
},
|
|
1019
|
+
peerDependencies: {
|
|
1020
|
+
"drizzle-orm": ">=0.29.0"
|
|
1021
|
+
},
|
|
1022
|
+
peerDependenciesMeta: {
|
|
1023
|
+
"drizzle-orm": {
|
|
1024
|
+
optional: true
|
|
1025
|
+
}
|
|
1026
|
+
},
|
|
1027
|
+
devDependencies: {
|
|
1028
|
+
"@prisma/client": "^5.10.0",
|
|
1029
|
+
"@types/pg": "^8.11.0",
|
|
1030
|
+
"drizzle-orm": "^0.45.3",
|
|
1031
|
+
typescript: "^5.4.0",
|
|
1032
|
+
vitest: "^4.1.11"
|
|
1033
|
+
},
|
|
1034
|
+
license: "MIT",
|
|
1035
|
+
author: "Christian Crank",
|
|
1036
|
+
homepage: "https://github.com/stratum-hq/Stratum/tree/main/packages/db-adapters#readme",
|
|
1037
|
+
bugs: "https://github.com/stratum-hq/Stratum/issues",
|
|
1038
|
+
engines: {
|
|
1039
|
+
node: ">=20.0.0"
|
|
1040
|
+
},
|
|
1041
|
+
repository: {
|
|
1042
|
+
type: "git",
|
|
1043
|
+
url: "https://github.com/stratum-hq/Stratum.git",
|
|
1044
|
+
directory: "packages/db-adapters"
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
|
|
1048
|
+
// ../lib/package.json
|
|
1049
|
+
var package_default3 = {
|
|
1050
|
+
name: "@stratum-hq/lib",
|
|
1051
|
+
version: "1.4.0",
|
|
1052
|
+
description: "Stratum tenant management library - framework-agnostic business logic",
|
|
1053
|
+
keywords: [
|
|
1054
|
+
"multi-tenancy",
|
|
1055
|
+
"multi-tenant",
|
|
1056
|
+
"saas",
|
|
1057
|
+
"b2b",
|
|
1058
|
+
"tenant",
|
|
1059
|
+
"tenant-management",
|
|
1060
|
+
"tenant-isolation",
|
|
1061
|
+
"node",
|
|
1062
|
+
"typescript",
|
|
1063
|
+
"postgresql",
|
|
1064
|
+
"row-level-security",
|
|
1065
|
+
"rls",
|
|
1066
|
+
"schema-isolation",
|
|
1067
|
+
"config-inheritance",
|
|
1068
|
+
"gdpr",
|
|
1069
|
+
"stratum"
|
|
1070
|
+
],
|
|
1071
|
+
main: "./dist/index.js",
|
|
1072
|
+
types: "./dist/index.d.ts",
|
|
1073
|
+
exports: {
|
|
1074
|
+
".": {
|
|
1075
|
+
types: "./dist/index.d.ts",
|
|
1076
|
+
default: "./dist/index.js"
|
|
1077
|
+
}
|
|
1078
|
+
},
|
|
1079
|
+
files: [
|
|
1080
|
+
"dist",
|
|
1081
|
+
"README.md",
|
|
1082
|
+
"LICENSE"
|
|
1083
|
+
],
|
|
1084
|
+
scripts: {
|
|
1085
|
+
build: "tsc && node ../../scripts/copy-dir.mjs src/migrations dist/migrations",
|
|
1086
|
+
test: "vitest run",
|
|
1087
|
+
typecheck: "tsc --noEmit",
|
|
1088
|
+
lint: "eslint .",
|
|
1089
|
+
clean: "rm -rf dist"
|
|
1090
|
+
},
|
|
1091
|
+
publishConfig: {
|
|
1092
|
+
access: "public",
|
|
1093
|
+
registry: "https://registry.npmjs.org/"
|
|
1094
|
+
},
|
|
1095
|
+
dependencies: {
|
|
1096
|
+
"@opentelemetry/api": "^1.9.0",
|
|
1097
|
+
"@stratum-hq/core": "^1.4.0",
|
|
1098
|
+
"@stratum-hq/db-adapters": "^1.2.0",
|
|
1099
|
+
"@stratum-hq/sdk": "^1.2.0",
|
|
1100
|
+
pg: "^8.11.0"
|
|
1101
|
+
},
|
|
1102
|
+
peerDependencies: {
|
|
1103
|
+
"@opentelemetry/api": "^1.0.0"
|
|
1104
|
+
},
|
|
1105
|
+
peerDependenciesMeta: {
|
|
1106
|
+
"@opentelemetry/api": {
|
|
1107
|
+
optional: true
|
|
1108
|
+
}
|
|
1109
|
+
},
|
|
1110
|
+
devDependencies: {
|
|
1111
|
+
"@types/pg": "^8.11.0",
|
|
1112
|
+
typescript: "^5.4.0",
|
|
1113
|
+
vitest: "^4.1.11"
|
|
1114
|
+
},
|
|
1115
|
+
license: "MIT",
|
|
1116
|
+
author: "Christian Crank",
|
|
1117
|
+
homepage: "https://github.com/stratum-hq/Stratum/tree/main/packages/lib#readme",
|
|
1118
|
+
bugs: "https://github.com/stratum-hq/Stratum/issues",
|
|
1119
|
+
engines: {
|
|
1120
|
+
node: ">=20.0.0"
|
|
1121
|
+
},
|
|
1122
|
+
repository: {
|
|
1123
|
+
type: "git",
|
|
1124
|
+
url: "https://github.com/stratum-hq/Stratum.git",
|
|
1125
|
+
directory: "packages/lib"
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
|
|
1129
|
+
// ../mongodb/package.json
|
|
1130
|
+
var package_default4 = {
|
|
1131
|
+
name: "@stratum-hq/mongodb",
|
|
1132
|
+
version: "0.4.0",
|
|
1133
|
+
description: "MongoDB tenant isolation for Stratum \u2014 shared collection, collection-per-tenant, and database-per-tenant strategies",
|
|
1134
|
+
keywords: [
|
|
1135
|
+
"multi-tenancy",
|
|
1136
|
+
"multi-tenant",
|
|
1137
|
+
"saas",
|
|
1138
|
+
"tenant",
|
|
1139
|
+
"tenant-isolation",
|
|
1140
|
+
"mongodb",
|
|
1141
|
+
"mongoose",
|
|
1142
|
+
"database",
|
|
1143
|
+
"stratum"
|
|
1144
|
+
],
|
|
1145
|
+
main: "./dist/index.js",
|
|
1146
|
+
types: "./dist/index.d.ts",
|
|
1147
|
+
exports: {
|
|
1148
|
+
".": {
|
|
1149
|
+
types: "./dist/index.d.ts",
|
|
1150
|
+
default: "./dist/index.js"
|
|
1151
|
+
}
|
|
1152
|
+
},
|
|
1153
|
+
files: [
|
|
1154
|
+
"dist",
|
|
1155
|
+
"README.md",
|
|
1156
|
+
"LICENSE"
|
|
1157
|
+
],
|
|
1158
|
+
scripts: {
|
|
1159
|
+
build: "tsc",
|
|
1160
|
+
test: "vitest run",
|
|
1161
|
+
"test:watch": "vitest",
|
|
1162
|
+
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
1163
|
+
typecheck: "tsc --noEmit",
|
|
1164
|
+
lint: "eslint .",
|
|
1165
|
+
clean: "rm -rf dist"
|
|
1166
|
+
},
|
|
1167
|
+
dependencies: {
|
|
1168
|
+
"@stratum-hq/core": "^1.4.0",
|
|
1169
|
+
"@stratum-hq/sdk": "^1.2.0"
|
|
1170
|
+
},
|
|
1171
|
+
peerDependencies: {
|
|
1172
|
+
mongodb: "^6.0.0 || ^7.0.0"
|
|
1173
|
+
},
|
|
1174
|
+
peerDependenciesMeta: {
|
|
1175
|
+
mongoose: {
|
|
1176
|
+
optional: true
|
|
1177
|
+
}
|
|
1178
|
+
},
|
|
1179
|
+
devDependencies: {
|
|
1180
|
+
"@stratum-hq/test-utils": "*",
|
|
1181
|
+
mongodb: "^7.6.0",
|
|
1182
|
+
mongoose: "^8.0.0",
|
|
1183
|
+
typescript: "^5.9.0",
|
|
1184
|
+
vitest: "^4.1.11"
|
|
1185
|
+
},
|
|
1186
|
+
publishConfig: {
|
|
1187
|
+
access: "public"
|
|
1188
|
+
},
|
|
1189
|
+
license: "MIT",
|
|
1190
|
+
author: "Christian Crank",
|
|
1191
|
+
homepage: "https://github.com/stratum-hq/Stratum/tree/main/packages/mongodb#readme",
|
|
1192
|
+
bugs: "https://github.com/stratum-hq/Stratum/issues",
|
|
1193
|
+
engines: {
|
|
1194
|
+
node: ">=20.0.0"
|
|
1195
|
+
},
|
|
1196
|
+
repository: {
|
|
1197
|
+
type: "git",
|
|
1198
|
+
url: "https://github.com/stratum-hq/Stratum.git",
|
|
1199
|
+
directory: "packages/mongodb"
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1202
|
+
|
|
1203
|
+
// ../mysql/package.json
|
|
1204
|
+
var package_default5 = {
|
|
1205
|
+
name: "@stratum-hq/mysql",
|
|
1206
|
+
version: "0.4.0",
|
|
1207
|
+
description: "MySQL tenant isolation for Stratum \u2014 shared table, table-per-tenant, and database-per-tenant strategies",
|
|
1208
|
+
keywords: [
|
|
1209
|
+
"multi-tenancy",
|
|
1210
|
+
"multi-tenant",
|
|
1211
|
+
"saas",
|
|
1212
|
+
"tenant",
|
|
1213
|
+
"tenant-isolation",
|
|
1214
|
+
"mysql",
|
|
1215
|
+
"database",
|
|
1216
|
+
"typeorm",
|
|
1217
|
+
"knex",
|
|
1218
|
+
"sequelize",
|
|
1219
|
+
"stratum"
|
|
1220
|
+
],
|
|
1221
|
+
main: "./dist/index.js",
|
|
1222
|
+
types: "./dist/index.d.ts",
|
|
1223
|
+
exports: {
|
|
1224
|
+
".": {
|
|
1225
|
+
types: "./dist/index.d.ts",
|
|
1226
|
+
default: "./dist/index.js"
|
|
1227
|
+
}
|
|
1228
|
+
},
|
|
1229
|
+
files: [
|
|
1230
|
+
"dist",
|
|
1231
|
+
"README.md",
|
|
1232
|
+
"LICENSE"
|
|
1233
|
+
],
|
|
1234
|
+
scripts: {
|
|
1235
|
+
build: "tsc",
|
|
1236
|
+
test: "vitest run",
|
|
1237
|
+
"test:watch": "vitest",
|
|
1238
|
+
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
1239
|
+
typecheck: "tsc --noEmit",
|
|
1240
|
+
lint: "eslint .",
|
|
1241
|
+
clean: "rm -rf dist"
|
|
1242
|
+
},
|
|
1243
|
+
dependencies: {
|
|
1244
|
+
"@stratum-hq/core": "^1.4.0",
|
|
1245
|
+
"@stratum-hq/sdk": "^1.2.0"
|
|
1246
|
+
},
|
|
1247
|
+
peerDependencies: {
|
|
1248
|
+
mysql2: "^3.0.0",
|
|
1249
|
+
knex: ">=3.0.0",
|
|
1250
|
+
typeorm: "^1.0.0",
|
|
1251
|
+
sequelize: "^6.0.0"
|
|
1252
|
+
},
|
|
1253
|
+
peerDependenciesMeta: {
|
|
1254
|
+
typeorm: {
|
|
1255
|
+
optional: true
|
|
1256
|
+
},
|
|
1257
|
+
knex: {
|
|
1258
|
+
optional: true
|
|
1259
|
+
},
|
|
1260
|
+
sequelize: {
|
|
1261
|
+
optional: true
|
|
1262
|
+
}
|
|
1263
|
+
},
|
|
1264
|
+
devDependencies: {
|
|
1265
|
+
knex: "^3.3.0",
|
|
1266
|
+
mysql2: "^3.24.4",
|
|
1267
|
+
typeorm: "^1.1.1",
|
|
1268
|
+
typescript: "^5.9.0",
|
|
1269
|
+
vitest: "^4.1.11"
|
|
1270
|
+
},
|
|
1271
|
+
publishConfig: {
|
|
1272
|
+
access: "public"
|
|
1273
|
+
},
|
|
1274
|
+
license: "MIT",
|
|
1275
|
+
author: "Christian Crank",
|
|
1276
|
+
homepage: "https://github.com/stratum-hq/Stratum/tree/main/packages/mysql#readme",
|
|
1277
|
+
bugs: "https://github.com/stratum-hq/Stratum/issues",
|
|
1278
|
+
engines: {
|
|
1279
|
+
node: ">=20.0.0"
|
|
1280
|
+
},
|
|
1281
|
+
repository: {
|
|
1282
|
+
type: "git",
|
|
1283
|
+
url: "https://github.com/stratum-hq/Stratum.git",
|
|
1284
|
+
directory: "packages/mysql"
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
|
|
1288
|
+
// ../nestjs/package.json
|
|
1289
|
+
var package_default6 = {
|
|
1290
|
+
name: "@stratum-hq/nestjs",
|
|
1291
|
+
version: "1.2.0",
|
|
1292
|
+
description: "Stratum NestJS integration \u2014 StratumGuard, @Tenant() decorator, and StratumModule",
|
|
1293
|
+
keywords: [
|
|
1294
|
+
"multi-tenancy",
|
|
1295
|
+
"multi-tenant",
|
|
1296
|
+
"saas",
|
|
1297
|
+
"tenant",
|
|
1298
|
+
"nestjs",
|
|
1299
|
+
"nest",
|
|
1300
|
+
"guard",
|
|
1301
|
+
"decorator",
|
|
1302
|
+
"middleware",
|
|
1303
|
+
"typescript",
|
|
1304
|
+
"stratum"
|
|
1305
|
+
],
|
|
1306
|
+
type: "module",
|
|
1307
|
+
main: "./dist/index.js",
|
|
1308
|
+
types: "./dist/index.d.ts",
|
|
1309
|
+
exports: {
|
|
1310
|
+
".": {
|
|
1311
|
+
types: "./dist/index.d.ts",
|
|
1312
|
+
default: "./dist/index.js"
|
|
1313
|
+
}
|
|
1314
|
+
},
|
|
1315
|
+
files: [
|
|
1316
|
+
"dist",
|
|
1317
|
+
"README.md",
|
|
1318
|
+
"LICENSE"
|
|
1319
|
+
],
|
|
1320
|
+
scripts: {
|
|
1321
|
+
build: "tsc",
|
|
1322
|
+
test: "vitest run",
|
|
1323
|
+
typecheck: "tsc --noEmit",
|
|
1324
|
+
lint: "eslint .",
|
|
1325
|
+
clean: "rm -rf dist"
|
|
1326
|
+
},
|
|
1327
|
+
publishConfig: {
|
|
1328
|
+
access: "public",
|
|
1329
|
+
registry: "https://registry.npmjs.org/"
|
|
1330
|
+
},
|
|
1331
|
+
peerDependencies: {
|
|
1332
|
+
"@nestjs/common": ">=10.0.0",
|
|
1333
|
+
"@nestjs/core": ">=10.0.0",
|
|
1334
|
+
"@stratum-hq/core": "^1.0.0",
|
|
1335
|
+
"@stratum-hq/sdk": "^1.2.0",
|
|
1336
|
+
"reflect-metadata": ">=0.1.13"
|
|
1337
|
+
},
|
|
1338
|
+
devDependencies: {
|
|
1339
|
+
"@nestjs/common": "^12.1.2",
|
|
1340
|
+
"@nestjs/core": "^12.1.2",
|
|
1341
|
+
"@nestjs/testing": "^12.1.2",
|
|
1342
|
+
"@stratum-hq/core": "*",
|
|
1343
|
+
"@stratum-hq/sdk": "*",
|
|
1344
|
+
"reflect-metadata": "^0.2.2",
|
|
1345
|
+
typescript: "^5.4.0",
|
|
1346
|
+
vitest: "^4.1.11"
|
|
1347
|
+
},
|
|
1348
|
+
license: "MIT",
|
|
1349
|
+
author: "Christian Crank",
|
|
1350
|
+
homepage: "https://github.com/stratum-hq/Stratum/tree/main/packages/nestjs#readme",
|
|
1351
|
+
bugs: "https://github.com/stratum-hq/Stratum/issues",
|
|
1352
|
+
engines: {
|
|
1353
|
+
node: ">=20.0.0"
|
|
1354
|
+
},
|
|
1355
|
+
repository: {
|
|
1356
|
+
type: "git",
|
|
1357
|
+
url: "https://github.com/stratum-hq/Stratum.git",
|
|
1358
|
+
directory: "packages/nestjs"
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
|
|
1362
|
+
// src/stratum-versions.ts
|
|
1363
|
+
var STRATUM_RANGES = {
|
|
1364
|
+
"@stratum-hq/db-adapters": `^${package_default2.version}`,
|
|
1365
|
+
"@stratum-hq/hono": `^${package_default.version}`,
|
|
1366
|
+
"@stratum-hq/lib": `^${package_default3.version}`,
|
|
1367
|
+
"@stratum-hq/mongodb": `^${package_default4.version}`,
|
|
1368
|
+
"@stratum-hq/mysql": `^${package_default5.version}`,
|
|
1369
|
+
"@stratum-hq/nestjs": `^${package_default6.version}`
|
|
1370
|
+
};
|
|
1371
|
+
|
|
792
1372
|
// src/generators/package-json.ts
|
|
793
1373
|
function generatePresetPackageJson(projectName, preset) {
|
|
794
1374
|
const deps = {
|
|
795
|
-
"@stratum-hq/lib": "
|
|
1375
|
+
"@stratum-hq/lib": STRATUM_RANGES["@stratum-hq/lib"]
|
|
796
1376
|
};
|
|
797
1377
|
const devDeps = {
|
|
798
1378
|
typescript: "^5.3.0",
|
|
799
1379
|
"@types/node": "^20.11.0"
|
|
800
1380
|
};
|
|
801
|
-
|
|
1381
|
+
if (preset.framework === "nestjs") {
|
|
1382
|
+
devDeps["tsc-watch"] = "^7.2.0";
|
|
1383
|
+
} else if (preset.framework !== "nextjs") {
|
|
1384
|
+
devDeps["tsx"] = "^4.7.0";
|
|
1385
|
+
}
|
|
1386
|
+
addDatabaseDeps(deps, devDeps, preset);
|
|
802
1387
|
addOrmDeps(deps, devDeps, preset);
|
|
803
1388
|
addFrameworkDeps(deps, devDeps, preset);
|
|
804
1389
|
addStratumDeps(deps, preset);
|
|
@@ -820,9 +1405,12 @@ function generatePresetPackageJson(projectName, preset) {
|
|
|
820
1405
|
2
|
|
821
1406
|
);
|
|
822
1407
|
}
|
|
823
|
-
function addDatabaseDeps(deps, preset) {
|
|
1408
|
+
function addDatabaseDeps(deps, devDeps, preset) {
|
|
824
1409
|
switch (preset.database) {
|
|
825
1410
|
case "postgres":
|
|
1411
|
+
if (preset.orm === "pg" || preset.orm === "prisma" || preset.orm === "drizzle") {
|
|
1412
|
+
devDeps["@types/pg"] = "^8.11.0";
|
|
1413
|
+
}
|
|
826
1414
|
if (preset.orm !== "prisma" && preset.orm !== "drizzle" && preset.orm !== "sequelize") {
|
|
827
1415
|
deps["pg"] = "^8.11.0";
|
|
828
1416
|
}
|
|
@@ -912,29 +1500,29 @@ function addFrameworkDeps(deps, devDeps, preset) {
|
|
|
912
1500
|
}
|
|
913
1501
|
function addStratumDeps(deps, preset) {
|
|
914
1502
|
if (preset.database === "postgres" && preset.orm !== "mongoose") {
|
|
915
|
-
deps["@stratum-hq/db-adapters"] = "
|
|
1503
|
+
deps["@stratum-hq/db-adapters"] = STRATUM_RANGES["@stratum-hq/db-adapters"];
|
|
916
1504
|
}
|
|
917
1505
|
if (preset.database === "mongodb") {
|
|
918
|
-
deps["@stratum-hq/mongodb"] = "
|
|
1506
|
+
deps["@stratum-hq/mongodb"] = STRATUM_RANGES["@stratum-hq/mongodb"];
|
|
919
1507
|
}
|
|
920
1508
|
if (preset.database === "mysql") {
|
|
921
|
-
deps["@stratum-hq/mysql"] = "
|
|
1509
|
+
deps["@stratum-hq/mysql"] = STRATUM_RANGES["@stratum-hq/mysql"];
|
|
922
1510
|
}
|
|
923
1511
|
if (preset.framework === "hono") {
|
|
924
|
-
deps["@stratum-hq/hono"] = "
|
|
1512
|
+
deps["@stratum-hq/hono"] = STRATUM_RANGES["@stratum-hq/hono"];
|
|
925
1513
|
}
|
|
926
1514
|
if (preset.framework === "nestjs") {
|
|
927
|
-
deps["@stratum-hq/nestjs"] = "
|
|
1515
|
+
deps["@stratum-hq/nestjs"] = STRATUM_RANGES["@stratum-hq/nestjs"];
|
|
928
1516
|
}
|
|
929
1517
|
}
|
|
930
1518
|
function getScripts(preset) {
|
|
931
1519
|
if (preset.framework === "nextjs") {
|
|
932
1520
|
return { dev: "next dev", build: "next build", start: "next start" };
|
|
933
1521
|
}
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
}
|
|
937
|
-
return { dev
|
|
1522
|
+
const entry = preset.framework === "nestjs" ? "main" : "index";
|
|
1523
|
+
const emittedDir = rootSources(preset).length > 0 ? "dist/src" : "dist";
|
|
1524
|
+
const dev = preset.framework === "nestjs" ? `tsc-watch --onSuccess "node --env-file=.env ${emittedDir}/${entry}.js"` : `tsx watch --env-file=.env src/${entry}.ts`;
|
|
1525
|
+
return { dev, build: "tsc", start: `node ${emittedDir}/${entry}.js` };
|
|
938
1526
|
}
|
|
939
1527
|
function sortKeys(obj) {
|
|
940
1528
|
const sorted = {};
|
|
@@ -946,7 +1534,6 @@ function sortKeys(obj) {
|
|
|
946
1534
|
|
|
947
1535
|
// src/generators/readme.ts
|
|
948
1536
|
function generatePresetReadme(projectName, preset) {
|
|
949
|
-
const devCmd = getDevCommand(preset);
|
|
950
1537
|
const dbStartCmd = getDbStartInfo(preset);
|
|
951
1538
|
const dbSetupNote = getDbSetupNote(preset);
|
|
952
1539
|
return `# ${projectName}
|
|
@@ -981,7 +1568,7 @@ ${dbSetupNote}
|
|
|
981
1568
|
### 4. Run the app
|
|
982
1569
|
|
|
983
1570
|
\`\`\`bash
|
|
984
|
-
|
|
1571
|
+
npm run dev
|
|
985
1572
|
\`\`\`
|
|
986
1573
|
|
|
987
1574
|
## Multi-tenancy
|
|
@@ -993,15 +1580,6 @@ ${getStrategyDescription(preset.strategy)}
|
|
|
993
1580
|
See the [Stratum docs](https://github.com/stratum-hq/Stratum) for full reference.
|
|
994
1581
|
`;
|
|
995
1582
|
}
|
|
996
|
-
function getDevCommand(preset) {
|
|
997
|
-
if (preset.framework === "nextjs")
|
|
998
|
-
return "npm run dev";
|
|
999
|
-
if (preset.framework === "nestjs")
|
|
1000
|
-
return "node --env-file=.env src/main.ts";
|
|
1001
|
-
if (preset.framework === "none")
|
|
1002
|
-
return "node --env-file=.env src/index.ts";
|
|
1003
|
-
return "node --env-file=.env src/index.ts";
|
|
1004
|
-
}
|
|
1005
1583
|
function getDbStartInfo(preset) {
|
|
1006
1584
|
switch (preset.database) {
|
|
1007
1585
|
case "postgres":
|
|
@@ -1038,7 +1616,7 @@ function getStrategyDescription(strategy) {
|
|
|
1038
1616
|
switch (strategy) {
|
|
1039
1617
|
case "rls":
|
|
1040
1618
|
return `- **Row-Level Security** -- PostgreSQL RLS policies filter rows by tenant automatically
|
|
1041
|
-
- Each query sets \`app.
|
|
1619
|
+
- Each query sets \`app.current_tenant_id\` and RLS enforces isolation
|
|
1042
1620
|
- All tenants share one database and schema`;
|
|
1043
1621
|
case "schema":
|
|
1044
1622
|
return `- **Schema-per-tenant** -- each tenant gets a dedicated PostgreSQL schema
|
|
@@ -1074,9 +1652,14 @@ function generatePresetEnv(projectName, preset) {
|
|
|
1074
1652
|
const dbName = projectName.replace(/[^a-z0-9]/gi, "_").toLowerCase();
|
|
1075
1653
|
const jwtSecret = crypto.randomBytes(32).toString("base64url");
|
|
1076
1654
|
let dbUrl;
|
|
1655
|
+
let adminUrlLine = "";
|
|
1077
1656
|
switch (preset.database) {
|
|
1078
1657
|
case "postgres":
|
|
1079
|
-
dbUrl = `postgres://${dbName}
|
|
1658
|
+
dbUrl = `postgres://${postgresAppRole(dbName)}:${POSTGRES_APP_PASSWORD}@localhost:5432/${dbName}`;
|
|
1659
|
+
adminUrlLine = `
|
|
1660
|
+
# Superuser: bootstrap and migrations only. It bypasses row-level security.
|
|
1661
|
+
DATABASE_ADMIN_URL=postgres://${dbName}:dev_password@localhost:5432/${dbName}
|
|
1662
|
+
`;
|
|
1080
1663
|
break;
|
|
1081
1664
|
case "mongodb":
|
|
1082
1665
|
dbUrl = `mongodb://${dbName}:dev_password@localhost:27017/${dbName}?authSource=admin`;
|
|
@@ -1091,7 +1674,7 @@ function generatePresetEnv(projectName, preset) {
|
|
|
1091
1674
|
|
|
1092
1675
|
# Database
|
|
1093
1676
|
${urlKey}=${dbUrl}
|
|
1094
|
-
|
|
1677
|
+
${adminUrlLine}
|
|
1095
1678
|
# Authentication
|
|
1096
1679
|
JWT_SECRET=${jwtSecret}
|
|
1097
1680
|
|
|
@@ -1137,27 +1720,7 @@ Creating ${projectName} with preset ${presetLabel}...
|
|
|
1137
1720
|
);
|
|
1138
1721
|
writeFile(
|
|
1139
1722
|
path.join(targetDir, "tsconfig.json"),
|
|
1140
|
-
|
|
1141
|
-
{
|
|
1142
|
-
compilerOptions: {
|
|
1143
|
-
target: "ESNext",
|
|
1144
|
-
module: "NodeNext",
|
|
1145
|
-
moduleResolution: "NodeNext",
|
|
1146
|
-
strict: true,
|
|
1147
|
-
outDir: "dist",
|
|
1148
|
-
rootDir: "src",
|
|
1149
|
-
declaration: true,
|
|
1150
|
-
skipLibCheck: true,
|
|
1151
|
-
esModuleInterop: true,
|
|
1152
|
-
...preset.framework === "nestjs" ? { experimentalDecorators: true, emitDecoratorMetadata: true } : {},
|
|
1153
|
-
...preset.framework === "nextjs" ? { jsx: "preserve", plugins: [{ name: "next" }] } : {}
|
|
1154
|
-
},
|
|
1155
|
-
include: ["src"],
|
|
1156
|
-
exclude: ["node_modules", "dist"]
|
|
1157
|
-
},
|
|
1158
|
-
null,
|
|
1159
|
-
2
|
|
1160
|
-
)
|
|
1723
|
+
generateTsconfig(preset.framework, rootSources(preset))
|
|
1161
1724
|
);
|
|
1162
1725
|
if (!skipInstall) {
|
|
1163
1726
|
console.log("\nInstalling dependencies...\n");
|
|
@@ -1260,7 +1823,7 @@ function generatePackageJson(projectName, template) {
|
|
|
1260
1823
|
}
|
|
1261
1824
|
};
|
|
1262
1825
|
const deps = {
|
|
1263
|
-
"@stratum-hq/lib": "
|
|
1826
|
+
"@stratum-hq/lib": STRATUM_RANGES["@stratum-hq/lib"],
|
|
1264
1827
|
pg: "^8.11.0",
|
|
1265
1828
|
...frameworkDeps[template]
|
|
1266
1829
|
};
|
|
@@ -1270,11 +1833,15 @@ function generatePackageJson(projectName, template) {
|
|
|
1270
1833
|
version: "0.1.0",
|
|
1271
1834
|
private: true,
|
|
1272
1835
|
type: "module",
|
|
1273
|
-
scripts: template === "nextjs" ? { dev: "next dev", build: "next build", start: "next start" } :
|
|
1836
|
+
scripts: template === "nextjs" ? { dev: "next dev", build: "next build", start: "next start" } : (
|
|
1837
|
+
// Node 20 cannot run a .ts file, so dev runs the source through tsx.
|
|
1838
|
+
{ dev: "tsx watch --env-file=.env src/index.ts", build: "tsc", start: "node dist/index.js" }
|
|
1839
|
+
),
|
|
1274
1840
|
dependencies: deps,
|
|
1275
1841
|
devDependencies: {
|
|
1276
1842
|
typescript: "^5.3.0",
|
|
1277
|
-
"@types/node": "^20.11.0"
|
|
1843
|
+
"@types/node": "^20.11.0",
|
|
1844
|
+
...template === "nextjs" ? {} : { tsx: "^4.7.0", "@types/pg": "^8.11.0" }
|
|
1278
1845
|
},
|
|
1279
1846
|
engines: {
|
|
1280
1847
|
node: ">=20.0.0"
|
|
@@ -1322,7 +1889,7 @@ CREATE EXTENSION IF NOT EXISTS "ltree";
|
|
|
1322
1889
|
-- The ltree extension enables hierarchical tenant trees
|
|
1323
1890
|
-- uuid-ossp provides uuid_generate_v4() for tenant IDs
|
|
1324
1891
|
COMMENT ON DATABASE ${dbName} IS 'Multi-tenant database for ${projectName}';
|
|
1325
|
-
`;
|
|
1892
|
+
${postgresAppRoleSql(dbName)}`;
|
|
1326
1893
|
}
|
|
1327
1894
|
function generateEnv(projectName) {
|
|
1328
1895
|
const dbName = projectName.replace(/[^a-z0-9]/gi, "_").toLowerCase();
|
|
@@ -1330,8 +1897,12 @@ function generateEnv(projectName) {
|
|
|
1330
1897
|
return `# Environment variables for ${projectName}
|
|
1331
1898
|
# Copy to .env and fill in values
|
|
1332
1899
|
|
|
1333
|
-
# Database
|
|
1334
|
-
|
|
1900
|
+
# Database. The app connects as the non-superuser role created in init.sql,
|
|
1901
|
+
# so row-level security applies to it.
|
|
1902
|
+
DATABASE_URL=postgres://${postgresAppRole(dbName)}:${POSTGRES_APP_PASSWORD}@localhost:5432/${dbName}
|
|
1903
|
+
|
|
1904
|
+
# Superuser: bootstrap and migrations only. It bypasses row-level security.
|
|
1905
|
+
DATABASE_ADMIN_URL=postgres://${dbName}:dev_password@localhost:5432/${dbName}
|
|
1335
1906
|
|
|
1336
1907
|
# Authentication
|
|
1337
1908
|
JWT_SECRET=${jwtSecret}
|
|
@@ -1419,7 +1990,7 @@ export default function Home() {
|
|
|
1419
1990
|
<p>Multi-tenant app powered by Stratum.</p>
|
|
1420
1991
|
<ul>
|
|
1421
1992
|
<li>Configure tenants via the Stratum control plane</li>
|
|
1422
|
-
<li>
|
|
1993
|
+
<li>Tenant is resolved from the subdomain in <code>middleware.ts</code></li>
|
|
1423
1994
|
<li>Use <code>@stratum-hq/lib</code> for tenant resolution</li>
|
|
1424
1995
|
</ul>
|
|
1425
1996
|
</main>
|
|
@@ -1428,18 +1999,19 @@ export default function Home() {
|
|
|
1428
1999
|
`;
|
|
1429
2000
|
}
|
|
1430
2001
|
function generateNextjsMiddleware2() {
|
|
1431
|
-
return `// middleware.ts \u2014 tenant resolution via subdomain
|
|
2002
|
+
return `// middleware.ts \u2014 tenant resolution via subdomain
|
|
1432
2003
|
import { NextRequest, NextResponse } from "next/server";
|
|
1433
2004
|
|
|
1434
2005
|
export function middleware(request: NextRequest) {
|
|
2006
|
+
// The tenant comes from the subdomain the request was routed to. Any
|
|
2007
|
+
// x-tenant-id the client sent is removed first, so server code that reads
|
|
2008
|
+
// x-tenant-id only ever sees the value set here. Once you add
|
|
2009
|
+
// authentication, check that the signed-in user belongs to this tenant.
|
|
1435
2010
|
const hostname = request.headers.get("host") || "";
|
|
1436
|
-
const
|
|
1437
|
-
const headerTenantId = request.headers.get("x-tenant-id");
|
|
1438
|
-
const pathTenantId = request.nextUrl.pathname.match(/^\\/tenant\\/([^/]+)/)?.[1];
|
|
1439
|
-
|
|
1440
|
-
const tenantId = headerTenantId || pathTenantId || subdomain;
|
|
2011
|
+
const tenantId = hostname.split(".")[0];
|
|
1441
2012
|
|
|
1442
2013
|
const requestHeaders = new Headers(request.headers);
|
|
2014
|
+
requestHeaders.delete("x-tenant-id");
|
|
1443
2015
|
if (tenantId && tenantId !== "localhost" && tenantId !== "www") {
|
|
1444
2016
|
requestHeaders.set("x-tenant-id", tenantId);
|
|
1445
2017
|
}
|
|
@@ -1453,7 +2025,6 @@ export const config = {
|
|
|
1453
2025
|
`;
|
|
1454
2026
|
}
|
|
1455
2027
|
function generateReadme(projectName, template) {
|
|
1456
|
-
const devCmd = template === "nextjs" ? "npm run dev" : "node --env-file=.env src/index.ts";
|
|
1457
2028
|
return `# ${projectName}
|
|
1458
2029
|
|
|
1459
2030
|
A multi-tenant application built with [Stratum](https://github.com/stratum-hq/Stratum).
|
|
@@ -1482,7 +2053,7 @@ npm install
|
|
|
1482
2053
|
### 4. Run the app
|
|
1483
2054
|
|
|
1484
2055
|
\`\`\`bash
|
|
1485
|
-
|
|
2056
|
+
npm run dev
|
|
1486
2057
|
\`\`\`
|
|
1487
2058
|
|
|
1488
2059
|
## Project structure
|
|
@@ -1493,14 +2064,14 @@ ${projectName}/
|
|
|
1493
2064
|
\u251C\u2500\u2500 docker-compose.yml
|
|
1494
2065
|
\u251C\u2500\u2500 init.sql # DB extensions (uuid-ossp, ltree)
|
|
1495
2066
|
\u251C\u2500\u2500 .env.example
|
|
1496
|
-
\u2514\u2500\u2500 package.json
|
|
2067
|
+
${template === "nextjs" ? "" : "\u251C\u2500\u2500 tsconfig.json\n"}\u2514\u2500\u2500 package.json
|
|
1497
2068
|
\`\`\`
|
|
1498
2069
|
|
|
1499
2070
|
## Multi-tenancy
|
|
1500
2071
|
|
|
1501
2072
|
This project uses Stratum for hierarchical multi-tenancy:
|
|
1502
2073
|
|
|
1503
|
-
- **Tenant resolution** \u2014 via
|
|
2074
|
+
- **Tenant resolution** \u2014 via subdomain (see \`middleware.ts\`); bind it to the signed-in user once you add authentication
|
|
1504
2075
|
- **Config inheritance** \u2014 settings flow down the tenant tree with override support
|
|
1505
2076
|
- **Permission ABAC** \u2014 role-based permissions with tenant-scoped enforcement
|
|
1506
2077
|
|
|
@@ -1520,8 +2091,10 @@ Creating ${projectName} with ${template} template...
|
|
|
1520
2091
|
writeFile2(path2.join(targetDir, ".env.example"), generateEnv(projectName));
|
|
1521
2092
|
if (template === "express") {
|
|
1522
2093
|
writeFile2(path2.join(targetDir, "src", "index.ts"), generateExpressServer(projectName));
|
|
2094
|
+
writeFile2(path2.join(targetDir, "tsconfig.json"), generateTsconfig(template));
|
|
1523
2095
|
} else if (template === "fastify") {
|
|
1524
2096
|
writeFile2(path2.join(targetDir, "src", "index.ts"), generateFastifyServer(projectName));
|
|
2097
|
+
writeFile2(path2.join(targetDir, "tsconfig.json"), generateTsconfig(template));
|
|
1525
2098
|
} else if (template === "nextjs") {
|
|
1526
2099
|
writeFile2(path2.join(targetDir, "src", "app", "page.tsx"), generateNextjsPage(projectName));
|
|
1527
2100
|
writeFile2(path2.join(targetDir, "middleware.ts"), generateNextjsMiddleware2());
|
|
@@ -1599,26 +2172,9 @@ Success! Created ${projectName} at ${targetDir}
|
|
|
1599
2172
|
if (skipInstall) {
|
|
1600
2173
|
console.log(" npm install");
|
|
1601
2174
|
}
|
|
1602
|
-
|
|
1603
|
-
const parsed = parsePresetString(preset);
|
|
1604
|
-
if (parsed.framework === "nextjs") {
|
|
1605
|
-
console.log(" npm run dev");
|
|
1606
|
-
} else if (parsed.framework === "nestjs") {
|
|
1607
|
-
console.log(" node --env-file=.env src/main.ts");
|
|
1608
|
-
} else {
|
|
1609
|
-
console.log(" node --env-file=.env src/index.ts");
|
|
1610
|
-
}
|
|
1611
|
-
} else if (template === "nextjs") {
|
|
1612
|
-
console.log(" npm run dev");
|
|
1613
|
-
} else {
|
|
1614
|
-
console.log(" node --env-file=.env src/index.ts");
|
|
1615
|
-
}
|
|
2175
|
+
console.log(" npm run dev");
|
|
1616
2176
|
console.log("");
|
|
1617
2177
|
}
|
|
1618
|
-
var __filename = fileURLToPath(import.meta.url);
|
|
1619
|
-
if (process.argv[1] === __filename) {
|
|
1620
|
-
main(process.argv.slice(2));
|
|
1621
|
-
}
|
|
1622
2178
|
export {
|
|
1623
2179
|
createProject,
|
|
1624
2180
|
main,
|