@stratum-hq/create 0.2.3 → 0.3.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.
@@ -0,0 +1,267 @@
1
+ import type { StackPreset } from "../matrix.js";
2
+
3
+ export interface MiddlewareFile {
4
+ filename: string;
5
+ content: string;
6
+ }
7
+
8
+ export function generateMiddleware(projectName: string, preset: StackPreset): MiddlewareFile[] {
9
+ switch (preset.framework) {
10
+ case "express":
11
+ return generateExpressMiddleware(projectName);
12
+ case "fastify":
13
+ return generateFastifyMiddleware(projectName);
14
+ case "nextjs":
15
+ return generateNextjsMiddleware(projectName);
16
+ case "hono":
17
+ return generateHonoMiddleware(projectName);
18
+ case "nestjs":
19
+ return generateNestjsMiddleware(projectName);
20
+ case "none":
21
+ return generateNoFramework(projectName);
22
+ }
23
+ }
24
+
25
+ function generateExpressMiddleware(projectName: string): MiddlewareFile[] {
26
+ return [
27
+ {
28
+ filename: "src/index.ts",
29
+ content: `import express from "express";
30
+
31
+ const app = express();
32
+ const port = Number(process.env.PORT) || 3000;
33
+
34
+ app.use(express.json());
35
+
36
+ // Tenant extraction middleware
37
+ app.use((req, _res, next) => {
38
+ const tenantId =
39
+ req.headers["x-tenant-id"] as string ||
40
+ req.hostname.split(".")[0];
41
+ (req as any).tenantId = tenantId;
42
+ next();
43
+ });
44
+
45
+ app.get("/health", (_req, res) => {
46
+ res.json({ status: "ok", project: "${projectName}" });
47
+ });
48
+
49
+ app.get("/tenants", async (req, res) => {
50
+ const tenantId = (req as any).tenantId;
51
+ res.json({ tenantId, message: "Replace with your tenant queries" });
52
+ });
53
+
54
+ app.listen(port, () => {
55
+ console.log(\`${projectName} running on http://localhost:\${port}\`);
56
+ });
57
+ `,
58
+ },
59
+ ];
60
+ }
61
+
62
+ function generateFastifyMiddleware(projectName: string): MiddlewareFile[] {
63
+ return [
64
+ {
65
+ filename: "src/index.ts",
66
+ content: `import Fastify from "fastify";
67
+
68
+ const fastify = Fastify({ logger: true });
69
+ const port = Number(process.env.PORT) || 3000;
70
+
71
+ // Tenant extraction plugin
72
+ fastify.decorateRequest("tenantId", "");
73
+ fastify.addHook("onRequest", async (request) => {
74
+ const tenantId =
75
+ (request.headers["x-tenant-id"] as string) ||
76
+ (request.hostname?.split(".")[0] ?? "");
77
+ (request as any).tenantId = tenantId;
78
+ });
79
+
80
+ fastify.get("/health", async () => {
81
+ return { status: "ok", project: "${projectName}" };
82
+ });
83
+
84
+ fastify.get("/tenants", async (request) => {
85
+ const tenantId = (request as any).tenantId;
86
+ return { tenantId, message: "Replace with your tenant queries" };
87
+ });
88
+
89
+ fastify.listen({ port, host: "0.0.0.0" }, (err) => {
90
+ if (err) {
91
+ fastify.log.error(err);
92
+ process.exit(1);
93
+ }
94
+ });
95
+ `,
96
+ },
97
+ ];
98
+ }
99
+
100
+ function generateNextjsMiddleware(projectName: string): MiddlewareFile[] {
101
+ return [
102
+ {
103
+ filename: "middleware.ts",
104
+ content: `// Next.js edge middleware for tenant resolution
105
+ import { NextRequest, NextResponse } from "next/server";
106
+
107
+ export function middleware(request: NextRequest) {
108
+ const hostname = request.headers.get("host") || "";
109
+ const subdomain = hostname.split(".")[0];
110
+ const headerTenantId = request.headers.get("x-tenant-id");
111
+ const pathTenantId = request.nextUrl.pathname.match(/^\\/tenant\\/([^/]+)/)?.[1];
112
+
113
+ const tenantId = headerTenantId || pathTenantId || subdomain;
114
+
115
+ const requestHeaders = new Headers(request.headers);
116
+ if (tenantId && tenantId !== "localhost" && tenantId !== "www") {
117
+ requestHeaders.set("x-tenant-id", tenantId);
118
+ }
119
+
120
+ return NextResponse.next({ request: { headers: requestHeaders } });
121
+ }
122
+
123
+ export const config = {
124
+ matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
125
+ };
126
+ `,
127
+ },
128
+ {
129
+ filename: "src/app/page.tsx",
130
+ content: `// app/page.tsx - ${projectName} root page
131
+ export default function Home() {
132
+ return (
133
+ <main style={{ padding: "2rem", fontFamily: "sans-serif" }}>
134
+ <h1>${projectName}</h1>
135
+ <p>Multi-tenant app powered by Stratum.</p>
136
+ <ul>
137
+ <li>Configure tenants via the Stratum control plane</li>
138
+ <li>Access tenant context via <code>x-tenant-id</code> header</li>
139
+ <li>Use <code>@stratum-hq/lib</code> for tenant resolution</li>
140
+ </ul>
141
+ </main>
142
+ );
143
+ }
144
+ `,
145
+ },
146
+ ];
147
+ }
148
+
149
+ function generateHonoMiddleware(projectName: string): MiddlewareFile[] {
150
+ return [
151
+ {
152
+ filename: "src/index.ts",
153
+ content: `import { Hono } from "hono";
154
+ import { serve } from "@hono/node-server";
155
+
156
+ const app = new Hono();
157
+
158
+ // Tenant extraction middleware
159
+ app.use("*", async (c, next) => {
160
+ const tenantId =
161
+ c.req.header("x-tenant-id") ||
162
+ new URL(c.req.url).hostname.split(".")[0];
163
+ c.set("tenantId", tenantId);
164
+ await next();
165
+ });
166
+
167
+ app.get("/health", (c) => {
168
+ return c.json({ status: "ok", project: "${projectName}" });
169
+ });
170
+
171
+ app.get("/tenants", (c) => {
172
+ const tenantId = c.get("tenantId");
173
+ return c.json({ tenantId, message: "Replace with your tenant queries" });
174
+ });
175
+
176
+ const port = Number(process.env.PORT) || 3000;
177
+ console.log(\`${projectName} running on http://localhost:\${port}\`);
178
+ serve({ fetch: app.fetch, port });
179
+ `,
180
+ },
181
+ ];
182
+ }
183
+
184
+ function generateNestjsMiddleware(projectName: string): MiddlewareFile[] {
185
+ return [
186
+ {
187
+ filename: "src/main.ts",
188
+ content: `import { NestFactory } from "@nestjs/core";
189
+ import { AppModule } from "./app.module.js";
190
+
191
+ async function bootstrap() {
192
+ const app = await NestFactory.create(AppModule);
193
+ const port = Number(process.env.PORT) || 3000;
194
+ await app.listen(port);
195
+ console.log(\`${projectName} running on http://localhost:\${port}\`);
196
+ }
197
+ bootstrap();
198
+ `,
199
+ },
200
+ {
201
+ filename: "src/app.module.ts",
202
+ content: `import { Module } from "@nestjs/common";
203
+ import { AppController } from "./app.controller.js";
204
+ import { TenantGuard } from "./tenant.guard.js";
205
+ import { APP_GUARD } from "@nestjs/core";
206
+
207
+ @Module({
208
+ controllers: [AppController],
209
+ providers: [
210
+ { provide: APP_GUARD, useClass: TenantGuard },
211
+ ],
212
+ })
213
+ export class AppModule {}
214
+ `,
215
+ },
216
+ {
217
+ filename: "src/app.controller.ts",
218
+ content: `import { Controller, Get, Req } from "@nestjs/common";
219
+
220
+ @Controller()
221
+ export class AppController {
222
+ @Get("health")
223
+ health() {
224
+ return { status: "ok", project: "${projectName}" };
225
+ }
226
+
227
+ @Get("tenants")
228
+ tenants(@Req() req: any) {
229
+ return { tenantId: req.tenantId, message: "Replace with your tenant queries" };
230
+ }
231
+ }
232
+ `,
233
+ },
234
+ {
235
+ filename: "src/tenant.guard.ts",
236
+ content: `import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
237
+
238
+ @Injectable()
239
+ export class TenantGuard implements CanActivate {
240
+ canActivate(context: ExecutionContext): boolean {
241
+ const request = context.switchToHttp().getRequest();
242
+ const tenantId =
243
+ request.headers["x-tenant-id"] ||
244
+ request.hostname?.split(".")[0];
245
+ request.tenantId = tenantId || null;
246
+ return true;
247
+ }
248
+ }
249
+ `,
250
+ },
251
+ ];
252
+ }
253
+
254
+ function generateNoFramework(projectName: string): MiddlewareFile[] {
255
+ return [
256
+ {
257
+ filename: "src/index.ts",
258
+ content: `// ${projectName} - Stratum multi-tenant setup (no framework)
259
+ // Use this as a starting point and integrate with your framework of choice.
260
+
261
+ console.log("${projectName} initialized");
262
+ console.log("Import your database setup from ./stratum-db or equivalent");
263
+ console.log("Add tenant resolution logic for your use case");
264
+ `,
265
+ },
266
+ ];
267
+ }
@@ -0,0 +1,174 @@
1
+ import type { StackPreset } from "../matrix.js";
2
+
3
+ export function generatePresetPackageJson(projectName: string, preset: StackPreset): string {
4
+ const deps: Record<string, string> = {
5
+ "@stratum-hq/lib": "^0.2.0",
6
+ };
7
+ const devDeps: Record<string, string> = {
8
+ typescript: "^5.3.0",
9
+ "@types/node": "^20.11.0",
10
+ };
11
+
12
+ // Database driver deps
13
+ addDatabaseDeps(deps, preset);
14
+
15
+ // ORM deps
16
+ addOrmDeps(deps, devDeps, preset);
17
+
18
+ // Framework deps
19
+ addFrameworkDeps(deps, devDeps, preset);
20
+
21
+ // Stratum adapter deps
22
+ addStratumDeps(deps, preset);
23
+
24
+ const scripts = getScripts(preset);
25
+
26
+ return JSON.stringify(
27
+ {
28
+ name: projectName,
29
+ version: "0.1.0",
30
+ private: true,
31
+ type: "module",
32
+ scripts,
33
+ dependencies: sortKeys(deps),
34
+ devDependencies: sortKeys(devDeps),
35
+ engines: {
36
+ node: ">=20.0.0",
37
+ },
38
+ },
39
+ null,
40
+ 2,
41
+ );
42
+ }
43
+
44
+ function addDatabaseDeps(deps: Record<string, string>, preset: StackPreset): void {
45
+ switch (preset.database) {
46
+ case "postgres":
47
+ if (preset.orm !== "prisma" && preset.orm !== "drizzle" && preset.orm !== "sequelize") {
48
+ deps["pg"] = "^8.11.0";
49
+ }
50
+ // prisma/drizzle/sequelize bring their own pg driver
51
+ if (preset.orm === "drizzle") {
52
+ deps["pg"] = "^8.11.0";
53
+ }
54
+ if (preset.orm === "pg") {
55
+ deps["pg"] = "^8.11.0";
56
+ }
57
+ if (preset.orm === "knex") {
58
+ deps["pg"] = "^8.11.0";
59
+ }
60
+ break;
61
+ case "mongodb":
62
+ // mongoose handles the driver
63
+ break;
64
+ case "mysql":
65
+ if (preset.orm === "drizzle" || preset.orm === "pg") {
66
+ deps["mysql2"] = "^3.9.0";
67
+ }
68
+ break;
69
+ }
70
+ }
71
+
72
+ function addOrmDeps(deps: Record<string, string>, devDeps: Record<string, string>, preset: StackPreset): void {
73
+ switch (preset.orm) {
74
+ case "prisma":
75
+ deps["@prisma/client"] = "^5.10.0";
76
+ devDeps["prisma"] = "^5.10.0";
77
+ if (preset.database === "postgres") {
78
+ deps["pg"] = "^8.11.0";
79
+ }
80
+ break;
81
+ case "drizzle":
82
+ deps["drizzle-orm"] = "^0.30.0";
83
+ devDeps["drizzle-kit"] = "^0.21.0";
84
+ if (preset.database === "postgres") {
85
+ deps["pg"] = "^8.11.0";
86
+ }
87
+ break;
88
+ case "sequelize":
89
+ deps["sequelize"] = "^6.37.0";
90
+ if (preset.database === "mysql") {
91
+ deps["mysql2"] = "^3.9.0";
92
+ } else {
93
+ deps["pg"] = "^8.11.0";
94
+ }
95
+ break;
96
+ case "knex":
97
+ deps["knex"] = "^3.1.0";
98
+ break;
99
+ case "mongoose":
100
+ deps["mongoose"] = "^8.2.0";
101
+ break;
102
+ case "pg":
103
+ // pg already handled in addDatabaseDeps
104
+ break;
105
+ }
106
+ }
107
+
108
+ function addFrameworkDeps(deps: Record<string, string>, devDeps: Record<string, string>, preset: StackPreset): void {
109
+ switch (preset.framework) {
110
+ case "express":
111
+ deps["express"] = "^4.18.0";
112
+ devDeps["@types/express"] = "^4.17.21";
113
+ break;
114
+ case "fastify":
115
+ deps["fastify"] = "^4.26.0";
116
+ break;
117
+ case "nextjs":
118
+ deps["next"] = "^15.5.16";
119
+ deps["react"] = "^19.0.0";
120
+ deps["react-dom"] = "^19.0.0";
121
+ devDeps["@types/react"] = "^19.0.0";
122
+ devDeps["@types/react-dom"] = "^19.0.0";
123
+ break;
124
+ case "hono":
125
+ deps["hono"] = "^4.1.0";
126
+ deps["@hono/node-server"] = "^1.8.0";
127
+ break;
128
+ case "nestjs":
129
+ deps["@nestjs/core"] = "^11.0.0";
130
+ deps["@nestjs/common"] = "^11.0.0";
131
+ deps["@nestjs/platform-express"] = "^11.0.0";
132
+ deps["reflect-metadata"] = "^0.2.0";
133
+ deps["rxjs"] = "^7.8.0";
134
+ break;
135
+ case "none":
136
+ break;
137
+ }
138
+ }
139
+
140
+ function addStratumDeps(deps: Record<string, string>, preset: StackPreset): void {
141
+ if (preset.database === "postgres" && preset.orm !== "mongoose") {
142
+ deps["@stratum-hq/db-adapters"] = "^0.2.0";
143
+ }
144
+ if (preset.database === "mongodb") {
145
+ deps["@stratum-hq/mongodb"] = "^0.2.0";
146
+ }
147
+ if (preset.database === "mysql") {
148
+ deps["@stratum-hq/mysql"] = "^0.2.0";
149
+ }
150
+ if (preset.framework === "hono") {
151
+ deps["@stratum-hq/hono"] = "^0.2.0";
152
+ }
153
+ if (preset.framework === "nestjs") {
154
+ deps["@stratum-hq/nestjs"] = "^0.2.0";
155
+ }
156
+ }
157
+
158
+ function getScripts(preset: StackPreset): Record<string, string> {
159
+ if (preset.framework === "nextjs") {
160
+ return { dev: "next dev", build: "next build", start: "next start" };
161
+ }
162
+ if (preset.framework === "nestjs") {
163
+ return { dev: "node --watch src/main.ts", build: "tsc", start: "node dist/main.js" };
164
+ }
165
+ return { dev: "node --watch src/index.js", build: "tsc", start: "node dist/index.js" };
166
+ }
167
+
168
+ function sortKeys(obj: Record<string, string>): Record<string, string> {
169
+ const sorted: Record<string, string> = {};
170
+ for (const key of Object.keys(obj).sort()) {
171
+ sorted[key] = obj[key];
172
+ }
173
+ return sorted;
174
+ }
@@ -0,0 +1,119 @@
1
+ import type { StackPreset } from "../matrix.js";
2
+
3
+ export function generatePresetReadme(projectName: string, preset: StackPreset): string {
4
+ const devCmd = getDevCommand(preset);
5
+ const dbStartCmd = getDbStartInfo(preset);
6
+ const dbSetupNote = getDbSetupNote(preset);
7
+
8
+ return `# ${projectName}
9
+
10
+ A multi-tenant application built with [Stratum](https://github.com/stratum-hq/Stratum).
11
+
12
+ **Stack:** ${preset.database} + ${preset.strategy} strategy + ${preset.orm} + ${preset.framework === "none" ? "no framework" : preset.framework}
13
+
14
+ ## Getting started
15
+
16
+ ### 1. Start the database
17
+
18
+ \`\`\`bash
19
+ docker compose up -d
20
+ \`\`\`
21
+ ${dbStartCmd}
22
+
23
+ ### 2. Configure environment
24
+
25
+ \`\`\`bash
26
+ cp .env.example .env
27
+ # Edit .env with your database credentials and Stratum API key
28
+ \`\`\`
29
+
30
+ ### 3. Install dependencies
31
+
32
+ \`\`\`bash
33
+ npm install
34
+ \`\`\`
35
+ ${dbSetupNote}
36
+
37
+ ### 4. Run the app
38
+
39
+ \`\`\`bash
40
+ ${devCmd}
41
+ \`\`\`
42
+
43
+ ## Multi-tenancy
44
+
45
+ This project uses Stratum for hierarchical multi-tenancy with the **${preset.strategy}** isolation strategy:
46
+
47
+ ${getStrategyDescription(preset.strategy)}
48
+
49
+ See the [Stratum docs](https://github.com/stratum-hq/Stratum) for full reference.
50
+ `;
51
+ }
52
+
53
+ function getDevCommand(preset: StackPreset): string {
54
+ if (preset.framework === "nextjs") return "npm run dev";
55
+ if (preset.framework === "nestjs") return "node --env-file=.env src/main.ts";
56
+ if (preset.framework === "none") return "node --env-file=.env src/index.ts";
57
+ return "node --env-file=.env src/index.ts";
58
+ }
59
+
60
+ function getDbStartInfo(preset: StackPreset): string {
61
+ switch (preset.database) {
62
+ case "postgres":
63
+ return "\nThis starts PostgreSQL 16 on port 5432.\n";
64
+ case "mongodb":
65
+ return "\nThis starts MongoDB 7 on port 27017.\n";
66
+ case "mysql":
67
+ return "\nThis starts MySQL 8 on port 3306.\n";
68
+ }
69
+ }
70
+
71
+ function getDbSetupNote(preset: StackPreset): string {
72
+ if (preset.orm === "prisma") {
73
+ return `
74
+ ### 3b. Generate Prisma client
75
+
76
+ \`\`\`bash
77
+ npx prisma generate
78
+ npx prisma db push
79
+ \`\`\`
80
+ `;
81
+ }
82
+ if (preset.orm === "drizzle") {
83
+ return `
84
+ ### 3b. Run Drizzle migrations
85
+
86
+ \`\`\`bash
87
+ npx drizzle-kit push
88
+ \`\`\`
89
+ `;
90
+ }
91
+ return "";
92
+ }
93
+
94
+ function getStrategyDescription(strategy: string): string {
95
+ switch (strategy) {
96
+ case "rls":
97
+ return `- **Row-Level Security** -- PostgreSQL RLS policies filter rows by tenant automatically
98
+ - Each query sets \`app.current_tenant\` and RLS enforces isolation
99
+ - All tenants share one database and schema`;
100
+ case "schema":
101
+ return `- **Schema-per-tenant** -- each tenant gets a dedicated PostgreSQL schema
102
+ - Queries are routed to the correct schema via search_path
103
+ - Shared database, isolated schemas`;
104
+ case "database":
105
+ return `- **Database-per-tenant** -- each tenant gets a fully isolated database
106
+ - Connection routing directs queries to the correct database
107
+ - Maximum isolation at the cost of more resource usage`;
108
+ case "collection":
109
+ return `- **Collection-per-tenant** -- each tenant gets dedicated MongoDB collections
110
+ - Collection names are prefixed or namespaced by tenant ID
111
+ - Shared database, isolated collections`;
112
+ case "table-prefix":
113
+ return `- **Table-prefix** -- tenant-specific tables with a naming prefix
114
+ - Tables are prefixed with the tenant identifier
115
+ - Shared database, prefixed table names`;
116
+ default:
117
+ return "";
118
+ }
119
+ }