@stratum-hq/create 0.3.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # @stratum-hq/create
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 1669fd7: Harden defaults in generated projects, CLI checks, React hooks and test helpers (GHSA-rrrp-gww6-44gr). Behavior changes: `StratumProvider`'s `apiKey` is optional and generated React code uses a server-side proxy instead; `TenantThemeProvider` ignores `customCss` that is not plain declarations; `assertConfigInheritance` now takes a Stratum instance instead of a pg pool.
8
+
9
+ ### Patch Changes
10
+
11
+ - 9de2ddb: Remove unused code from the generator. No change in generated output.
12
+ - dca0826: Harden tenant isolation in the schema-per-tenant and database-per-tenant strategies and the Prisma and Drizzle adapters (GHSA-jhhc-cm2c-jh27). Behavior change: the schema-per-tenant `search_path` is now the tenant schema alone, without `public`; queries that call extension functions or types from another schema must schema-qualify them or opt that schema in with the new `extraSearchPath` option.
13
+
14
+ ## 0.3.1
15
+
16
+ ### Patch Changes
17
+
18
+ - b55ae70: Correct and complete package metadata for the npm registry listing.
19
+
20
+ Every published package now declares `license` (MIT), `author`, `homepage`, and
21
+ `bugs`. Runtime packages declare `engines` (Node >=20) to match the project's
22
+ support policy; this fixes `@stratum-hq/cli`, which previously declared Node >=18.
23
+ `@stratum-hq/mysql` and `@stratum-hq/mongodb` gain the `keywords` array they were
24
+ missing. No runtime code changes.
25
+
26
+ - c17b1a5: Point `@stratum-hq/create`'s `exports["./matrix"]` at built output instead of raw source (#219, from the #133 v1.0 surface review).
27
+
28
+ The `./matrix` subpath previously resolved (and shipped) `./src/matrix.ts` for both the `import` and `types` conditions, blessing a raw-source subpath unlike every other package. The build now emits `dist/matrix.js` and `./matrix` resolves there, matching the package's `.` entry. The stack-combination matrix API is unchanged.
29
+
30
+ - c17b1a5: Give the `@stratum-hq/db-adapters` barrel one consistent naming scheme for the tenant-context helpers (#219, from the #133 v1.0 surface review).
31
+
32
+ The barrel previously exposed the same "run in tenant context" concept under colliding names patched over with `as` aliases. The 1.0 names use an `<orm>` prefix so no export is an alias workaround:
33
+ - `withTenant` (Prisma) -> `prismaWithTenant`
34
+ - `withDrizzleTenant` -> `drizzleWithTenant`
35
+ - `withTenantScope` (Sequelize) -> `sequelizeWithTenantScope`
36
+ - `withDrizzleTenantScope` -> `drizzleWithTenantScope`
37
+ - `enableRLSMigration` (migration helper) -> `enableRLSForMigration` (distinct from the runtime `enableRLS`)
38
+
39
+ Behavior is identical; only the exported names change. The `@stratum-hq/cli` and `@stratum-hq/create` scaffolding templates emit the new `prismaWithTenant` name. Update your imports to the new names.
40
+
41
+ - 4adcbb5: Stop shipping test files in published tarballs. tsc-built packages now exclude **tests** directories and .test/.spec files from compilation, so dist and the tarball contain only real package output. The create package, which ships source for its ./matrix export, excludes tests via .npmignore instead. The vitest runner is unaffected and still runs tests from src.
42
+
3
43
  ## 0.3.0
4
44
 
5
45
  ### Minor Changes
package/dist/index.js CHANGED
@@ -37,19 +37,14 @@ var ALL_STRATEGIES = ["rls", "schema", "database", "collection", "table-prefix"]
37
37
  var ALL_ORMS = ["prisma", "drizzle", "sequelize", "knex", "mongoose", "pg"];
38
38
  function isValidPreset(preset) {
39
39
  const config = VALID_COMBINATIONS[preset.database];
40
- if (!config)
41
- return false;
42
- if (!config.strategies.includes(preset.strategy))
43
- return false;
44
- if (!config.orms.includes(preset.orm))
45
- return false;
46
- if (!config.frameworks.includes(preset.framework))
47
- return false;
40
+ if (!config) return false;
41
+ if (!config.strategies.includes(preset.strategy)) return false;
42
+ if (!config.orms.includes(preset.orm)) return false;
43
+ if (!config.frameworks.includes(preset.framework)) return false;
48
44
  return true;
49
45
  }
50
46
  function parsePresetString(s) {
51
- if (!s || typeof s !== "string")
52
- return null;
47
+ if (!s || typeof s !== "string") return null;
53
48
  const parts = s.toLowerCase().split("-");
54
49
  let database;
55
50
  let strategy;
@@ -65,14 +60,10 @@ function parsePresetString(s) {
65
60
  } else {
66
61
  return null;
67
62
  }
68
- if (!ALL_DATABASES.includes(database))
69
- return null;
70
- if (!ALL_STRATEGIES.includes(strategy))
71
- return null;
72
- if (!ALL_ORMS.includes(orm))
73
- return null;
74
- if (!ALL_FRAMEWORKS.includes(framework))
75
- return null;
63
+ if (!ALL_DATABASES.includes(database)) return null;
64
+ if (!ALL_STRATEGIES.includes(strategy)) return null;
65
+ if (!ALL_ORMS.includes(orm)) return null;
66
+ if (!ALL_FRAMEWORKS.includes(framework)) return null;
76
67
  return {
77
68
  database,
78
69
  strategy,
@@ -182,6 +173,36 @@ volumes:
182
173
  }
183
174
 
184
175
  // src/generators/init-sql.ts
176
+ function postgresAppRole(dbName) {
177
+ return `${dbName}_app`;
178
+ }
179
+ var POSTGRES_APP_PASSWORD = "dev_app_password";
180
+ function postgresAppRoleSql(dbName, strategy) {
181
+ const role = postgresAppRole(dbName);
182
+ let strategyGrant = "";
183
+ if (strategy === "schema") {
184
+ strategyGrant = `
185
+ -- schema-per-tenant: the app creates one schema per tenant
186
+ GRANT CREATE ON DATABASE ${dbName} TO ${role};
187
+ `;
188
+ } else if (strategy === "database") {
189
+ strategyGrant = `
190
+ -- database-per-tenant: the app creates one database per tenant
191
+ ALTER ROLE ${role} CREATEDB;
192
+ `;
193
+ }
194
+ return `
195
+ -- Application role. The app connects as ${role} (DATABASE_URL), never as the
196
+ -- bootstrap superuser (DATABASE_ADMIN_URL): a superuser or BYPASSRLS role
197
+ -- ignores every row-level security policy, FORCE included. Use the superuser
198
+ -- only for bootstrap and migrations.
199
+ CREATE ROLE ${role} WITH LOGIN PASSWORD '${POSTGRES_APP_PASSWORD}' NOSUPERUSER NOBYPASSRLS NOCREATEROLE;
200
+ GRANT CONNECT ON DATABASE ${dbName} TO ${role};
201
+ GRANT USAGE, CREATE ON SCHEMA public TO ${role};
202
+ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${role};
203
+ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO ${role};
204
+ ${strategyGrant}`;
205
+ }
185
206
  function generatePresetInitSql(projectName, preset) {
186
207
  const dbName = projectName.replace(/[^a-z0-9]/gi, "_").toLowerCase();
187
208
  switch (preset.database) {
@@ -201,10 +222,13 @@ function generatePostgresInit(projectName, dbName, strategy) {
201
222
  -- Add RLS policies to each tenant-scoped table:
202
223
  --
203
224
  -- ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;
225
+ -- ALTER TABLE your_table FORCE ROW LEVEL SECURITY;
204
226
  -- CREATE POLICY tenant_isolation ON your_table
205
- -- USING (tenant_id = current_setting('app.current_tenant')::uuid);
227
+ -- USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
206
228
  --
207
- -- The Stratum db-adapters package sets app.current_tenant automatically.
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",
@@ -283,7 +306,7 @@ model Tenant {
283
306
  content: `// Prisma client with Stratum tenant-scoped queries
284
307
  import { PrismaClient } from "@prisma/client";
285
308
  import { Pool } from "pg";
286
- import { withTenant } from "@stratum-hq/db-adapters";
309
+ import { prismaWithTenant } from "@stratum-hq/db-adapters";
287
310
 
288
311
  const prisma = new PrismaClient();
289
312
  const pool = new Pool({
@@ -293,7 +316,7 @@ const pool = new Pool({
293
316
  // Create a tenant-scoped Prisma client.
294
317
  // All queries through this client are automatically filtered by RLS.
295
318
  export function createTenantPrisma(getTenantId: () => string) {
296
- return withTenant(prisma, getTenantId, pool);
319
+ return prismaWithTenant(prisma, getTenantId, pool);
297
320
  }
298
321
 
299
322
  // Usage:
@@ -330,7 +353,7 @@ export const db = drizzle(connection);
330
353
  content: `// Drizzle ORM with Stratum tenant-scoped queries
331
354
  import { drizzle } from "drizzle-orm/node-postgres";
332
355
  import { Pool } from "pg";
333
- import { withTenantDrizzle } from "@stratum-hq/db-adapters";
356
+ import { drizzleWithTenant } from "@stratum-hq/db-adapters";
334
357
 
335
358
  const pool = new Pool({
336
359
  connectionString: process.env.DATABASE_URL,
@@ -341,12 +364,13 @@ export const db = drizzle(pool);
341
364
  // Create a tenant-scoped Drizzle instance.
342
365
  // Queries are automatically filtered by RLS policy.
343
366
  export function createTenantDb(getTenantId: () => string) {
344
- return withTenantDrizzle(db, getTenantId, pool);
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
  `
@@ -561,11 +585,13 @@ const port = Number(process.env.PORT) || 3000;
561
585
 
562
586
  app.use(express.json());
563
587
 
564
- // Tenant extraction middleware
588
+ // Tenant extraction middleware. The tenant comes from the subdomain the
589
+ // request was routed to. Do not take it from a client-supplied header such as
590
+ // x-tenant-id: any caller can set one and pick another tenant. Once you add
591
+ // authentication, check that the signed-in user belongs to this tenant, or
592
+ // derive the tenant from the verified session or JWT instead.
565
593
  app.use((req, _res, next) => {
566
- const tenantId =
567
- req.headers["x-tenant-id"] as string ||
568
- req.hostname.split(".")[0];
594
+ const tenantId = req.hostname.split(".")[0];
569
595
  (req as any).tenantId = tenantId;
570
596
  next();
571
597
  });
@@ -595,12 +621,14 @@ function generateFastifyMiddleware(projectName) {
595
621
  const fastify = Fastify({ logger: true });
596
622
  const port = Number(process.env.PORT) || 3000;
597
623
 
598
- // Tenant extraction plugin
624
+ // Tenant extraction hook. The tenant comes from the subdomain the
625
+ // request was routed to. Do not take it from a client-supplied header such as
626
+ // x-tenant-id: any caller can set one and pick another tenant. Once you add
627
+ // authentication, check that the signed-in user belongs to this tenant, or
628
+ // derive the tenant from the verified session or JWT instead.
599
629
  fastify.decorateRequest("tenantId", "");
600
630
  fastify.addHook("onRequest", async (request) => {
601
- const tenantId =
602
- (request.headers["x-tenant-id"] as string) ||
603
- (request.hostname?.split(".")[0] ?? "");
631
+ const tenantId = request.hostname?.split(".")[0] ?? "";
604
632
  (request as any).tenantId = tenantId;
605
633
  });
606
634
 
@@ -631,14 +659,15 @@ function generateNextjsMiddleware(projectName) {
631
659
  import { NextRequest, NextResponse } from "next/server";
632
660
 
633
661
  export function middleware(request: NextRequest) {
662
+ // The tenant comes from the subdomain the request was routed to. Any
663
+ // x-tenant-id the client sent is removed first, so server code that reads
664
+ // x-tenant-id only ever sees the value set here. Once you add
665
+ // authentication, check that the signed-in user belongs to this tenant.
634
666
  const hostname = request.headers.get("host") || "";
635
- const subdomain = hostname.split(".")[0];
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;
667
+ const tenantId = hostname.split(".")[0];
640
668
 
641
669
  const requestHeaders = new Headers(request.headers);
670
+ requestHeaders.delete("x-tenant-id");
642
671
  if (tenantId && tenantId !== "localhost" && tenantId !== "www") {
643
672
  requestHeaders.set("x-tenant-id", tenantId);
644
673
  }
@@ -661,7 +690,7 @@ export default function Home() {
661
690
  <p>Multi-tenant app powered by Stratum.</p>
662
691
  <ul>
663
692
  <li>Configure tenants via the Stratum control plane</li>
664
- <li>Access tenant context via <code>x-tenant-id</code> header</li>
693
+ <li>Tenant is resolved from the subdomain in <code>middleware.ts</code></li>
665
694
  <li>Use <code>@stratum-hq/lib</code> for tenant resolution</li>
666
695
  </ul>
667
696
  </main>
@@ -680,11 +709,13 @@ import { serve } from "@hono/node-server";
680
709
 
681
710
  const app = new Hono();
682
711
 
683
- // Tenant extraction middleware
712
+ // Tenant extraction middleware. The tenant comes from the subdomain the
713
+ // request was routed to. Do not take it from a client-supplied header such as
714
+ // x-tenant-id: any caller can set one and pick another tenant. Once you add
715
+ // authentication, check that the signed-in user belongs to this tenant, or
716
+ // derive the tenant from the verified session or JWT instead.
684
717
  app.use("*", async (c, next) => {
685
- const tenantId =
686
- c.req.header("x-tenant-id") ||
687
- new URL(c.req.url).hostname.split(".")[0];
718
+ const tenantId = new URL(c.req.url).hostname.split(".")[0];
688
719
  c.set("tenantId", tenantId);
689
720
  await next();
690
721
  });
@@ -763,9 +794,11 @@ export class AppController {
763
794
  export class TenantGuard implements CanActivate {
764
795
  canActivate(context: ExecutionContext): boolean {
765
796
  const request = context.switchToHttp().getRequest();
766
- const tenantId =
767
- request.headers["x-tenant-id"] ||
768
- request.hostname?.split(".")[0];
797
+ // The tenant comes from the subdomain the request was routed to. Do not
798
+ // take it from a client-supplied header such as x-tenant-id: any caller
799
+ // can set one and pick another tenant. Once you add authentication, check
800
+ // that the signed-in user belongs to this tenant here.
801
+ const tenantId = request.hostname?.split(".")[0];
769
802
  request.tenantId = tenantId || null;
770
803
  return true;
771
804
  }
@@ -994,12 +1027,9 @@ See the [Stratum docs](https://github.com/stratum-hq/Stratum) for full reference
994
1027
  `;
995
1028
  }
996
1029
  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";
1030
+ if (preset.framework === "nextjs") return "npm run dev";
1031
+ if (preset.framework === "nestjs") return "node --env-file=.env src/main.ts";
1032
+ if (preset.framework === "none") return "node --env-file=.env src/index.ts";
1003
1033
  return "node --env-file=.env src/index.ts";
1004
1034
  }
1005
1035
  function getDbStartInfo(preset) {
@@ -1074,9 +1104,14 @@ function generatePresetEnv(projectName, preset) {
1074
1104
  const dbName = projectName.replace(/[^a-z0-9]/gi, "_").toLowerCase();
1075
1105
  const jwtSecret = crypto.randomBytes(32).toString("base64url");
1076
1106
  let dbUrl;
1107
+ let adminUrlLine = "";
1077
1108
  switch (preset.database) {
1078
1109
  case "postgres":
1079
- dbUrl = `postgres://${dbName}:dev_password@localhost:5432/${dbName}`;
1110
+ dbUrl = `postgres://${postgresAppRole(dbName)}:${POSTGRES_APP_PASSWORD}@localhost:5432/${dbName}`;
1111
+ adminUrlLine = `
1112
+ # Superuser: bootstrap and migrations only. It bypasses row-level security.
1113
+ DATABASE_ADMIN_URL=postgres://${dbName}:dev_password@localhost:5432/${dbName}
1114
+ `;
1080
1115
  break;
1081
1116
  case "mongodb":
1082
1117
  dbUrl = `mongodb://${dbName}:dev_password@localhost:27017/${dbName}?authSource=admin`;
@@ -1091,7 +1126,7 @@ function generatePresetEnv(projectName, preset) {
1091
1126
 
1092
1127
  # Database
1093
1128
  ${urlKey}=${dbUrl}
1094
-
1129
+ ${adminUrlLine}
1095
1130
  # Authentication
1096
1131
  JWT_SECRET=${jwtSecret}
1097
1132
 
@@ -1322,7 +1357,7 @@ CREATE EXTENSION IF NOT EXISTS "ltree";
1322
1357
  -- The ltree extension enables hierarchical tenant trees
1323
1358
  -- uuid-ossp provides uuid_generate_v4() for tenant IDs
1324
1359
  COMMENT ON DATABASE ${dbName} IS 'Multi-tenant database for ${projectName}';
1325
- `;
1360
+ ${postgresAppRoleSql(dbName)}`;
1326
1361
  }
1327
1362
  function generateEnv(projectName) {
1328
1363
  const dbName = projectName.replace(/[^a-z0-9]/gi, "_").toLowerCase();
@@ -1330,8 +1365,12 @@ function generateEnv(projectName) {
1330
1365
  return `# Environment variables for ${projectName}
1331
1366
  # Copy to .env and fill in values
1332
1367
 
1333
- # Database
1334
- DATABASE_URL=postgres://${dbName}:dev_password@localhost:5432/${dbName}
1368
+ # Database. The app connects as the non-superuser role created in init.sql,
1369
+ # so row-level security applies to it.
1370
+ DATABASE_URL=postgres://${postgresAppRole(dbName)}:${POSTGRES_APP_PASSWORD}@localhost:5432/${dbName}
1371
+
1372
+ # Superuser: bootstrap and migrations only. It bypasses row-level security.
1373
+ DATABASE_ADMIN_URL=postgres://${dbName}:dev_password@localhost:5432/${dbName}
1335
1374
 
1336
1375
  # Authentication
1337
1376
  JWT_SECRET=${jwtSecret}
@@ -1419,7 +1458,7 @@ export default function Home() {
1419
1458
  <p>Multi-tenant app powered by Stratum.</p>
1420
1459
  <ul>
1421
1460
  <li>Configure tenants via the Stratum control plane</li>
1422
- <li>Access tenant context via <code>x-tenant-id</code> header</li>
1461
+ <li>Tenant is resolved from the subdomain in <code>middleware.ts</code></li>
1423
1462
  <li>Use <code>@stratum-hq/lib</code> for tenant resolution</li>
1424
1463
  </ul>
1425
1464
  </main>
@@ -1428,18 +1467,19 @@ export default function Home() {
1428
1467
  `;
1429
1468
  }
1430
1469
  function generateNextjsMiddleware2() {
1431
- return `// middleware.ts \u2014 tenant resolution via subdomain or header
1470
+ return `// middleware.ts \u2014 tenant resolution via subdomain
1432
1471
  import { NextRequest, NextResponse } from "next/server";
1433
1472
 
1434
1473
  export function middleware(request: NextRequest) {
1474
+ // The tenant comes from the subdomain the request was routed to. Any
1475
+ // x-tenant-id the client sent is removed first, so server code that reads
1476
+ // x-tenant-id only ever sees the value set here. Once you add
1477
+ // authentication, check that the signed-in user belongs to this tenant.
1435
1478
  const hostname = request.headers.get("host") || "";
1436
- const subdomain = hostname.split(".")[0];
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;
1479
+ const tenantId = hostname.split(".")[0];
1441
1480
 
1442
1481
  const requestHeaders = new Headers(request.headers);
1482
+ requestHeaders.delete("x-tenant-id");
1443
1483
  if (tenantId && tenantId !== "localhost" && tenantId !== "www") {
1444
1484
  requestHeaders.set("x-tenant-id", tenantId);
1445
1485
  }
@@ -1500,7 +1540,7 @@ ${projectName}/
1500
1540
 
1501
1541
  This project uses Stratum for hierarchical multi-tenancy:
1502
1542
 
1503
- - **Tenant resolution** \u2014 via JWT claim, subdomain, or \`x-tenant-id\` header
1543
+ - **Tenant resolution** \u2014 via subdomain (see \`middleware.ts\`); bind it to the signed-in user once you add authentication
1504
1544
  - **Config inheritance** \u2014 settings flow down the tenant tree with override support
1505
1545
  - **Permission ABAC** \u2014 role-based permissions with tenant-scoped enforcement
1506
1546
 
package/dist/matrix.js ADDED
@@ -0,0 +1,115 @@
1
+ // src/matrix.ts
2
+ var POSTGRES_STRATEGIES = ["rls", "schema", "database"];
3
+ var POSTGRES_ORMS = ["prisma", "drizzle", "sequelize", "knex", "pg"];
4
+ var MONGODB_STRATEGIES = ["database", "collection"];
5
+ var MONGODB_ORMS = ["mongoose"];
6
+ var MYSQL_STRATEGIES = ["database", "table-prefix"];
7
+ var MYSQL_ORMS = ["sequelize", "knex", "pg"];
8
+ var ALL_FRAMEWORKS = ["express", "fastify", "nextjs", "hono", "nestjs", "none"];
9
+ var VALID_COMBINATIONS = {
10
+ postgres: {
11
+ strategies: POSTGRES_STRATEGIES,
12
+ orms: POSTGRES_ORMS,
13
+ frameworks: ALL_FRAMEWORKS
14
+ },
15
+ mongodb: {
16
+ strategies: MONGODB_STRATEGIES,
17
+ orms: MONGODB_ORMS,
18
+ frameworks: ALL_FRAMEWORKS
19
+ },
20
+ mysql: {
21
+ strategies: MYSQL_STRATEGIES,
22
+ orms: MYSQL_ORMS,
23
+ frameworks: ALL_FRAMEWORKS
24
+ }
25
+ };
26
+ var ALL_DATABASES = ["postgres", "mongodb", "mysql"];
27
+ var ALL_STRATEGIES = ["rls", "schema", "database", "collection", "table-prefix"];
28
+ var ALL_ORMS = ["prisma", "drizzle", "sequelize", "knex", "mongoose", "pg"];
29
+ function isValidPreset(preset) {
30
+ const config = VALID_COMBINATIONS[preset.database];
31
+ if (!config) return false;
32
+ if (!config.strategies.includes(preset.strategy)) return false;
33
+ if (!config.orms.includes(preset.orm)) return false;
34
+ if (!config.frameworks.includes(preset.framework)) return false;
35
+ return true;
36
+ }
37
+ function parsePresetString(s) {
38
+ if (!s || typeof s !== "string") return null;
39
+ const parts = s.toLowerCase().split("-");
40
+ let database;
41
+ let strategy;
42
+ let orm;
43
+ let framework;
44
+ if (parts.length === 4) {
45
+ [database, strategy, orm, framework] = parts;
46
+ } else if (parts.length === 5) {
47
+ database = parts[0];
48
+ strategy = `${parts[1]}-${parts[2]}`;
49
+ orm = parts[3];
50
+ framework = parts[4];
51
+ } else {
52
+ return null;
53
+ }
54
+ if (!ALL_DATABASES.includes(database)) return null;
55
+ if (!ALL_STRATEGIES.includes(strategy)) return null;
56
+ if (!ALL_ORMS.includes(orm)) return null;
57
+ if (!ALL_FRAMEWORKS.includes(framework)) return null;
58
+ return {
59
+ database,
60
+ strategy,
61
+ orm,
62
+ framework
63
+ };
64
+ }
65
+ function formatPresetString(preset) {
66
+ return `${preset.database}-${preset.strategy}-${preset.orm}-${preset.framework}`;
67
+ }
68
+ function getValidOptions(partial) {
69
+ let databases = [...ALL_DATABASES];
70
+ let strategies = [];
71
+ let orms = [];
72
+ let frameworks = [];
73
+ if (partial.database) {
74
+ databases = [partial.database];
75
+ }
76
+ const strategySet = /* @__PURE__ */ new Set();
77
+ const ormSet = /* @__PURE__ */ new Set();
78
+ const frameworkSet = /* @__PURE__ */ new Set();
79
+ for (const db of databases) {
80
+ const config = VALID_COMBINATIONS[db];
81
+ if (partial.strategy && !config.strategies.includes(partial.strategy)) continue;
82
+ if (partial.orm && !config.orms.includes(partial.orm)) continue;
83
+ for (const s of config.strategies) strategySet.add(s);
84
+ for (const o of config.orms) ormSet.add(o);
85
+ for (const f of config.frameworks) frameworkSet.add(f);
86
+ }
87
+ strategies = [...strategySet];
88
+ orms = [...ormSet];
89
+ frameworks = [...frameworkSet];
90
+ if (partial.strategy) {
91
+ strategies = strategies.filter((s) => s === partial.strategy);
92
+ }
93
+ if (partial.orm) {
94
+ orms = orms.filter((o) => o === partial.orm);
95
+ }
96
+ if (partial.framework) {
97
+ frameworks = frameworks.filter((f) => f === partial.framework);
98
+ }
99
+ if (partial.strategy || partial.orm) {
100
+ databases = databases.filter((db) => {
101
+ const config = VALID_COMBINATIONS[db];
102
+ if (partial.strategy && !config.strategies.includes(partial.strategy)) return false;
103
+ if (partial.orm && !config.orms.includes(partial.orm)) return false;
104
+ return true;
105
+ });
106
+ }
107
+ return { databases, strategies, orms, frameworks };
108
+ }
109
+ export {
110
+ VALID_COMBINATIONS,
111
+ formatPresetString,
112
+ getValidOptions,
113
+ isValidPreset,
114
+ parsePresetString
115
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stratum-hq/create",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Create a new Stratum multi-tenancy project",
5
5
  "keywords": [
6
6
  "multi-tenancy",
@@ -20,19 +20,18 @@
20
20
  },
21
21
  "exports": {
22
22
  ".": "./dist/index.js",
23
- "./matrix": {
24
- "import": "./src/matrix.ts",
25
- "types": "./src/matrix.ts"
26
- }
23
+ "./matrix": "./dist/matrix.js"
27
24
  },
28
25
  "type": "module",
29
26
  "scripts": {
30
- "build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@stratum-hq/*",
27
+ "build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@stratum-hq/* && esbuild src/matrix.ts --bundle --platform=node --format=esm --outfile=dist/matrix.js --external:@stratum-hq/*",
28
+ "typecheck": "tsc --noEmit",
29
+ "lint": "eslint .",
31
30
  "test": "vitest run"
32
31
  },
33
32
  "devDependencies": {
34
- "esbuild": "^0.20.0",
35
- "vitest": "^1.6.0",
33
+ "esbuild": "^0.28.2",
34
+ "vitest": "^4.1.11",
36
35
  "typescript": "^5.3.0"
37
36
  },
38
37
  "publishConfig": {
@@ -40,6 +39,12 @@
40
39
  "registry": "https://registry.npmjs.org/"
41
40
  },
42
41
  "license": "MIT",
42
+ "author": "Christian Crank",
43
+ "homepage": "https://github.com/stratum-hq/Stratum/tree/main/packages/create#readme",
44
+ "bugs": "https://github.com/stratum-hq/Stratum/issues",
45
+ "engines": {
46
+ "node": ">=20.0.0"
47
+ },
43
48
  "repository": {
44
49
  "type": "git",
45
50
  "url": "https://github.com/stratum-hq/Stratum.git",
@@ -24,9 +24,6 @@ export function generateDbSetup(preset: StackPreset): DbSetupFile[] {
24
24
 
25
25
  function generatePrismaSetup(preset: StackPreset): DbSetupFile[] {
26
26
  const provider = preset.database === "mysql" ? "mysql" : "postgresql";
27
- const envUrl = preset.database === "mysql"
28
- ? "mysql://user:password@localhost:3306/dbname"
29
- : "postgres://user:password@localhost:5432/dbname";
30
27
 
31
28
  return [
32
29
  {
@@ -58,7 +55,7 @@ model Tenant {
58
55
  content: `// Prisma client with Stratum tenant-scoped queries
59
56
  import { PrismaClient } from "@prisma/client";
60
57
  import { Pool } from "pg";
61
- import { withTenant } from "@stratum-hq/db-adapters";
58
+ import { prismaWithTenant } from "@stratum-hq/db-adapters";
62
59
 
63
60
  const prisma = new PrismaClient();
64
61
  const pool = new Pool({
@@ -68,7 +65,7 @@ const pool = new Pool({
68
65
  // Create a tenant-scoped Prisma client.
69
66
  // All queries through this client are automatically filtered by RLS.
70
67
  export function createTenantPrisma(getTenantId: () => string) {
71
- return withTenant(prisma, getTenantId, pool);
68
+ return prismaWithTenant(prisma, getTenantId, pool);
72
69
  }
73
70
 
74
71
  // Usage:
@@ -107,7 +104,7 @@ export const db = drizzle(connection);
107
104
  content: `// Drizzle ORM with Stratum tenant-scoped queries
108
105
  import { drizzle } from "drizzle-orm/node-postgres";
109
106
  import { Pool } from "pg";
110
- import { withTenantDrizzle } from "@stratum-hq/db-adapters";
107
+ import { drizzleWithTenant } from "@stratum-hq/db-adapters";
111
108
 
112
109
  const pool = new Pool({
113
110
  connectionString: process.env.DATABASE_URL,
@@ -118,12 +115,13 @@ export const db = drizzle(pool);
118
115
  // Create a tenant-scoped Drizzle instance.
119
116
  // Queries are automatically filtered by RLS policy.
120
117
  export function createTenantDb(getTenantId: () => string) {
121
- return withTenantDrizzle(db, getTenantId, pool);
118
+ return drizzleWithTenant(db, getTenantId, pool);
122
119
  }
123
120
 
124
- // Usage:
121
+ // Usage: run tenant queries inside transaction() so the tenant context and
122
+ // the query share one connection.
125
123
  // const tenantDb = createTenantDb(() => currentTenantId);
126
- // const rows = await tenantDb.select().from(orders);
124
+ // const rows = await tenantDb.transaction((tx) => tx.select().from(orders));
127
125
 
128
126
  export { pool };
129
127
  `,