@stratum-hq/create 0.5.0 → 0.6.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 +22 -0
- package/README.md +31 -7
- package/dist/index.js +270 -191
- package/package.json +17 -3
- package/src/generators/db-setup.ts +43 -1
- package/src/generators/init-sql.ts +56 -11
- package/src/generators/middleware.ts +50 -18
- package/src/generators/package-json.ts +24 -13
- package/src/generators/readme.ts +11 -8
- package/src/generators/tsconfig.ts +12 -6
- package/src/index.ts +58 -102
- package/src/matrix.ts +1 -1
- package/src/preset-project.ts +10 -2
package/src/index.ts
CHANGED
|
@@ -2,12 +2,23 @@ import * as fs from "fs";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import { execSync } from "child_process";
|
|
5
|
-
import { parsePresetString, isValidPreset } from "./matrix.js";
|
|
5
|
+
import { parsePresetString, isValidPreset, type StackPreset } from "./matrix.js";
|
|
6
6
|
import { createPresetProject } from "./preset-project.js";
|
|
7
7
|
import { STRATUM_RANGES } from "./stratum-versions.js";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
postgresAppRole,
|
|
10
|
+
postgresAppRoleSql,
|
|
11
|
+
postgresStratumRole,
|
|
12
|
+
POSTGRES_APP_PASSWORD,
|
|
13
|
+
POSTGRES_STRATUM_PASSWORD,
|
|
14
|
+
} from "./generators/init-sql.js";
|
|
9
15
|
import { generateTsconfig } from "./generators/tsconfig.js";
|
|
10
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
expressServer,
|
|
18
|
+
fastifyServer,
|
|
19
|
+
nextjsRootLayout,
|
|
20
|
+
nextjsTenantMiddleware,
|
|
21
|
+
} from "./generators/middleware.js";
|
|
11
22
|
|
|
12
23
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
13
24
|
|
|
@@ -111,11 +122,11 @@ function writeFile(filePath: string, content: string): void {
|
|
|
111
122
|
function generatePackageJson(projectName: string, template: Template): string {
|
|
112
123
|
const frameworkDeps: Record<Template, Record<string, string>> = {
|
|
113
124
|
express: {
|
|
114
|
-
express: "^4.
|
|
125
|
+
express: "^4.22.3",
|
|
115
126
|
"@types/express": "^4.17.21",
|
|
116
127
|
},
|
|
117
128
|
fastify: {
|
|
118
|
-
fastify: "^
|
|
129
|
+
fastify: "^5.12.5",
|
|
119
130
|
},
|
|
120
131
|
nextjs: {
|
|
121
132
|
next: "^15.5.16",
|
|
@@ -123,14 +134,14 @@ function generatePackageJson(projectName: string, template: Template): string {
|
|
|
123
134
|
"react-dom": "^19.0.0",
|
|
124
135
|
"@types/react": "^19.0.0",
|
|
125
136
|
"@types/react-dom": "^19.0.0",
|
|
126
|
-
// The middleware verifies the tenant JWT with jose.
|
|
127
|
-
jose: "^6.2.12",
|
|
128
137
|
},
|
|
129
138
|
};
|
|
130
139
|
|
|
131
140
|
const deps = {
|
|
132
141
|
"@stratum-hq/lib": STRATUM_RANGES["@stratum-hq/lib"],
|
|
133
142
|
pg: "^8.11.0",
|
|
143
|
+
// Every template verifies the tenant JWT with jose.
|
|
144
|
+
jose: "^6.2.12",
|
|
134
145
|
...frameworkDeps[template],
|
|
135
146
|
};
|
|
136
147
|
|
|
@@ -149,7 +160,7 @@ function generatePackageJson(projectName: string, template: Template): string {
|
|
|
149
160
|
devDependencies: {
|
|
150
161
|
typescript: "^5.3.0",
|
|
151
162
|
"@types/node": "^20.11.0",
|
|
152
|
-
...(template === "nextjs" ? {} : { tsx: "^4.
|
|
163
|
+
...(template === "nextjs" ? {} : { tsx: "^4.19.3", "@types/pg": "^8.11.0" }),
|
|
153
164
|
},
|
|
154
165
|
engines: {
|
|
155
166
|
node: ">=20.0.0",
|
|
@@ -213,7 +224,10 @@ function generateEnv(projectName: string): string {
|
|
|
213
224
|
DATABASE_URL=postgres://${postgresAppRole(dbName)}:${POSTGRES_APP_PASSWORD}@localhost:5432/${dbName}
|
|
214
225
|
|
|
215
226
|
# Superuser: bootstrap and migrations only. It bypasses row-level security.
|
|
216
|
-
|
|
227
|
+
DATABASE_SUPERUSER_URL=postgres://${dbName}:dev_password@localhost:5432/${dbName}
|
|
228
|
+
|
|
229
|
+
# Stratum's own login: the library's adminPool, which runs the Stratum migrations (see init.sql).
|
|
230
|
+
STRATUM_ADMIN_DATABASE_URL=postgres://${postgresStratumRole(dbName)}:${POSTGRES_STRATUM_PASSWORD}@localhost:5432/${dbName}
|
|
217
231
|
|
|
218
232
|
# Authentication
|
|
219
233
|
JWT_SECRET=${jwtSecret}
|
|
@@ -226,77 +240,8 @@ NODE_ENV=development
|
|
|
226
240
|
`;
|
|
227
241
|
}
|
|
228
242
|
|
|
229
|
-
function generateExpressServer(projectName: string): string {
|
|
230
|
-
return `import express from "express";
|
|
231
|
-
import { Pool } from "pg";
|
|
232
|
-
|
|
233
|
-
const app = express();
|
|
234
|
-
const port = process.env.PORT || 3000;
|
|
235
|
-
|
|
236
|
-
const pool = new Pool({
|
|
237
|
-
connectionString: process.env.DATABASE_URL,
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
app.use(express.json());
|
|
241
|
-
|
|
242
|
-
app.get("/health", (_req, res) => {
|
|
243
|
-
res.json({ status: "ok", project: "${projectName}" });
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
app.get("/tenants", async (_req, res) => {
|
|
247
|
-
try {
|
|
248
|
-
// Example: query your tenant table
|
|
249
|
-
// const { rows } = await pool.query("SELECT * FROM tenants LIMIT 20");
|
|
250
|
-
// res.json(rows);
|
|
251
|
-
res.json({ message: "Replace this with your tenant queries", pool: !!pool });
|
|
252
|
-
} catch (err) {
|
|
253
|
-
res.status(500).json({ error: String(err) });
|
|
254
|
-
}
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
app.listen(port, () => {
|
|
258
|
-
console.log(\`${projectName} server running on http://localhost:\${port}\`);
|
|
259
|
-
});
|
|
260
|
-
`;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function generateFastifyServer(projectName: string): string {
|
|
264
|
-
return `import Fastify from "fastify";
|
|
265
|
-
import { Pool } from "pg";
|
|
266
|
-
|
|
267
|
-
const fastify = Fastify({ logger: true });
|
|
268
|
-
const port = Number(process.env.PORT) || 3000;
|
|
269
|
-
|
|
270
|
-
const pool = new Pool({
|
|
271
|
-
connectionString: process.env.DATABASE_URL,
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
fastify.get("/health", async () => {
|
|
275
|
-
return { status: "ok", project: "${projectName}" };
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
fastify.get("/tenants", async (_request, reply) => {
|
|
279
|
-
try {
|
|
280
|
-
// Example: query your tenant table
|
|
281
|
-
// const { rows } = await pool.query("SELECT * FROM tenants LIMIT 20");
|
|
282
|
-
// return rows;
|
|
283
|
-
return { message: "Replace this with your tenant queries", pool: !!pool };
|
|
284
|
-
} catch (err) {
|
|
285
|
-
return reply.status(500).send({ error: String(err) });
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
fastify.listen({ port, host: "0.0.0.0" }, (err) => {
|
|
290
|
-
if (err) {
|
|
291
|
-
fastify.log.error(err);
|
|
292
|
-
process.exit(1);
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
`;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
243
|
function generateNextjsPage(projectName: string): string {
|
|
299
|
-
return `// app/page.tsx
|
|
244
|
+
return `// app/page.tsx: ${projectName} root page
|
|
300
245
|
export default function Home() {
|
|
301
246
|
return (
|
|
302
247
|
<main style={{ padding: "2rem", fontFamily: "sans-serif" }}>
|
|
@@ -304,7 +249,7 @@ export default function Home() {
|
|
|
304
249
|
<p>Multi-tenant app powered by Stratum.</p>
|
|
305
250
|
<ul>
|
|
306
251
|
<li>Configure tenants via the Stratum control plane</li>
|
|
307
|
-
<li>The tenant comes from a verified JWT in <code>middleware.ts</code></li>
|
|
252
|
+
<li>The tenant comes from a verified JWT in <code>src/middleware.ts</code></li>
|
|
308
253
|
<li>Use <code>@stratum-hq/lib</code> for tenant resolution</li>
|
|
309
254
|
</ul>
|
|
310
255
|
</main>
|
|
@@ -330,7 +275,7 @@ docker compose up -d
|
|
|
330
275
|
|
|
331
276
|
\`\`\`bash
|
|
332
277
|
cp .env.example .env
|
|
333
|
-
# Edit .env
|
|
278
|
+
# Edit .env: update DATABASE_URL, JWT_SECRET, and STRATUM_API_KEY
|
|
334
279
|
\`\`\`
|
|
335
280
|
|
|
336
281
|
### 3. Install dependencies
|
|
@@ -360,9 +305,13 @@ ${template === "nextjs" ? "" : "├── tsconfig.json\n"}└── package.jso
|
|
|
360
305
|
|
|
361
306
|
This project uses Stratum for hierarchical multi-tenancy:
|
|
362
307
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
- **
|
|
308
|
+
${
|
|
309
|
+
template === "nextjs"
|
|
310
|
+
? "- **Tenant resolution**: from the `tenant_id` claim of a bearer token verified with `JWT_SECRET` (see `src/middleware.ts`); the subdomain is only a display slug"
|
|
311
|
+
: "- **Tenant resolution**: the tenant middleware in `src/index.ts` takes the tenant from the `tenant_id` claim of a bearer token verified with `JWT_SECRET` (HS256, using `jose`). A token that does not verify is rejected with 401, and `GET /tenants` answers 401 without a tenant. The hostname and headers such as `x-tenant-id` are never used"
|
|
312
|
+
}
|
|
313
|
+
- **Config inheritance**: settings flow down the tenant tree with override support
|
|
314
|
+
- **Permission ABAC**: role-based permissions with tenant-scoped enforcement
|
|
366
315
|
|
|
367
316
|
See the [Stratum docs](https://github.com/stratum-hq/Stratum) for full reference.
|
|
368
317
|
`;
|
|
@@ -395,14 +344,15 @@ export function createProject(
|
|
|
395
344
|
|
|
396
345
|
// Server starter file
|
|
397
346
|
if (template === "express") {
|
|
398
|
-
writeFile(path.join(targetDir, "src", "index.ts"),
|
|
347
|
+
writeFile(path.join(targetDir, "src", "index.ts"), expressServer(projectName));
|
|
399
348
|
writeFile(path.join(targetDir, "tsconfig.json"), generateTsconfig(template));
|
|
400
349
|
} else if (template === "fastify") {
|
|
401
|
-
writeFile(path.join(targetDir, "src", "index.ts"),
|
|
350
|
+
writeFile(path.join(targetDir, "src", "index.ts"), fastifyServer(projectName));
|
|
402
351
|
writeFile(path.join(targetDir, "tsconfig.json"), generateTsconfig(template));
|
|
403
352
|
} else if (template === "nextjs") {
|
|
353
|
+
writeFile(path.join(targetDir, "src", "app", "layout.tsx"), nextjsRootLayout(projectName));
|
|
404
354
|
writeFile(path.join(targetDir, "src", "app", "page.tsx"), generateNextjsPage(projectName));
|
|
405
|
-
writeFile(path.join(targetDir, "middleware.ts"), nextjsTenantMiddleware());
|
|
355
|
+
writeFile(path.join(targetDir, "src", "middleware.ts"), nextjsTenantMiddleware());
|
|
406
356
|
}
|
|
407
357
|
|
|
408
358
|
// README
|
|
@@ -453,6 +403,25 @@ export function main(argv: string[]): void {
|
|
|
453
403
|
process.exit(1);
|
|
454
404
|
}
|
|
455
405
|
|
|
406
|
+
// Check the preset before touching the file system, so an invalid preset
|
|
407
|
+
// neither leaves an empty directory nor removes one under --force.
|
|
408
|
+
let parsedPreset: StackPreset | null = null;
|
|
409
|
+
if (preset) {
|
|
410
|
+
parsedPreset = parsePresetString(preset);
|
|
411
|
+
if (!parsedPreset) {
|
|
412
|
+
console.error(`Error: Invalid preset string "${preset}".`);
|
|
413
|
+
console.error("Format: {database}-{strategy}-{orm}-{framework}");
|
|
414
|
+
console.error("Example: postgres-rls-prisma-express, mongodb-database-mongoose-hono");
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
|
417
|
+
if (!isValidPreset(parsedPreset)) {
|
|
418
|
+
console.error(`Error: Invalid preset combination "${preset}".`);
|
|
419
|
+
console.error(`${parsedPreset.database} does not support ${parsedPreset.strategy}/${parsedPreset.orm} together.`);
|
|
420
|
+
console.error("Run with --help to see valid combinations.");
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
456
425
|
const targetDir = path.resolve(process.cwd(), projectName);
|
|
457
426
|
|
|
458
427
|
if (fs.existsSync(targetDir) && !force) {
|
|
@@ -468,21 +437,8 @@ export function main(argv: string[]): void {
|
|
|
468
437
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
469
438
|
|
|
470
439
|
// Preset path: full stack generation
|
|
471
|
-
if (
|
|
472
|
-
|
|
473
|
-
if (!parsed) {
|
|
474
|
-
console.error(`Error: Invalid preset string "${preset}".`);
|
|
475
|
-
console.error("Format: {database}-{strategy}-{orm}-{framework}");
|
|
476
|
-
console.error("Example: postgres-rls-prisma-express, mongodb-database-mongoose-hono");
|
|
477
|
-
process.exit(1);
|
|
478
|
-
}
|
|
479
|
-
if (!isValidPreset(parsed)) {
|
|
480
|
-
console.error(`Error: Invalid preset combination "${preset}".`);
|
|
481
|
-
console.error(`${parsed.database} does not support ${parsed.strategy}/${parsed.orm} together.`);
|
|
482
|
-
console.error("Run with --help to see valid combinations.");
|
|
483
|
-
process.exit(1);
|
|
484
|
-
}
|
|
485
|
-
createPresetProject(projectName, parsed, targetDir, skipInstall);
|
|
440
|
+
if (parsedPreset) {
|
|
441
|
+
createPresetProject(projectName, parsedPreset, targetDir, skipInstall);
|
|
486
442
|
} else {
|
|
487
443
|
// Template path: existing behavior, untouched
|
|
488
444
|
createProject(projectName, template, targetDir, skipInstall);
|
package/src/matrix.ts
CHANGED
|
@@ -75,7 +75,7 @@ export function parsePresetString(s: string): StackPreset | null {
|
|
|
75
75
|
|
|
76
76
|
const parts = s.toLowerCase().split("-");
|
|
77
77
|
|
|
78
|
-
// Handle "table-prefix" which contains a hyphen
|
|
78
|
+
// Handle "table-prefix" which contains a hyphen; it will split into
|
|
79
79
|
// 5 parts: [db, strategy1, "table", "prefix", orm, framework] or similar.
|
|
80
80
|
// We need to reconstruct multi-word tokens.
|
|
81
81
|
// Format: {database}-{strategy}-{orm}-{framework}
|
package/src/preset-project.ts
CHANGED
|
@@ -4,7 +4,13 @@ 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 {
|
|
7
|
+
import {
|
|
8
|
+
generatePresetInitSql,
|
|
9
|
+
postgresAppRole,
|
|
10
|
+
postgresStratumRole,
|
|
11
|
+
POSTGRES_APP_PASSWORD,
|
|
12
|
+
POSTGRES_STRATUM_PASSWORD,
|
|
13
|
+
} from "./generators/init-sql.js";
|
|
8
14
|
import { generateDbSetup } from "./generators/db-setup.js";
|
|
9
15
|
import { generateMiddleware } from "./generators/middleware.js";
|
|
10
16
|
import { generatePresetPackageJson } from "./generators/package-json.js";
|
|
@@ -32,7 +38,9 @@ function generatePresetEnv(projectName: string, preset: StackPreset): string {
|
|
|
32
38
|
// row-level security applies to it. The superuser URL is for bootstrap
|
|
33
39
|
// and migrations only.
|
|
34
40
|
dbUrl = `postgres://${postgresAppRole(dbName)}:${POSTGRES_APP_PASSWORD}@localhost:5432/${dbName}`;
|
|
35
|
-
adminUrlLine = `\n# Superuser: bootstrap and migrations only. It bypasses row-level security.\
|
|
41
|
+
adminUrlLine = `\n# Superuser: bootstrap and migrations only. It bypasses row-level security.\nDATABASE_SUPERUSER_URL=postgres://${dbName}:dev_password@localhost:5432/${dbName}\n` +
|
|
42
|
+
`\n# Stratum's own login: the library's adminPool, which runs the Stratum migrations (see init.sql).\n` +
|
|
43
|
+
`STRATUM_ADMIN_DATABASE_URL=postgres://${postgresStratumRole(dbName)}:${POSTGRES_STRATUM_PASSWORD}@localhost:5432/${dbName}\n`;
|
|
36
44
|
break;
|
|
37
45
|
case "mongodb":
|
|
38
46
|
dbUrl = `mongodb://${dbName}:dev_password@localhost:27017/${dbName}?authSource=admin`;
|