@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/src/index.ts CHANGED
@@ -1,12 +1,12 @@
1
- #!/usr/bin/env node
2
-
3
1
  import * as fs from "fs";
4
2
  import * as path from "path";
5
3
  import crypto from "node:crypto";
6
4
  import { execSync } from "child_process";
7
- import { fileURLToPath } from "url";
8
- import { parsePresetString, isValidPreset, formatPresetString } from "./matrix.js";
5
+ import { parsePresetString, isValidPreset } from "./matrix.js";
9
6
  import { createPresetProject } from "./preset-project.js";
7
+ import { STRATUM_RANGES } from "./stratum-versions.js";
8
+ import { postgresAppRole, postgresAppRoleSql, POSTGRES_APP_PASSWORD } from "./generators/init-sql.js";
9
+ import { generateTsconfig } from "./generators/tsconfig.js";
10
10
 
11
11
  // ─── Types ────────────────────────────────────────────────────────────────────
12
12
 
@@ -126,7 +126,7 @@ function generatePackageJson(projectName: string, template: Template): string {
126
126
  };
127
127
 
128
128
  const deps = {
129
- "@stratum-hq/lib": "^0.2.0",
129
+ "@stratum-hq/lib": STRATUM_RANGES["@stratum-hq/lib"],
130
130
  pg: "^8.11.0",
131
131
  ...frameworkDeps[template],
132
132
  };
@@ -140,11 +140,13 @@ function generatePackageJson(projectName: string, template: Template): string {
140
140
  scripts:
141
141
  template === "nextjs"
142
142
  ? { dev: "next dev", build: "next build", start: "next start" }
143
- : { dev: "node --watch src/index.js", build: "tsc", start: "node dist/index.js" },
143
+ : // Node 20 cannot run a .ts file, so dev runs the source through tsx.
144
+ { dev: "tsx watch --env-file=.env src/index.ts", build: "tsc", start: "node dist/index.js" },
144
145
  dependencies: deps,
145
146
  devDependencies: {
146
147
  typescript: "^5.3.0",
147
148
  "@types/node": "^20.11.0",
149
+ ...(template === "nextjs" ? {} : { tsx: "^4.7.0", "@types/pg": "^8.11.0" }),
148
150
  },
149
151
  engines: {
150
152
  node: ">=20.0.0",
@@ -194,7 +196,7 @@ CREATE EXTENSION IF NOT EXISTS "ltree";
194
196
  -- The ltree extension enables hierarchical tenant trees
195
197
  -- uuid-ossp provides uuid_generate_v4() for tenant IDs
196
198
  COMMENT ON DATABASE ${dbName} IS 'Multi-tenant database for ${projectName}';
197
- `;
199
+ ${postgresAppRoleSql(dbName)}`;
198
200
  }
199
201
 
200
202
  function generateEnv(projectName: string): string {
@@ -203,8 +205,12 @@ function generateEnv(projectName: string): string {
203
205
  return `# Environment variables for ${projectName}
204
206
  # Copy to .env and fill in values
205
207
 
206
- # Database
207
- DATABASE_URL=postgres://${dbName}:dev_password@localhost:5432/${dbName}
208
+ # Database. The app connects as the non-superuser role created in init.sql,
209
+ # so row-level security applies to it.
210
+ DATABASE_URL=postgres://${postgresAppRole(dbName)}:${POSTGRES_APP_PASSWORD}@localhost:5432/${dbName}
211
+
212
+ # Superuser: bootstrap and migrations only. It bypasses row-level security.
213
+ DATABASE_ADMIN_URL=postgres://${dbName}:dev_password@localhost:5432/${dbName}
208
214
 
209
215
  # Authentication
210
216
  JWT_SECRET=${jwtSecret}
@@ -295,7 +301,7 @@ export default function Home() {
295
301
  <p>Multi-tenant app powered by Stratum.</p>
296
302
  <ul>
297
303
  <li>Configure tenants via the Stratum control plane</li>
298
- <li>Access tenant context via <code>x-tenant-id</code> header</li>
304
+ <li>Tenant is resolved from the subdomain in <code>middleware.ts</code></li>
299
305
  <li>Use <code>@stratum-hq/lib</code> for tenant resolution</li>
300
306
  </ul>
301
307
  </main>
@@ -305,18 +311,19 @@ export default function Home() {
305
311
  }
306
312
 
307
313
  function generateNextjsMiddleware(): string {
308
- return `// middleware.ts — tenant resolution via subdomain or header
314
+ return `// middleware.ts — tenant resolution via subdomain
309
315
  import { NextRequest, NextResponse } from "next/server";
310
316
 
311
317
  export function middleware(request: NextRequest) {
318
+ // The tenant comes from the subdomain the request was routed to. Any
319
+ // x-tenant-id the client sent is removed first, so server code that reads
320
+ // x-tenant-id only ever sees the value set here. Once you add
321
+ // authentication, check that the signed-in user belongs to this tenant.
312
322
  const hostname = request.headers.get("host") || "";
313
- const subdomain = hostname.split(".")[0];
314
- const headerTenantId = request.headers.get("x-tenant-id");
315
- const pathTenantId = request.nextUrl.pathname.match(/^\\/tenant\\/([^/]+)/)?.[1];
316
-
317
- const tenantId = headerTenantId || pathTenantId || subdomain;
323
+ const tenantId = hostname.split(".")[0];
318
324
 
319
325
  const requestHeaders = new Headers(request.headers);
326
+ requestHeaders.delete("x-tenant-id");
320
327
  if (tenantId && tenantId !== "localhost" && tenantId !== "www") {
321
328
  requestHeaders.set("x-tenant-id", tenantId);
322
329
  }
@@ -331,9 +338,6 @@ export const config = {
331
338
  }
332
339
 
333
340
  function generateReadme(projectName: string, template: Template): string {
334
- const devCmd =
335
- template === "nextjs" ? "npm run dev" : "node --env-file=.env src/index.ts";
336
-
337
341
  return `# ${projectName}
338
342
 
339
343
  A multi-tenant application built with [Stratum](https://github.com/stratum-hq/Stratum).
@@ -362,7 +366,7 @@ npm install
362
366
  ### 4. Run the app
363
367
 
364
368
  \`\`\`bash
365
- ${devCmd}
369
+ npm run dev
366
370
  \`\`\`
367
371
 
368
372
  ## Project structure
@@ -373,14 +377,14 @@ ${projectName}/
373
377
  ├── docker-compose.yml
374
378
  ├── init.sql # DB extensions (uuid-ossp, ltree)
375
379
  ├── .env.example
376
- └── package.json
380
+ ${template === "nextjs" ? "" : "├── tsconfig.json\n"}└── package.json
377
381
  \`\`\`
378
382
 
379
383
  ## Multi-tenancy
380
384
 
381
385
  This project uses Stratum for hierarchical multi-tenancy:
382
386
 
383
- - **Tenant resolution** — via JWT claim, subdomain, or \`x-tenant-id\` header
387
+ - **Tenant resolution** — via subdomain (see \`middleware.ts\`); bind it to the signed-in user once you add authentication
384
388
  - **Config inheritance** — settings flow down the tenant tree with override support
385
389
  - **Permission ABAC** — role-based permissions with tenant-scoped enforcement
386
390
 
@@ -416,8 +420,10 @@ export function createProject(
416
420
  // Server starter file
417
421
  if (template === "express") {
418
422
  writeFile(path.join(targetDir, "src", "index.ts"), generateExpressServer(projectName));
423
+ writeFile(path.join(targetDir, "tsconfig.json"), generateTsconfig(template));
419
424
  } else if (template === "fastify") {
420
425
  writeFile(path.join(targetDir, "src", "index.ts"), generateFastifyServer(projectName));
426
+ writeFile(path.join(targetDir, "tsconfig.json"), generateTsconfig(template));
421
427
  } else if (template === "nextjs") {
422
428
  writeFile(path.join(targetDir, "src", "app", "page.tsx"), generateNextjsPage(projectName));
423
429
  writeFile(path.join(targetDir, "middleware.ts"), generateNextjsMiddleware());
@@ -514,25 +520,6 @@ export function main(argv: string[]): void {
514
520
  if (skipInstall) {
515
521
  console.log(" npm install");
516
522
  }
517
- if (preset) {
518
- const parsed = parsePresetString(preset)!;
519
- if (parsed.framework === "nextjs") {
520
- console.log(" npm run dev");
521
- } else if (parsed.framework === "nestjs") {
522
- console.log(" node --env-file=.env src/main.ts");
523
- } else {
524
- console.log(" node --env-file=.env src/index.ts");
525
- }
526
- } else if (template === "nextjs") {
527
- console.log(" npm run dev");
528
- } else {
529
- console.log(" node --env-file=.env src/index.ts");
530
- }
523
+ console.log(" npm run dev");
531
524
  console.log("");
532
525
  }
533
-
534
- // Run when executed directly (not when imported by tests)
535
- const __filename = fileURLToPath(import.meta.url);
536
- if (process.argv[1] === __filename) {
537
- main(process.argv.slice(2));
538
- }
@@ -4,11 +4,12 @@ import crypto from "node:crypto";
4
4
  import { execSync } from "child_process";
5
5
  import type { StackPreset } from "./matrix.js";
6
6
  import { generatePresetDockerCompose } from "./generators/docker-compose.js";
7
- import { generatePresetInitSql } from "./generators/init-sql.js";
7
+ import { generatePresetInitSql, postgresAppRole, POSTGRES_APP_PASSWORD } from "./generators/init-sql.js";
8
8
  import { generateDbSetup } from "./generators/db-setup.js";
9
9
  import { generateMiddleware } from "./generators/middleware.js";
10
10
  import { generatePresetPackageJson } from "./generators/package-json.js";
11
11
  import { generatePresetReadme } from "./generators/readme.js";
12
+ import { generateTsconfig, rootSources } from "./generators/tsconfig.js";
12
13
 
13
14
  function writeFile(filePath: string, content: string): void {
14
15
  const dir = path.dirname(filePath);
@@ -24,9 +25,14 @@ function generatePresetEnv(projectName: string, preset: StackPreset): string {
24
25
  const jwtSecret = crypto.randomBytes(32).toString("base64url");
25
26
 
26
27
  let dbUrl: string;
28
+ let adminUrlLine = "";
27
29
  switch (preset.database) {
28
30
  case "postgres":
29
- dbUrl = `postgres://${dbName}:dev_password@localhost:5432/${dbName}`;
31
+ // The app connects as the non-superuser role created in init.sql, so
32
+ // row-level security applies to it. The superuser URL is for bootstrap
33
+ // and migrations only.
34
+ dbUrl = `postgres://${postgresAppRole(dbName)}:${POSTGRES_APP_PASSWORD}@localhost:5432/${dbName}`;
35
+ adminUrlLine = `\n# Superuser: bootstrap and migrations only. It bypasses row-level security.\nDATABASE_ADMIN_URL=postgres://${dbName}:dev_password@localhost:5432/${dbName}\n`;
30
36
  break;
31
37
  case "mongodb":
32
38
  dbUrl = `mongodb://${dbName}:dev_password@localhost:27017/${dbName}?authSource=admin`;
@@ -43,7 +49,7 @@ function generatePresetEnv(projectName: string, preset: StackPreset): string {
43
49
 
44
50
  # Database
45
51
  ${urlKey}=${dbUrl}
46
-
52
+ ${adminUrlLine}
47
53
  # Authentication
48
54
  JWT_SECRET=${jwtSecret}
49
55
 
@@ -109,31 +115,7 @@ export function createPresetProject(
109
115
  // tsconfig.json
110
116
  writeFile(
111
117
  path.join(targetDir, "tsconfig.json"),
112
- JSON.stringify(
113
- {
114
- compilerOptions: {
115
- target: "ESNext",
116
- module: "NodeNext",
117
- moduleResolution: "NodeNext",
118
- strict: true,
119
- outDir: "dist",
120
- rootDir: "src",
121
- declaration: true,
122
- skipLibCheck: true,
123
- esModuleInterop: true,
124
- ...(preset.framework === "nestjs"
125
- ? { experimentalDecorators: true, emitDecoratorMetadata: true }
126
- : {}),
127
- ...(preset.framework === "nextjs"
128
- ? { jsx: "preserve", plugins: [{ name: "next" }] }
129
- : {}),
130
- },
131
- include: ["src"],
132
- exclude: ["node_modules", "dist"],
133
- },
134
- null,
135
- 2,
136
- ),
118
+ generateTsconfig(preset.framework, rootSources(preset)),
137
119
  );
138
120
 
139
121
  // npm install
@@ -0,0 +1,20 @@
1
+ // esbuild inlines these files when it bundles the CLI, so the published package carries
2
+ // the versions of the workspace it was built from. The ranges are fixed at create's build
3
+ // time. When a pinned package gets a major bump, or a minor bump while at 0.x, release
4
+ // create again, or new projects keep the old range.
5
+ import hono from "../../hono/package.json" with { type: "json" };
6
+ import dbAdapters from "../../db-adapters/package.json" with { type: "json" };
7
+ import lib from "../../lib/package.json" with { type: "json" };
8
+ import mongodb from "../../mongodb/package.json" with { type: "json" };
9
+ import mysql from "../../mysql/package.json" with { type: "json" };
10
+ import nestjs from "../../nestjs/package.json" with { type: "json" };
11
+
12
+ /** Caret ranges on the current workspace versions of the packages a generated project uses. */
13
+ export const STRATUM_RANGES = {
14
+ "@stratum-hq/db-adapters": `^${dbAdapters.version}`,
15
+ "@stratum-hq/hono": `^${hono.version}`,
16
+ "@stratum-hq/lib": `^${lib.version}`,
17
+ "@stratum-hq/mongodb": `^${mongodb.version}`,
18
+ "@stratum-hq/mysql": `^${mysql.version}`,
19
+ "@stratum-hq/nestjs": `^${nestjs.version}`,
20
+ } as const;
package/turbo.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://turbo.build/schema.json",
3
+ "extends": ["//"],
4
+ "tasks": {
5
+ "build": {
6
+ "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/packages/*/package.json"]
7
+ },
8
+ "typecheck": {
9
+ "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/packages/*/package.json"]
10
+ },
11
+ "test": {
12
+ "inputs": [
13
+ "$TURBO_DEFAULT$",
14
+ "$TURBO_ROOT$/packages/*/package.json",
15
+ "$TURBO_ROOT$/packages/{core,db-adapters,mysql,mongodb}/src/**"
16
+ ]
17
+ }
18
+ }
19
+ }