@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.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/index.js +1245 -37
- package/package.json +21 -1
- package/src/__tests__/matrix.test.ts +283 -0
- package/src/__tests__/preset.test.ts +266 -0
- package/src/generators/db-setup.ts +320 -0
- package/src/generators/docker-compose.ts +98 -0
- package/src/generators/init-sql.ts +57 -0
- package/src/generators/middleware.ts +267 -0
- package/src/generators/package-json.ts +174 -0
- package/src/generators/readme.ts +119 -0
- package/src/index.ts +61 -11
- package/src/matrix.ts +186 -0
- package/src/preset-project.ts +148 -0
|
@@ -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
|
+
}
|