@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
package/dist/index.js
CHANGED
|
@@ -1,16 +1,1183 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
+
import * as fs2 from "fs";
|
|
5
|
+
import * as path2 from "path";
|
|
6
|
+
import crypto2 from "node:crypto";
|
|
7
|
+
import { execSync as execSync2 } from "child_process";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
|
|
10
|
+
// src/matrix.ts
|
|
11
|
+
var POSTGRES_STRATEGIES = ["rls", "schema", "database"];
|
|
12
|
+
var POSTGRES_ORMS = ["prisma", "drizzle", "sequelize", "knex", "pg"];
|
|
13
|
+
var MONGODB_STRATEGIES = ["database", "collection"];
|
|
14
|
+
var MONGODB_ORMS = ["mongoose"];
|
|
15
|
+
var MYSQL_STRATEGIES = ["database", "table-prefix"];
|
|
16
|
+
var MYSQL_ORMS = ["sequelize", "knex", "pg"];
|
|
17
|
+
var ALL_FRAMEWORKS = ["express", "fastify", "nextjs", "hono", "nestjs", "none"];
|
|
18
|
+
var VALID_COMBINATIONS = {
|
|
19
|
+
postgres: {
|
|
20
|
+
strategies: POSTGRES_STRATEGIES,
|
|
21
|
+
orms: POSTGRES_ORMS,
|
|
22
|
+
frameworks: ALL_FRAMEWORKS
|
|
23
|
+
},
|
|
24
|
+
mongodb: {
|
|
25
|
+
strategies: MONGODB_STRATEGIES,
|
|
26
|
+
orms: MONGODB_ORMS,
|
|
27
|
+
frameworks: ALL_FRAMEWORKS
|
|
28
|
+
},
|
|
29
|
+
mysql: {
|
|
30
|
+
strategies: MYSQL_STRATEGIES,
|
|
31
|
+
orms: MYSQL_ORMS,
|
|
32
|
+
frameworks: ALL_FRAMEWORKS
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var ALL_DATABASES = ["postgres", "mongodb", "mysql"];
|
|
36
|
+
var ALL_STRATEGIES = ["rls", "schema", "database", "collection", "table-prefix"];
|
|
37
|
+
var ALL_ORMS = ["prisma", "drizzle", "sequelize", "knex", "mongoose", "pg"];
|
|
38
|
+
function isValidPreset(preset) {
|
|
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;
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
function parsePresetString(s) {
|
|
51
|
+
if (!s || typeof s !== "string")
|
|
52
|
+
return null;
|
|
53
|
+
const parts = s.toLowerCase().split("-");
|
|
54
|
+
let database;
|
|
55
|
+
let strategy;
|
|
56
|
+
let orm;
|
|
57
|
+
let framework;
|
|
58
|
+
if (parts.length === 4) {
|
|
59
|
+
[database, strategy, orm, framework] = parts;
|
|
60
|
+
} else if (parts.length === 5) {
|
|
61
|
+
database = parts[0];
|
|
62
|
+
strategy = `${parts[1]}-${parts[2]}`;
|
|
63
|
+
orm = parts[3];
|
|
64
|
+
framework = parts[4];
|
|
65
|
+
} else {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
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;
|
|
76
|
+
return {
|
|
77
|
+
database,
|
|
78
|
+
strategy,
|
|
79
|
+
orm,
|
|
80
|
+
framework
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/preset-project.ts
|
|
4
85
|
import * as fs from "fs";
|
|
5
86
|
import * as path from "path";
|
|
87
|
+
import crypto from "node:crypto";
|
|
6
88
|
import { execSync } from "child_process";
|
|
7
|
-
|
|
89
|
+
|
|
90
|
+
// src/generators/docker-compose.ts
|
|
91
|
+
function generatePresetDockerCompose(projectName, preset) {
|
|
92
|
+
const dbName = projectName.replace(/[^a-z0-9]/gi, "_").toLowerCase();
|
|
93
|
+
switch (preset.database) {
|
|
94
|
+
case "postgres":
|
|
95
|
+
return generatePostgresCompose(projectName, dbName);
|
|
96
|
+
case "mongodb":
|
|
97
|
+
return generateMongoCompose(projectName, dbName);
|
|
98
|
+
case "mysql":
|
|
99
|
+
return generateMysqlCompose(projectName, dbName);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function generatePostgresCompose(projectName, dbName) {
|
|
103
|
+
return `# Docker Compose for ${projectName}
|
|
104
|
+
# Start with: docker compose up -d
|
|
105
|
+
version: "3.8"
|
|
106
|
+
|
|
107
|
+
services:
|
|
108
|
+
db:
|
|
109
|
+
image: postgres:16-alpine
|
|
110
|
+
environment:
|
|
111
|
+
POSTGRES_DB: ${dbName}
|
|
112
|
+
POSTGRES_USER: ${dbName}
|
|
113
|
+
POSTGRES_PASSWORD: dev_password
|
|
114
|
+
ports:
|
|
115
|
+
- "5432:5432"
|
|
116
|
+
volumes:
|
|
117
|
+
- db_data:/var/lib/postgresql/data
|
|
118
|
+
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
|
119
|
+
healthcheck:
|
|
120
|
+
test: ["CMD-SHELL", "pg_isready -U ${dbName}"]
|
|
121
|
+
interval: 5s
|
|
122
|
+
timeout: 5s
|
|
123
|
+
retries: 5
|
|
124
|
+
|
|
125
|
+
volumes:
|
|
126
|
+
db_data:
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
129
|
+
function generateMongoCompose(projectName, dbName) {
|
|
130
|
+
return `# Docker Compose for ${projectName}
|
|
131
|
+
# Start with: docker compose up -d
|
|
132
|
+
version: "3.8"
|
|
133
|
+
|
|
134
|
+
services:
|
|
135
|
+
db:
|
|
136
|
+
image: mongo:7
|
|
137
|
+
environment:
|
|
138
|
+
MONGO_INITDB_ROOT_USERNAME: ${dbName}
|
|
139
|
+
MONGO_INITDB_ROOT_PASSWORD: dev_password
|
|
140
|
+
MONGO_INITDB_DATABASE: ${dbName}
|
|
141
|
+
ports:
|
|
142
|
+
- "27017:27017"
|
|
143
|
+
volumes:
|
|
144
|
+
- db_data:/data/db
|
|
145
|
+
healthcheck:
|
|
146
|
+
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
|
|
147
|
+
interval: 5s
|
|
148
|
+
timeout: 5s
|
|
149
|
+
retries: 5
|
|
150
|
+
|
|
151
|
+
volumes:
|
|
152
|
+
db_data:
|
|
153
|
+
`;
|
|
154
|
+
}
|
|
155
|
+
function generateMysqlCompose(projectName, dbName) {
|
|
156
|
+
return `# Docker Compose for ${projectName}
|
|
157
|
+
# Start with: docker compose up -d
|
|
158
|
+
version: "3.8"
|
|
159
|
+
|
|
160
|
+
services:
|
|
161
|
+
db:
|
|
162
|
+
image: mysql:8
|
|
163
|
+
environment:
|
|
164
|
+
MYSQL_DATABASE: ${dbName}
|
|
165
|
+
MYSQL_USER: ${dbName}
|
|
166
|
+
MYSQL_PASSWORD: dev_password
|
|
167
|
+
MYSQL_ROOT_PASSWORD: dev_root_password
|
|
168
|
+
ports:
|
|
169
|
+
- "3306:3306"
|
|
170
|
+
volumes:
|
|
171
|
+
- db_data:/var/lib/mysql
|
|
172
|
+
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
|
173
|
+
healthcheck:
|
|
174
|
+
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
|
175
|
+
interval: 5s
|
|
176
|
+
timeout: 5s
|
|
177
|
+
retries: 5
|
|
178
|
+
|
|
179
|
+
volumes:
|
|
180
|
+
db_data:
|
|
181
|
+
`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/generators/init-sql.ts
|
|
185
|
+
function generatePresetInitSql(projectName, preset) {
|
|
186
|
+
const dbName = projectName.replace(/[^a-z0-9]/gi, "_").toLowerCase();
|
|
187
|
+
switch (preset.database) {
|
|
188
|
+
case "postgres":
|
|
189
|
+
return generatePostgresInit(projectName, dbName, preset.strategy);
|
|
190
|
+
case "mongodb":
|
|
191
|
+
return null;
|
|
192
|
+
case "mysql":
|
|
193
|
+
return generateMysqlInit(projectName, dbName);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function generatePostgresInit(projectName, dbName, strategy) {
|
|
197
|
+
let rlsBlock = "";
|
|
198
|
+
if (strategy === "rls") {
|
|
199
|
+
rlsBlock = `
|
|
200
|
+
-- Enable Row-Level Security for tenant isolation
|
|
201
|
+
-- Add RLS policies to each tenant-scoped table:
|
|
202
|
+
--
|
|
203
|
+
-- ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;
|
|
204
|
+
-- CREATE POLICY tenant_isolation ON your_table
|
|
205
|
+
-- USING (tenant_id = current_setting('app.current_tenant')::uuid);
|
|
206
|
+
--
|
|
207
|
+
-- The Stratum db-adapters package sets app.current_tenant automatically.
|
|
208
|
+
`;
|
|
209
|
+
}
|
|
210
|
+
return `-- Initialize ${projectName} database
|
|
211
|
+
-- Enable required extensions for Stratum multi-tenancy
|
|
212
|
+
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
213
|
+
CREATE EXTENSION IF NOT EXISTS "ltree";
|
|
214
|
+
|
|
215
|
+
-- The ltree extension enables hierarchical tenant trees
|
|
216
|
+
-- uuid-ossp provides uuid_generate_v4() for tenant IDs
|
|
217
|
+
COMMENT ON DATABASE ${dbName} IS 'Multi-tenant database for ${projectName}';
|
|
218
|
+
${rlsBlock}`;
|
|
219
|
+
}
|
|
220
|
+
function generateMysqlInit(projectName, dbName) {
|
|
221
|
+
return `-- Initialize ${projectName} database
|
|
222
|
+
-- MySQL setup for Stratum multi-tenancy
|
|
223
|
+
|
|
224
|
+
-- Ensure utf8mb4 for the database
|
|
225
|
+
ALTER DATABASE ${dbName} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
226
|
+
|
|
227
|
+
-- Tenant metadata table
|
|
228
|
+
CREATE TABLE IF NOT EXISTS _stratum_tenants (
|
|
229
|
+
id VARCHAR(36) PRIMARY KEY,
|
|
230
|
+
name VARCHAR(255) NOT NULL,
|
|
231
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
232
|
+
);
|
|
233
|
+
`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/generators/db-setup.ts
|
|
237
|
+
function generateDbSetup(preset) {
|
|
238
|
+
switch (preset.orm) {
|
|
239
|
+
case "prisma":
|
|
240
|
+
return generatePrismaSetup(preset);
|
|
241
|
+
case "drizzle":
|
|
242
|
+
return generateDrizzleSetup(preset);
|
|
243
|
+
case "sequelize":
|
|
244
|
+
return generateSequelizeSetup(preset);
|
|
245
|
+
case "knex":
|
|
246
|
+
return generateKnexSetup(preset);
|
|
247
|
+
case "mongoose":
|
|
248
|
+
return generateMongooseSetup(preset);
|
|
249
|
+
case "pg":
|
|
250
|
+
return generatePgSetup(preset);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function generatePrismaSetup(preset) {
|
|
254
|
+
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
|
+
return [
|
|
257
|
+
{
|
|
258
|
+
filename: "prisma/schema.prisma",
|
|
259
|
+
content: `// Prisma schema for Stratum multi-tenancy
|
|
260
|
+
generator client {
|
|
261
|
+
provider = "prisma-client-js"
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
datasource db {
|
|
265
|
+
provider = "${provider}"
|
|
266
|
+
url = env("DATABASE_URL")
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
model Tenant {
|
|
270
|
+
id String @id @default(uuid())
|
|
271
|
+
name String
|
|
272
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
273
|
+
|
|
274
|
+
@@map("tenants")
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Add your tenant-scoped models here.
|
|
278
|
+
// Each model that needs tenant isolation should have a tenantId field.
|
|
279
|
+
`
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
filename: "src/stratum-prisma.ts",
|
|
283
|
+
content: `// Prisma client with Stratum tenant-scoped queries
|
|
284
|
+
import { PrismaClient } from "@prisma/client";
|
|
285
|
+
import { Pool } from "pg";
|
|
286
|
+
import { withTenant } from "@stratum-hq/db-adapters";
|
|
287
|
+
|
|
288
|
+
const prisma = new PrismaClient();
|
|
289
|
+
const pool = new Pool({
|
|
290
|
+
connectionString: process.env.DATABASE_URL,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Create a tenant-scoped Prisma client.
|
|
294
|
+
// All queries through this client are automatically filtered by RLS.
|
|
295
|
+
export function createTenantPrisma(getTenantId: () => string) {
|
|
296
|
+
return withTenant(prisma, getTenantId, pool);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Usage:
|
|
300
|
+
// const tenantPrisma = createTenantPrisma(() => currentTenantId);
|
|
301
|
+
// const orders = await tenantPrisma.order.findMany();
|
|
302
|
+
|
|
303
|
+
export { prisma, pool };
|
|
304
|
+
`
|
|
305
|
+
}
|
|
306
|
+
];
|
|
307
|
+
}
|
|
308
|
+
function generateDrizzleSetup(preset) {
|
|
309
|
+
const files = [];
|
|
310
|
+
if (preset.database === "mysql") {
|
|
311
|
+
files.push({
|
|
312
|
+
filename: "src/stratum-drizzle.ts",
|
|
313
|
+
content: `// Drizzle ORM with Stratum tenant-scoped queries (MySQL)
|
|
314
|
+
import { drizzle } from "drizzle-orm/mysql2";
|
|
315
|
+
import mysql from "mysql2/promise";
|
|
316
|
+
|
|
317
|
+
const connection = await mysql.createConnection({
|
|
318
|
+
uri: process.env.DATABASE_URL,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
export const db = drizzle(connection);
|
|
322
|
+
|
|
323
|
+
// For tenant scoping, prefix table access with the tenant database name
|
|
324
|
+
// or use the @stratum-hq/mysql adapter for automatic routing.
|
|
325
|
+
`
|
|
326
|
+
});
|
|
327
|
+
} else {
|
|
328
|
+
files.push({
|
|
329
|
+
filename: "src/stratum-drizzle.ts",
|
|
330
|
+
content: `// Drizzle ORM with Stratum tenant-scoped queries
|
|
331
|
+
import { drizzle } from "drizzle-orm/node-postgres";
|
|
332
|
+
import { Pool } from "pg";
|
|
333
|
+
import { withTenantDrizzle } from "@stratum-hq/db-adapters";
|
|
334
|
+
|
|
335
|
+
const pool = new Pool({
|
|
336
|
+
connectionString: process.env.DATABASE_URL,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
export const db = drizzle(pool);
|
|
340
|
+
|
|
341
|
+
// Create a tenant-scoped Drizzle instance.
|
|
342
|
+
// Queries are automatically filtered by RLS policy.
|
|
343
|
+
export function createTenantDb(getTenantId: () => string) {
|
|
344
|
+
return withTenantDrizzle(db, getTenantId, pool);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Usage:
|
|
348
|
+
// const tenantDb = createTenantDb(() => currentTenantId);
|
|
349
|
+
// const rows = await tenantDb.select().from(orders);
|
|
350
|
+
|
|
351
|
+
export { pool };
|
|
352
|
+
`
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
files.push({
|
|
356
|
+
filename: "drizzle.config.ts",
|
|
357
|
+
content: `import type { Config } from "drizzle-kit";
|
|
358
|
+
|
|
359
|
+
export default {
|
|
360
|
+
schema: "./src/schema.ts",
|
|
361
|
+
out: "./drizzle",
|
|
362
|
+
${preset.database === "mysql" ? 'dialect: "mysql",' : 'dialect: "postgresql",'}
|
|
363
|
+
dbCredentials: {
|
|
364
|
+
url: process.env.DATABASE_URL!,
|
|
365
|
+
},
|
|
366
|
+
} satisfies Config;
|
|
367
|
+
`
|
|
368
|
+
});
|
|
369
|
+
return files;
|
|
370
|
+
}
|
|
371
|
+
function generateSequelizeSetup(preset) {
|
|
372
|
+
const dialect = preset.database === "mysql" ? "mysql" : "postgres";
|
|
373
|
+
return [
|
|
374
|
+
{
|
|
375
|
+
filename: "src/stratum-sequelize.ts",
|
|
376
|
+
content: `// Sequelize with Stratum tenant-scoped queries
|
|
377
|
+
import { Sequelize, DataTypes, Model } from "sequelize";
|
|
378
|
+
|
|
379
|
+
const sequelize = new Sequelize(process.env.DATABASE_URL!, {
|
|
380
|
+
dialect: "${dialect}",
|
|
381
|
+
logging: false,
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Define a tenant-scoped model example
|
|
385
|
+
class Tenant extends Model {}
|
|
386
|
+
Tenant.init(
|
|
387
|
+
{
|
|
388
|
+
id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4 },
|
|
389
|
+
name: { type: DataTypes.STRING, allowNull: false },
|
|
390
|
+
},
|
|
391
|
+
{ sequelize, tableName: "tenants", underscored: true },
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
// For tenant isolation, add a tenantId scope to each model:
|
|
395
|
+
// MyModel.addScope("tenant", (tenantId) => ({ where: { tenant_id: tenantId } }));
|
|
396
|
+
// const rows = await MyModel.scope({ method: ["tenant", currentTenantId] }).findAll();
|
|
397
|
+
|
|
398
|
+
export { sequelize, Tenant };
|
|
399
|
+
`
|
|
400
|
+
}
|
|
401
|
+
];
|
|
402
|
+
}
|
|
403
|
+
function generateKnexSetup(preset) {
|
|
404
|
+
const client = preset.database === "mysql" ? "mysql2" : "pg";
|
|
405
|
+
return [
|
|
406
|
+
{
|
|
407
|
+
filename: "knexfile.ts",
|
|
408
|
+
content: `// Knex configuration for Stratum
|
|
409
|
+
import type { Knex } from "knex";
|
|
410
|
+
|
|
411
|
+
const config: Knex.Config = {
|
|
412
|
+
client: "${client}",
|
|
413
|
+
connection: process.env.DATABASE_URL,
|
|
414
|
+
migrations: {
|
|
415
|
+
directory: "./migrations",
|
|
416
|
+
extension: "ts",
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
export default config;
|
|
421
|
+
`
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
filename: "src/stratum-knex.ts",
|
|
425
|
+
content: `// Knex with Stratum tenant-scoped queries
|
|
426
|
+
import Knex from "knex";
|
|
427
|
+
import config from "../knexfile.js";
|
|
428
|
+
|
|
429
|
+
const knex = Knex(config);
|
|
430
|
+
|
|
431
|
+
// Create a tenant-scoped query builder.
|
|
432
|
+
// For RLS strategy, set the session variable before queries.
|
|
433
|
+
export async function withTenantScope(tenantId: string, fn: (db: typeof knex) => Promise<void>) {
|
|
434
|
+
${preset.database === "postgres" ? ` await knex.raw("SET app.current_tenant = ?", [tenantId]);` : ` // For ${preset.database}, scope queries by tenant_id column`}
|
|
435
|
+
try {
|
|
436
|
+
await fn(knex);
|
|
437
|
+
} finally {
|
|
438
|
+
${preset.database === "postgres" ? ` await knex.raw("RESET app.current_tenant");` : ` // Scope cleanup not needed for column-based isolation`}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export { knex };
|
|
443
|
+
`
|
|
444
|
+
}
|
|
445
|
+
];
|
|
446
|
+
}
|
|
447
|
+
function generateMongooseSetup(_preset) {
|
|
448
|
+
return [
|
|
449
|
+
{
|
|
450
|
+
filename: "src/stratum-mongoose.ts",
|
|
451
|
+
content: `// Mongoose with Stratum multi-tenant support
|
|
452
|
+
import mongoose from "mongoose";
|
|
453
|
+
import { createTenantConnection } from "@stratum-hq/mongodb";
|
|
454
|
+
|
|
455
|
+
// Main connection (used for tenant metadata)
|
|
456
|
+
const mainConnection = mongoose.createConnection(
|
|
457
|
+
process.env.MONGODB_URI || "mongodb://localhost:27017/main",
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
// Create a tenant-scoped connection.
|
|
461
|
+
// Each tenant gets its own database (database-per-tenant strategy)
|
|
462
|
+
// or collection prefix (collection strategy).
|
|
463
|
+
export function getTenantConnection(tenantId: string) {
|
|
464
|
+
return createTenantConnection(mainConnection, tenantId);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Define schemas that work across tenant connections
|
|
468
|
+
export const TenantSchema = new mongoose.Schema({
|
|
469
|
+
name: { type: String, required: true },
|
|
470
|
+
createdAt: { type: Date, default: Date.now },
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Usage:
|
|
474
|
+
// const conn = getTenantConnection("tenant-abc");
|
|
475
|
+
// const Order = conn.model("Order", OrderSchema);
|
|
476
|
+
// const orders = await Order.find();
|
|
477
|
+
|
|
478
|
+
export { mainConnection };
|
|
479
|
+
`
|
|
480
|
+
}
|
|
481
|
+
];
|
|
482
|
+
}
|
|
483
|
+
function generatePgSetup(preset) {
|
|
484
|
+
if (preset.database === "mysql") {
|
|
485
|
+
return [
|
|
486
|
+
{
|
|
487
|
+
filename: "src/stratum-db.ts",
|
|
488
|
+
content: `// Raw MySQL client with Stratum tenant-scoped queries
|
|
489
|
+
import mysql from "mysql2/promise";
|
|
490
|
+
|
|
491
|
+
const pool = mysql.createPool({
|
|
492
|
+
uri: process.env.DATABASE_URL,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// Execute a query scoped to a tenant by filtering on tenant_id
|
|
496
|
+
export async function tenantQuery(tenantId: string, sql: string, params: unknown[] = []) {
|
|
497
|
+
const [rows] = await pool.execute(sql, [...params, tenantId]);
|
|
498
|
+
return rows;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Usage:
|
|
502
|
+
// const orders = await tenantQuery("tenant-abc", "SELECT * FROM orders WHERE tenant_id = ?");
|
|
503
|
+
|
|
504
|
+
export { pool };
|
|
505
|
+
`
|
|
506
|
+
}
|
|
507
|
+
];
|
|
508
|
+
}
|
|
509
|
+
return [
|
|
510
|
+
{
|
|
511
|
+
filename: "src/stratum-db.ts",
|
|
512
|
+
content: `// PostgreSQL client with Stratum tenant-scoped queries
|
|
513
|
+
import { Pool } from "pg";
|
|
514
|
+
import { createTenantPool } from "@stratum-hq/db-adapters";
|
|
515
|
+
|
|
516
|
+
const pool = new Pool({
|
|
517
|
+
connectionString: process.env.DATABASE_URL,
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// Create a tenant-scoped pool.
|
|
521
|
+
// Sets app.current_tenant on each connection for RLS enforcement.
|
|
522
|
+
export function getTenantPool(tenantId: string) {
|
|
523
|
+
return createTenantPool(pool, tenantId);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Usage:
|
|
527
|
+
// const tenantPool = getTenantPool("tenant-abc");
|
|
528
|
+
// const { rows } = await tenantPool.query("SELECT * FROM orders");
|
|
529
|
+
|
|
530
|
+
export { pool };
|
|
531
|
+
`
|
|
532
|
+
}
|
|
533
|
+
];
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/generators/middleware.ts
|
|
537
|
+
function generateMiddleware(projectName, preset) {
|
|
538
|
+
switch (preset.framework) {
|
|
539
|
+
case "express":
|
|
540
|
+
return generateExpressMiddleware(projectName);
|
|
541
|
+
case "fastify":
|
|
542
|
+
return generateFastifyMiddleware(projectName);
|
|
543
|
+
case "nextjs":
|
|
544
|
+
return generateNextjsMiddleware(projectName);
|
|
545
|
+
case "hono":
|
|
546
|
+
return generateHonoMiddleware(projectName);
|
|
547
|
+
case "nestjs":
|
|
548
|
+
return generateNestjsMiddleware(projectName);
|
|
549
|
+
case "none":
|
|
550
|
+
return generateNoFramework(projectName);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
function generateExpressMiddleware(projectName) {
|
|
554
|
+
return [
|
|
555
|
+
{
|
|
556
|
+
filename: "src/index.ts",
|
|
557
|
+
content: `import express from "express";
|
|
558
|
+
|
|
559
|
+
const app = express();
|
|
560
|
+
const port = Number(process.env.PORT) || 3000;
|
|
561
|
+
|
|
562
|
+
app.use(express.json());
|
|
563
|
+
|
|
564
|
+
// Tenant extraction middleware
|
|
565
|
+
app.use((req, _res, next) => {
|
|
566
|
+
const tenantId =
|
|
567
|
+
req.headers["x-tenant-id"] as string ||
|
|
568
|
+
req.hostname.split(".")[0];
|
|
569
|
+
(req as any).tenantId = tenantId;
|
|
570
|
+
next();
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
app.get("/health", (_req, res) => {
|
|
574
|
+
res.json({ status: "ok", project: "${projectName}" });
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
app.get("/tenants", async (req, res) => {
|
|
578
|
+
const tenantId = (req as any).tenantId;
|
|
579
|
+
res.json({ tenantId, message: "Replace with your tenant queries" });
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
app.listen(port, () => {
|
|
583
|
+
console.log(\`${projectName} running on http://localhost:\${port}\`);
|
|
584
|
+
});
|
|
585
|
+
`
|
|
586
|
+
}
|
|
587
|
+
];
|
|
588
|
+
}
|
|
589
|
+
function generateFastifyMiddleware(projectName) {
|
|
590
|
+
return [
|
|
591
|
+
{
|
|
592
|
+
filename: "src/index.ts",
|
|
593
|
+
content: `import Fastify from "fastify";
|
|
594
|
+
|
|
595
|
+
const fastify = Fastify({ logger: true });
|
|
596
|
+
const port = Number(process.env.PORT) || 3000;
|
|
597
|
+
|
|
598
|
+
// Tenant extraction plugin
|
|
599
|
+
fastify.decorateRequest("tenantId", "");
|
|
600
|
+
fastify.addHook("onRequest", async (request) => {
|
|
601
|
+
const tenantId =
|
|
602
|
+
(request.headers["x-tenant-id"] as string) ||
|
|
603
|
+
(request.hostname?.split(".")[0] ?? "");
|
|
604
|
+
(request as any).tenantId = tenantId;
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
fastify.get("/health", async () => {
|
|
608
|
+
return { status: "ok", project: "${projectName}" };
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
fastify.get("/tenants", async (request) => {
|
|
612
|
+
const tenantId = (request as any).tenantId;
|
|
613
|
+
return { tenantId, message: "Replace with your tenant queries" };
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
fastify.listen({ port, host: "0.0.0.0" }, (err) => {
|
|
617
|
+
if (err) {
|
|
618
|
+
fastify.log.error(err);
|
|
619
|
+
process.exit(1);
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
`
|
|
623
|
+
}
|
|
624
|
+
];
|
|
625
|
+
}
|
|
626
|
+
function generateNextjsMiddleware(projectName) {
|
|
627
|
+
return [
|
|
628
|
+
{
|
|
629
|
+
filename: "middleware.ts",
|
|
630
|
+
content: `// Next.js edge middleware for tenant resolution
|
|
631
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
632
|
+
|
|
633
|
+
export function middleware(request: NextRequest) {
|
|
634
|
+
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;
|
|
640
|
+
|
|
641
|
+
const requestHeaders = new Headers(request.headers);
|
|
642
|
+
if (tenantId && tenantId !== "localhost" && tenantId !== "www") {
|
|
643
|
+
requestHeaders.set("x-tenant-id", tenantId);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return NextResponse.next({ request: { headers: requestHeaders } });
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export const config = {
|
|
650
|
+
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
|
|
651
|
+
};
|
|
652
|
+
`
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
filename: "src/app/page.tsx",
|
|
656
|
+
content: `// app/page.tsx - ${projectName} root page
|
|
657
|
+
export default function Home() {
|
|
658
|
+
return (
|
|
659
|
+
<main style={{ padding: "2rem", fontFamily: "sans-serif" }}>
|
|
660
|
+
<h1>${projectName}</h1>
|
|
661
|
+
<p>Multi-tenant app powered by Stratum.</p>
|
|
662
|
+
<ul>
|
|
663
|
+
<li>Configure tenants via the Stratum control plane</li>
|
|
664
|
+
<li>Access tenant context via <code>x-tenant-id</code> header</li>
|
|
665
|
+
<li>Use <code>@stratum-hq/lib</code> for tenant resolution</li>
|
|
666
|
+
</ul>
|
|
667
|
+
</main>
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
`
|
|
671
|
+
}
|
|
672
|
+
];
|
|
673
|
+
}
|
|
674
|
+
function generateHonoMiddleware(projectName) {
|
|
675
|
+
return [
|
|
676
|
+
{
|
|
677
|
+
filename: "src/index.ts",
|
|
678
|
+
content: `import { Hono } from "hono";
|
|
679
|
+
import { serve } from "@hono/node-server";
|
|
680
|
+
|
|
681
|
+
const app = new Hono();
|
|
682
|
+
|
|
683
|
+
// Tenant extraction middleware
|
|
684
|
+
app.use("*", async (c, next) => {
|
|
685
|
+
const tenantId =
|
|
686
|
+
c.req.header("x-tenant-id") ||
|
|
687
|
+
new URL(c.req.url).hostname.split(".")[0];
|
|
688
|
+
c.set("tenantId", tenantId);
|
|
689
|
+
await next();
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
app.get("/health", (c) => {
|
|
693
|
+
return c.json({ status: "ok", project: "${projectName}" });
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
app.get("/tenants", (c) => {
|
|
697
|
+
const tenantId = c.get("tenantId");
|
|
698
|
+
return c.json({ tenantId, message: "Replace with your tenant queries" });
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
const port = Number(process.env.PORT) || 3000;
|
|
702
|
+
console.log(\`${projectName} running on http://localhost:\${port}\`);
|
|
703
|
+
serve({ fetch: app.fetch, port });
|
|
704
|
+
`
|
|
705
|
+
}
|
|
706
|
+
];
|
|
707
|
+
}
|
|
708
|
+
function generateNestjsMiddleware(projectName) {
|
|
709
|
+
return [
|
|
710
|
+
{
|
|
711
|
+
filename: "src/main.ts",
|
|
712
|
+
content: `import { NestFactory } from "@nestjs/core";
|
|
713
|
+
import { AppModule } from "./app.module.js";
|
|
714
|
+
|
|
715
|
+
async function bootstrap() {
|
|
716
|
+
const app = await NestFactory.create(AppModule);
|
|
717
|
+
const port = Number(process.env.PORT) || 3000;
|
|
718
|
+
await app.listen(port);
|
|
719
|
+
console.log(\`${projectName} running on http://localhost:\${port}\`);
|
|
720
|
+
}
|
|
721
|
+
bootstrap();
|
|
722
|
+
`
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
filename: "src/app.module.ts",
|
|
726
|
+
content: `import { Module } from "@nestjs/common";
|
|
727
|
+
import { AppController } from "./app.controller.js";
|
|
728
|
+
import { TenantGuard } from "./tenant.guard.js";
|
|
729
|
+
import { APP_GUARD } from "@nestjs/core";
|
|
730
|
+
|
|
731
|
+
@Module({
|
|
732
|
+
controllers: [AppController],
|
|
733
|
+
providers: [
|
|
734
|
+
{ provide: APP_GUARD, useClass: TenantGuard },
|
|
735
|
+
],
|
|
736
|
+
})
|
|
737
|
+
export class AppModule {}
|
|
738
|
+
`
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
filename: "src/app.controller.ts",
|
|
742
|
+
content: `import { Controller, Get, Req } from "@nestjs/common";
|
|
743
|
+
|
|
744
|
+
@Controller()
|
|
745
|
+
export class AppController {
|
|
746
|
+
@Get("health")
|
|
747
|
+
health() {
|
|
748
|
+
return { status: "ok", project: "${projectName}" };
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
@Get("tenants")
|
|
752
|
+
tenants(@Req() req: any) {
|
|
753
|
+
return { tenantId: req.tenantId, message: "Replace with your tenant queries" };
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
`
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
filename: "src/tenant.guard.ts",
|
|
760
|
+
content: `import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
|
|
761
|
+
|
|
762
|
+
@Injectable()
|
|
763
|
+
export class TenantGuard implements CanActivate {
|
|
764
|
+
canActivate(context: ExecutionContext): boolean {
|
|
765
|
+
const request = context.switchToHttp().getRequest();
|
|
766
|
+
const tenantId =
|
|
767
|
+
request.headers["x-tenant-id"] ||
|
|
768
|
+
request.hostname?.split(".")[0];
|
|
769
|
+
request.tenantId = tenantId || null;
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
`
|
|
774
|
+
}
|
|
775
|
+
];
|
|
776
|
+
}
|
|
777
|
+
function generateNoFramework(projectName) {
|
|
778
|
+
return [
|
|
779
|
+
{
|
|
780
|
+
filename: "src/index.ts",
|
|
781
|
+
content: `// ${projectName} - Stratum multi-tenant setup (no framework)
|
|
782
|
+
// Use this as a starting point and integrate with your framework of choice.
|
|
783
|
+
|
|
784
|
+
console.log("${projectName} initialized");
|
|
785
|
+
console.log("Import your database setup from ./stratum-db or equivalent");
|
|
786
|
+
console.log("Add tenant resolution logic for your use case");
|
|
787
|
+
`
|
|
788
|
+
}
|
|
789
|
+
];
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// src/generators/package-json.ts
|
|
793
|
+
function generatePresetPackageJson(projectName, preset) {
|
|
794
|
+
const deps = {
|
|
795
|
+
"@stratum-hq/lib": "^0.2.0"
|
|
796
|
+
};
|
|
797
|
+
const devDeps = {
|
|
798
|
+
typescript: "^5.3.0",
|
|
799
|
+
"@types/node": "^20.11.0"
|
|
800
|
+
};
|
|
801
|
+
addDatabaseDeps(deps, preset);
|
|
802
|
+
addOrmDeps(deps, devDeps, preset);
|
|
803
|
+
addFrameworkDeps(deps, devDeps, preset);
|
|
804
|
+
addStratumDeps(deps, preset);
|
|
805
|
+
const scripts = getScripts(preset);
|
|
806
|
+
return JSON.stringify(
|
|
807
|
+
{
|
|
808
|
+
name: projectName,
|
|
809
|
+
version: "0.1.0",
|
|
810
|
+
private: true,
|
|
811
|
+
type: "module",
|
|
812
|
+
scripts,
|
|
813
|
+
dependencies: sortKeys(deps),
|
|
814
|
+
devDependencies: sortKeys(devDeps),
|
|
815
|
+
engines: {
|
|
816
|
+
node: ">=20.0.0"
|
|
817
|
+
}
|
|
818
|
+
},
|
|
819
|
+
null,
|
|
820
|
+
2
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
function addDatabaseDeps(deps, preset) {
|
|
824
|
+
switch (preset.database) {
|
|
825
|
+
case "postgres":
|
|
826
|
+
if (preset.orm !== "prisma" && preset.orm !== "drizzle" && preset.orm !== "sequelize") {
|
|
827
|
+
deps["pg"] = "^8.11.0";
|
|
828
|
+
}
|
|
829
|
+
if (preset.orm === "drizzle") {
|
|
830
|
+
deps["pg"] = "^8.11.0";
|
|
831
|
+
}
|
|
832
|
+
if (preset.orm === "pg") {
|
|
833
|
+
deps["pg"] = "^8.11.0";
|
|
834
|
+
}
|
|
835
|
+
if (preset.orm === "knex") {
|
|
836
|
+
deps["pg"] = "^8.11.0";
|
|
837
|
+
}
|
|
838
|
+
break;
|
|
839
|
+
case "mongodb":
|
|
840
|
+
break;
|
|
841
|
+
case "mysql":
|
|
842
|
+
if (preset.orm === "drizzle" || preset.orm === "pg") {
|
|
843
|
+
deps["mysql2"] = "^3.9.0";
|
|
844
|
+
}
|
|
845
|
+
break;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
function addOrmDeps(deps, devDeps, preset) {
|
|
849
|
+
switch (preset.orm) {
|
|
850
|
+
case "prisma":
|
|
851
|
+
deps["@prisma/client"] = "^5.10.0";
|
|
852
|
+
devDeps["prisma"] = "^5.10.0";
|
|
853
|
+
if (preset.database === "postgres") {
|
|
854
|
+
deps["pg"] = "^8.11.0";
|
|
855
|
+
}
|
|
856
|
+
break;
|
|
857
|
+
case "drizzle":
|
|
858
|
+
deps["drizzle-orm"] = "^0.30.0";
|
|
859
|
+
devDeps["drizzle-kit"] = "^0.21.0";
|
|
860
|
+
if (preset.database === "postgres") {
|
|
861
|
+
deps["pg"] = "^8.11.0";
|
|
862
|
+
}
|
|
863
|
+
break;
|
|
864
|
+
case "sequelize":
|
|
865
|
+
deps["sequelize"] = "^6.37.0";
|
|
866
|
+
if (preset.database === "mysql") {
|
|
867
|
+
deps["mysql2"] = "^3.9.0";
|
|
868
|
+
} else {
|
|
869
|
+
deps["pg"] = "^8.11.0";
|
|
870
|
+
}
|
|
871
|
+
break;
|
|
872
|
+
case "knex":
|
|
873
|
+
deps["knex"] = "^3.1.0";
|
|
874
|
+
break;
|
|
875
|
+
case "mongoose":
|
|
876
|
+
deps["mongoose"] = "^8.2.0";
|
|
877
|
+
break;
|
|
878
|
+
case "pg":
|
|
879
|
+
break;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
function addFrameworkDeps(deps, devDeps, preset) {
|
|
883
|
+
switch (preset.framework) {
|
|
884
|
+
case "express":
|
|
885
|
+
deps["express"] = "^4.18.0";
|
|
886
|
+
devDeps["@types/express"] = "^4.17.21";
|
|
887
|
+
break;
|
|
888
|
+
case "fastify":
|
|
889
|
+
deps["fastify"] = "^4.26.0";
|
|
890
|
+
break;
|
|
891
|
+
case "nextjs":
|
|
892
|
+
deps["next"] = "^15.5.16";
|
|
893
|
+
deps["react"] = "^19.0.0";
|
|
894
|
+
deps["react-dom"] = "^19.0.0";
|
|
895
|
+
devDeps["@types/react"] = "^19.0.0";
|
|
896
|
+
devDeps["@types/react-dom"] = "^19.0.0";
|
|
897
|
+
break;
|
|
898
|
+
case "hono":
|
|
899
|
+
deps["hono"] = "^4.1.0";
|
|
900
|
+
deps["@hono/node-server"] = "^1.8.0";
|
|
901
|
+
break;
|
|
902
|
+
case "nestjs":
|
|
903
|
+
deps["@nestjs/core"] = "^11.0.0";
|
|
904
|
+
deps["@nestjs/common"] = "^11.0.0";
|
|
905
|
+
deps["@nestjs/platform-express"] = "^11.0.0";
|
|
906
|
+
deps["reflect-metadata"] = "^0.2.0";
|
|
907
|
+
deps["rxjs"] = "^7.8.0";
|
|
908
|
+
break;
|
|
909
|
+
case "none":
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
function addStratumDeps(deps, preset) {
|
|
914
|
+
if (preset.database === "postgres" && preset.orm !== "mongoose") {
|
|
915
|
+
deps["@stratum-hq/db-adapters"] = "^0.2.0";
|
|
916
|
+
}
|
|
917
|
+
if (preset.database === "mongodb") {
|
|
918
|
+
deps["@stratum-hq/mongodb"] = "^0.2.0";
|
|
919
|
+
}
|
|
920
|
+
if (preset.database === "mysql") {
|
|
921
|
+
deps["@stratum-hq/mysql"] = "^0.2.0";
|
|
922
|
+
}
|
|
923
|
+
if (preset.framework === "hono") {
|
|
924
|
+
deps["@stratum-hq/hono"] = "^0.2.0";
|
|
925
|
+
}
|
|
926
|
+
if (preset.framework === "nestjs") {
|
|
927
|
+
deps["@stratum-hq/nestjs"] = "^0.2.0";
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
function getScripts(preset) {
|
|
931
|
+
if (preset.framework === "nextjs") {
|
|
932
|
+
return { dev: "next dev", build: "next build", start: "next start" };
|
|
933
|
+
}
|
|
934
|
+
if (preset.framework === "nestjs") {
|
|
935
|
+
return { dev: "node --watch src/main.ts", build: "tsc", start: "node dist/main.js" };
|
|
936
|
+
}
|
|
937
|
+
return { dev: "node --watch src/index.js", build: "tsc", start: "node dist/index.js" };
|
|
938
|
+
}
|
|
939
|
+
function sortKeys(obj) {
|
|
940
|
+
const sorted = {};
|
|
941
|
+
for (const key of Object.keys(obj).sort()) {
|
|
942
|
+
sorted[key] = obj[key];
|
|
943
|
+
}
|
|
944
|
+
return sorted;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// src/generators/readme.ts
|
|
948
|
+
function generatePresetReadme(projectName, preset) {
|
|
949
|
+
const devCmd = getDevCommand(preset);
|
|
950
|
+
const dbStartCmd = getDbStartInfo(preset);
|
|
951
|
+
const dbSetupNote = getDbSetupNote(preset);
|
|
952
|
+
return `# ${projectName}
|
|
953
|
+
|
|
954
|
+
A multi-tenant application built with [Stratum](https://github.com/stratum-hq/Stratum).
|
|
955
|
+
|
|
956
|
+
**Stack:** ${preset.database} + ${preset.strategy} strategy + ${preset.orm} + ${preset.framework === "none" ? "no framework" : preset.framework}
|
|
957
|
+
|
|
958
|
+
## Getting started
|
|
959
|
+
|
|
960
|
+
### 1. Start the database
|
|
961
|
+
|
|
962
|
+
\`\`\`bash
|
|
963
|
+
docker compose up -d
|
|
964
|
+
\`\`\`
|
|
965
|
+
${dbStartCmd}
|
|
966
|
+
|
|
967
|
+
### 2. Configure environment
|
|
968
|
+
|
|
969
|
+
\`\`\`bash
|
|
970
|
+
cp .env.example .env
|
|
971
|
+
# Edit .env with your database credentials and Stratum API key
|
|
972
|
+
\`\`\`
|
|
973
|
+
|
|
974
|
+
### 3. Install dependencies
|
|
975
|
+
|
|
976
|
+
\`\`\`bash
|
|
977
|
+
npm install
|
|
978
|
+
\`\`\`
|
|
979
|
+
${dbSetupNote}
|
|
980
|
+
|
|
981
|
+
### 4. Run the app
|
|
982
|
+
|
|
983
|
+
\`\`\`bash
|
|
984
|
+
${devCmd}
|
|
985
|
+
\`\`\`
|
|
986
|
+
|
|
987
|
+
## Multi-tenancy
|
|
988
|
+
|
|
989
|
+
This project uses Stratum for hierarchical multi-tenancy with the **${preset.strategy}** isolation strategy:
|
|
990
|
+
|
|
991
|
+
${getStrategyDescription(preset.strategy)}
|
|
992
|
+
|
|
993
|
+
See the [Stratum docs](https://github.com/stratum-hq/Stratum) for full reference.
|
|
994
|
+
`;
|
|
995
|
+
}
|
|
996
|
+
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";
|
|
1003
|
+
return "node --env-file=.env src/index.ts";
|
|
1004
|
+
}
|
|
1005
|
+
function getDbStartInfo(preset) {
|
|
1006
|
+
switch (preset.database) {
|
|
1007
|
+
case "postgres":
|
|
1008
|
+
return "\nThis starts PostgreSQL 16 on port 5432.\n";
|
|
1009
|
+
case "mongodb":
|
|
1010
|
+
return "\nThis starts MongoDB 7 on port 27017.\n";
|
|
1011
|
+
case "mysql":
|
|
1012
|
+
return "\nThis starts MySQL 8 on port 3306.\n";
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
function getDbSetupNote(preset) {
|
|
1016
|
+
if (preset.orm === "prisma") {
|
|
1017
|
+
return `
|
|
1018
|
+
### 3b. Generate Prisma client
|
|
1019
|
+
|
|
1020
|
+
\`\`\`bash
|
|
1021
|
+
npx prisma generate
|
|
1022
|
+
npx prisma db push
|
|
1023
|
+
\`\`\`
|
|
1024
|
+
`;
|
|
1025
|
+
}
|
|
1026
|
+
if (preset.orm === "drizzle") {
|
|
1027
|
+
return `
|
|
1028
|
+
### 3b. Run Drizzle migrations
|
|
1029
|
+
|
|
1030
|
+
\`\`\`bash
|
|
1031
|
+
npx drizzle-kit push
|
|
1032
|
+
\`\`\`
|
|
1033
|
+
`;
|
|
1034
|
+
}
|
|
1035
|
+
return "";
|
|
1036
|
+
}
|
|
1037
|
+
function getStrategyDescription(strategy) {
|
|
1038
|
+
switch (strategy) {
|
|
1039
|
+
case "rls":
|
|
1040
|
+
return `- **Row-Level Security** -- PostgreSQL RLS policies filter rows by tenant automatically
|
|
1041
|
+
- Each query sets \`app.current_tenant\` and RLS enforces isolation
|
|
1042
|
+
- All tenants share one database and schema`;
|
|
1043
|
+
case "schema":
|
|
1044
|
+
return `- **Schema-per-tenant** -- each tenant gets a dedicated PostgreSQL schema
|
|
1045
|
+
- Queries are routed to the correct schema via search_path
|
|
1046
|
+
- Shared database, isolated schemas`;
|
|
1047
|
+
case "database":
|
|
1048
|
+
return `- **Database-per-tenant** -- each tenant gets a fully isolated database
|
|
1049
|
+
- Connection routing directs queries to the correct database
|
|
1050
|
+
- Maximum isolation at the cost of more resource usage`;
|
|
1051
|
+
case "collection":
|
|
1052
|
+
return `- **Collection-per-tenant** -- each tenant gets dedicated MongoDB collections
|
|
1053
|
+
- Collection names are prefixed or namespaced by tenant ID
|
|
1054
|
+
- Shared database, isolated collections`;
|
|
1055
|
+
case "table-prefix":
|
|
1056
|
+
return `- **Table-prefix** -- tenant-specific tables with a naming prefix
|
|
1057
|
+
- Tables are prefixed with the tenant identifier
|
|
1058
|
+
- Shared database, prefixed table names`;
|
|
1059
|
+
default:
|
|
1060
|
+
return "";
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// src/preset-project.ts
|
|
1065
|
+
function writeFile(filePath, content) {
|
|
1066
|
+
const dir = path.dirname(filePath);
|
|
1067
|
+
if (!fs.existsSync(dir)) {
|
|
1068
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1069
|
+
}
|
|
1070
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
1071
|
+
console.log(` created ${path.relative(process.cwd(), filePath)}`);
|
|
1072
|
+
}
|
|
1073
|
+
function generatePresetEnv(projectName, preset) {
|
|
1074
|
+
const dbName = projectName.replace(/[^a-z0-9]/gi, "_").toLowerCase();
|
|
1075
|
+
const jwtSecret = crypto.randomBytes(32).toString("base64url");
|
|
1076
|
+
let dbUrl;
|
|
1077
|
+
switch (preset.database) {
|
|
1078
|
+
case "postgres":
|
|
1079
|
+
dbUrl = `postgres://${dbName}:dev_password@localhost:5432/${dbName}`;
|
|
1080
|
+
break;
|
|
1081
|
+
case "mongodb":
|
|
1082
|
+
dbUrl = `mongodb://${dbName}:dev_password@localhost:27017/${dbName}?authSource=admin`;
|
|
1083
|
+
break;
|
|
1084
|
+
case "mysql":
|
|
1085
|
+
dbUrl = `mysql://${dbName}:dev_password@localhost:3306/${dbName}`;
|
|
1086
|
+
break;
|
|
1087
|
+
}
|
|
1088
|
+
const urlKey = preset.database === "mongodb" ? "MONGODB_URI" : "DATABASE_URL";
|
|
1089
|
+
return `# Environment variables for ${projectName}
|
|
1090
|
+
# Copy to .env and fill in values
|
|
1091
|
+
|
|
1092
|
+
# Database
|
|
1093
|
+
${urlKey}=${dbUrl}
|
|
1094
|
+
|
|
1095
|
+
# Authentication
|
|
1096
|
+
JWT_SECRET=${jwtSecret}
|
|
1097
|
+
|
|
1098
|
+
# Stratum Control Plane
|
|
1099
|
+
STRATUM_URL=http://localhost:3001
|
|
1100
|
+
STRATUM_API_KEY=sk_test_your_key_here
|
|
1101
|
+
|
|
1102
|
+
NODE_ENV=development
|
|
1103
|
+
`;
|
|
1104
|
+
}
|
|
1105
|
+
function createPresetProject(projectName, preset, targetDir, skipInstall) {
|
|
1106
|
+
const presetLabel = `${preset.database}/${preset.strategy}/${preset.orm}/${preset.framework}`;
|
|
1107
|
+
console.log(`
|
|
1108
|
+
Creating ${projectName} with preset ${presetLabel}...
|
|
1109
|
+
`);
|
|
1110
|
+
writeFile(
|
|
1111
|
+
path.join(targetDir, "package.json"),
|
|
1112
|
+
generatePresetPackageJson(projectName, preset)
|
|
1113
|
+
);
|
|
1114
|
+
writeFile(
|
|
1115
|
+
path.join(targetDir, "docker-compose.yml"),
|
|
1116
|
+
generatePresetDockerCompose(projectName, preset)
|
|
1117
|
+
);
|
|
1118
|
+
const initSql = generatePresetInitSql(projectName, preset);
|
|
1119
|
+
if (initSql !== null) {
|
|
1120
|
+
writeFile(path.join(targetDir, "init.sql"), initSql);
|
|
1121
|
+
}
|
|
1122
|
+
writeFile(
|
|
1123
|
+
path.join(targetDir, ".env.example"),
|
|
1124
|
+
generatePresetEnv(projectName, preset)
|
|
1125
|
+
);
|
|
1126
|
+
const dbFiles = generateDbSetup(preset);
|
|
1127
|
+
for (const file of dbFiles) {
|
|
1128
|
+
writeFile(path.join(targetDir, file.filename), file.content);
|
|
1129
|
+
}
|
|
1130
|
+
const middlewareFiles = generateMiddleware(projectName, preset);
|
|
1131
|
+
for (const file of middlewareFiles) {
|
|
1132
|
+
writeFile(path.join(targetDir, file.filename), file.content);
|
|
1133
|
+
}
|
|
1134
|
+
writeFile(
|
|
1135
|
+
path.join(targetDir, "README.md"),
|
|
1136
|
+
generatePresetReadme(projectName, preset)
|
|
1137
|
+
);
|
|
1138
|
+
writeFile(
|
|
1139
|
+
path.join(targetDir, "tsconfig.json"),
|
|
1140
|
+
JSON.stringify(
|
|
1141
|
+
{
|
|
1142
|
+
compilerOptions: {
|
|
1143
|
+
target: "ESNext",
|
|
1144
|
+
module: "NodeNext",
|
|
1145
|
+
moduleResolution: "NodeNext",
|
|
1146
|
+
strict: true,
|
|
1147
|
+
outDir: "dist",
|
|
1148
|
+
rootDir: "src",
|
|
1149
|
+
declaration: true,
|
|
1150
|
+
skipLibCheck: true,
|
|
1151
|
+
esModuleInterop: true,
|
|
1152
|
+
...preset.framework === "nestjs" ? { experimentalDecorators: true, emitDecoratorMetadata: true } : {},
|
|
1153
|
+
...preset.framework === "nextjs" ? { jsx: "preserve", plugins: [{ name: "next" }] } : {}
|
|
1154
|
+
},
|
|
1155
|
+
include: ["src"],
|
|
1156
|
+
exclude: ["node_modules", "dist"]
|
|
1157
|
+
},
|
|
1158
|
+
null,
|
|
1159
|
+
2
|
|
1160
|
+
)
|
|
1161
|
+
);
|
|
1162
|
+
if (!skipInstall) {
|
|
1163
|
+
console.log("\nInstalling dependencies...\n");
|
|
1164
|
+
try {
|
|
1165
|
+
execSync("npm install", { cwd: targetDir, stdio: "inherit" });
|
|
1166
|
+
} catch {
|
|
1167
|
+
console.warn("\nWarning: npm install failed. Run it manually in the project directory.");
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// src/index.ts
|
|
8
1173
|
function parseArgs(argv) {
|
|
9
1174
|
const args = [...argv];
|
|
10
1175
|
let projectName = null;
|
|
11
1176
|
let template = "express";
|
|
1177
|
+
let preset = null;
|
|
12
1178
|
let skipInstall = false;
|
|
13
1179
|
let force = false;
|
|
1180
|
+
let templateExplicit = false;
|
|
14
1181
|
for (let i = 0; i < args.length; i++) {
|
|
15
1182
|
const arg = args[i];
|
|
16
1183
|
if (arg === "--skip-install") {
|
|
@@ -21,6 +1188,7 @@ function parseArgs(argv) {
|
|
|
21
1188
|
const val = args[i + 1];
|
|
22
1189
|
if (val === "express" || val === "fastify" || val === "nextjs") {
|
|
23
1190
|
template = val;
|
|
1191
|
+
templateExplicit = true;
|
|
24
1192
|
i++;
|
|
25
1193
|
} else {
|
|
26
1194
|
console.error(`Unknown template: ${val}. Available: express, fastify, nextjs`);
|
|
@@ -30,36 +1198,49 @@ function parseArgs(argv) {
|
|
|
30
1198
|
const val = arg.slice("--template=".length);
|
|
31
1199
|
if (val === "express" || val === "fastify" || val === "nextjs") {
|
|
32
1200
|
template = val;
|
|
1201
|
+
templateExplicit = true;
|
|
33
1202
|
} else {
|
|
34
1203
|
console.error(`Unknown template: ${val}. Available: express, fastify, nextjs`);
|
|
35
1204
|
process.exit(1);
|
|
36
1205
|
}
|
|
1206
|
+
} else if (arg === "--preset") {
|
|
1207
|
+
preset = args[i + 1] || null;
|
|
1208
|
+
i++;
|
|
1209
|
+
} else if (arg.startsWith("--preset=")) {
|
|
1210
|
+
preset = arg.slice("--preset=".length) || null;
|
|
37
1211
|
} else if (!arg.startsWith("--")) {
|
|
38
1212
|
projectName = arg;
|
|
39
1213
|
}
|
|
40
1214
|
}
|
|
41
|
-
|
|
1215
|
+
if (preset && templateExplicit) {
|
|
1216
|
+
console.error("Error: --template and --preset cannot be used together. Use one or the other.");
|
|
1217
|
+
process.exit(1);
|
|
1218
|
+
}
|
|
1219
|
+
return { projectName, template, preset, skipInstall, force };
|
|
42
1220
|
}
|
|
43
1221
|
function printUsage() {
|
|
44
1222
|
console.log("Usage: create-stratum <project-name> [options]");
|
|
45
1223
|
console.log("");
|
|
46
1224
|
console.log("Options:");
|
|
47
1225
|
console.log(" --template <express|fastify|nextjs> Framework template (default: express)");
|
|
1226
|
+
console.log(" --preset <db-strategy-orm-framework> Full stack preset (e.g. postgres-rls-prisma-express)");
|
|
48
1227
|
console.log(" --skip-install Skip npm install");
|
|
49
1228
|
console.log(" --force Overwrite existing directory");
|
|
50
1229
|
console.log("");
|
|
51
1230
|
console.log("Examples:");
|
|
52
1231
|
console.log(" npx @stratum-hq/create my-app");
|
|
53
1232
|
console.log(" npx @stratum-hq/create my-app --template fastify");
|
|
54
|
-
console.log(" npx @stratum-hq/create my-app --
|
|
1233
|
+
console.log(" npx @stratum-hq/create my-app --preset postgres-rls-prisma-express");
|
|
1234
|
+
console.log(" npx @stratum-hq/create my-app --preset mongodb-database-mongoose-hono");
|
|
1235
|
+
console.log(" npx @stratum-hq/create my-app --preset mysql-table-prefix-sequelize-nestjs");
|
|
55
1236
|
}
|
|
56
|
-
function
|
|
57
|
-
const dir =
|
|
58
|
-
if (!
|
|
59
|
-
|
|
1237
|
+
function writeFile2(filePath, content) {
|
|
1238
|
+
const dir = path2.dirname(filePath);
|
|
1239
|
+
if (!fs2.existsSync(dir)) {
|
|
1240
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
60
1241
|
}
|
|
61
|
-
|
|
62
|
-
console.log(` created ${
|
|
1242
|
+
fs2.writeFileSync(filePath, content, "utf8");
|
|
1243
|
+
console.log(` created ${path2.relative(process.cwd(), filePath)}`);
|
|
63
1244
|
}
|
|
64
1245
|
function generatePackageJson(projectName, template) {
|
|
65
1246
|
const frameworkDeps = {
|
|
@@ -71,11 +1252,11 @@ function generatePackageJson(projectName, template) {
|
|
|
71
1252
|
fastify: "^4.26.0"
|
|
72
1253
|
},
|
|
73
1254
|
nextjs: {
|
|
74
|
-
next: "^
|
|
75
|
-
react: "^
|
|
76
|
-
"react-dom": "^
|
|
77
|
-
"@types/react": "^
|
|
78
|
-
"@types/react-dom": "^
|
|
1255
|
+
next: "^15.5.16",
|
|
1256
|
+
react: "^19.0.0",
|
|
1257
|
+
"react-dom": "^19.0.0",
|
|
1258
|
+
"@types/react": "^19.0.0",
|
|
1259
|
+
"@types/react-dom": "^19.0.0"
|
|
79
1260
|
}
|
|
80
1261
|
};
|
|
81
1262
|
const deps = {
|
|
@@ -145,6 +1326,7 @@ COMMENT ON DATABASE ${dbName} IS 'Multi-tenant database for ${projectName}';
|
|
|
145
1326
|
}
|
|
146
1327
|
function generateEnv(projectName) {
|
|
147
1328
|
const dbName = projectName.replace(/[^a-z0-9]/gi, "_").toLowerCase();
|
|
1329
|
+
const jwtSecret = crypto2.randomBytes(32).toString("base64url");
|
|
148
1330
|
return `# Environment variables for ${projectName}
|
|
149
1331
|
# Copy to .env and fill in values
|
|
150
1332
|
|
|
@@ -152,7 +1334,7 @@ function generateEnv(projectName) {
|
|
|
152
1334
|
DATABASE_URL=postgres://${dbName}:dev_password@localhost:5432/${dbName}
|
|
153
1335
|
|
|
154
1336
|
# Authentication
|
|
155
|
-
JWT_SECRET
|
|
1337
|
+
JWT_SECRET=${jwtSecret}
|
|
156
1338
|
|
|
157
1339
|
# Stratum Control Plane
|
|
158
1340
|
STRATUM_URL=http://localhost:3001
|
|
@@ -245,7 +1427,7 @@ export default function Home() {
|
|
|
245
1427
|
}
|
|
246
1428
|
`;
|
|
247
1429
|
}
|
|
248
|
-
function
|
|
1430
|
+
function generateNextjsMiddleware2() {
|
|
249
1431
|
return `// middleware.ts \u2014 tenant resolution via subdomain or header
|
|
250
1432
|
import { NextRequest, NextResponse } from "next/server";
|
|
251
1433
|
|
|
@@ -329,26 +1511,26 @@ function createProject(projectName, template, targetDir, skipInstall) {
|
|
|
329
1511
|
console.log(`
|
|
330
1512
|
Creating ${projectName} with ${template} template...
|
|
331
1513
|
`);
|
|
332
|
-
|
|
333
|
-
|
|
1514
|
+
writeFile2(
|
|
1515
|
+
path2.join(targetDir, "package.json"),
|
|
334
1516
|
generatePackageJson(projectName, template)
|
|
335
1517
|
);
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
1518
|
+
writeFile2(path2.join(targetDir, "docker-compose.yml"), generateDockerCompose(projectName));
|
|
1519
|
+
writeFile2(path2.join(targetDir, "init.sql"), generateInitSql(projectName));
|
|
1520
|
+
writeFile2(path2.join(targetDir, ".env.example"), generateEnv(projectName));
|
|
339
1521
|
if (template === "express") {
|
|
340
|
-
|
|
1522
|
+
writeFile2(path2.join(targetDir, "src", "index.ts"), generateExpressServer(projectName));
|
|
341
1523
|
} else if (template === "fastify") {
|
|
342
|
-
|
|
1524
|
+
writeFile2(path2.join(targetDir, "src", "index.ts"), generateFastifyServer(projectName));
|
|
343
1525
|
} else if (template === "nextjs") {
|
|
344
|
-
|
|
345
|
-
|
|
1526
|
+
writeFile2(path2.join(targetDir, "src", "app", "page.tsx"), generateNextjsPage(projectName));
|
|
1527
|
+
writeFile2(path2.join(targetDir, "middleware.ts"), generateNextjsMiddleware2());
|
|
346
1528
|
}
|
|
347
|
-
|
|
1529
|
+
writeFile2(path2.join(targetDir, "README.md"), generateReadme(projectName, template));
|
|
348
1530
|
if (!skipInstall) {
|
|
349
1531
|
console.log("\nInstalling dependencies...\n");
|
|
350
1532
|
try {
|
|
351
|
-
|
|
1533
|
+
execSync2("npm install", { cwd: targetDir, stdio: "inherit" });
|
|
352
1534
|
} catch {
|
|
353
1535
|
console.warn("\nWarning: npm install failed. Run it manually in the project directory.");
|
|
354
1536
|
}
|
|
@@ -362,14 +1544,14 @@ function validateProjectName(projectName) {
|
|
|
362
1544
|
if (!VALID_PROJECT_NAME.test(projectName)) {
|
|
363
1545
|
return `"${projectName}" is not a valid project name. Use only letters, numbers, dots, hyphens, and underscores. Must start with a letter or number.`;
|
|
364
1546
|
}
|
|
365
|
-
const targetDir =
|
|
366
|
-
if (
|
|
1547
|
+
const targetDir = path2.resolve(process.cwd(), projectName);
|
|
1548
|
+
if (path2.dirname(targetDir) !== process.cwd()) {
|
|
367
1549
|
return "Project name must not contain path separators.";
|
|
368
1550
|
}
|
|
369
1551
|
return null;
|
|
370
1552
|
}
|
|
371
1553
|
function main(argv) {
|
|
372
|
-
const { projectName, template, skipInstall, force } = parseArgs(argv);
|
|
1554
|
+
const { projectName, template, preset, skipInstall, force } = parseArgs(argv);
|
|
373
1555
|
if (!projectName) {
|
|
374
1556
|
printUsage();
|
|
375
1557
|
process.exit(1);
|
|
@@ -379,17 +1561,34 @@ function main(argv) {
|
|
|
379
1561
|
console.error(`Error: ${validationError}`);
|
|
380
1562
|
process.exit(1);
|
|
381
1563
|
}
|
|
382
|
-
const targetDir =
|
|
383
|
-
if (
|
|
1564
|
+
const targetDir = path2.resolve(process.cwd(), projectName);
|
|
1565
|
+
if (fs2.existsSync(targetDir) && !force) {
|
|
384
1566
|
console.error(`Error: Directory "${projectName}" already exists.`);
|
|
385
1567
|
console.error(`Use --force to overwrite.`);
|
|
386
1568
|
process.exit(1);
|
|
387
1569
|
}
|
|
388
|
-
if (
|
|
389
|
-
|
|
1570
|
+
if (fs2.existsSync(targetDir) && force) {
|
|
1571
|
+
fs2.rmSync(targetDir, { recursive: true, force: true });
|
|
1572
|
+
}
|
|
1573
|
+
fs2.mkdirSync(targetDir, { recursive: true });
|
|
1574
|
+
if (preset) {
|
|
1575
|
+
const parsed = parsePresetString(preset);
|
|
1576
|
+
if (!parsed) {
|
|
1577
|
+
console.error(`Error: Invalid preset string "${preset}".`);
|
|
1578
|
+
console.error("Format: {database}-{strategy}-{orm}-{framework}");
|
|
1579
|
+
console.error("Example: postgres-rls-prisma-express, mongodb-database-mongoose-hono");
|
|
1580
|
+
process.exit(1);
|
|
1581
|
+
}
|
|
1582
|
+
if (!isValidPreset(parsed)) {
|
|
1583
|
+
console.error(`Error: Invalid preset combination "${preset}".`);
|
|
1584
|
+
console.error(`${parsed.database} does not support ${parsed.strategy}/${parsed.orm} together.`);
|
|
1585
|
+
console.error("Run with --help to see valid combinations.");
|
|
1586
|
+
process.exit(1);
|
|
1587
|
+
}
|
|
1588
|
+
createPresetProject(projectName, parsed, targetDir, skipInstall);
|
|
1589
|
+
} else {
|
|
1590
|
+
createProject(projectName, template, targetDir, skipInstall);
|
|
390
1591
|
}
|
|
391
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
392
|
-
createProject(projectName, template, targetDir, skipInstall);
|
|
393
1592
|
console.log(`
|
|
394
1593
|
Success! Created ${projectName} at ${targetDir}
|
|
395
1594
|
`);
|
|
@@ -400,7 +1599,16 @@ Success! Created ${projectName} at ${targetDir}
|
|
|
400
1599
|
if (skipInstall) {
|
|
401
1600
|
console.log(" npm install");
|
|
402
1601
|
}
|
|
403
|
-
if (
|
|
1602
|
+
if (preset) {
|
|
1603
|
+
const parsed = parsePresetString(preset);
|
|
1604
|
+
if (parsed.framework === "nextjs") {
|
|
1605
|
+
console.log(" npm run dev");
|
|
1606
|
+
} else if (parsed.framework === "nestjs") {
|
|
1607
|
+
console.log(" node --env-file=.env src/main.ts");
|
|
1608
|
+
} else {
|
|
1609
|
+
console.log(" node --env-file=.env src/index.ts");
|
|
1610
|
+
}
|
|
1611
|
+
} else if (template === "nextjs") {
|
|
404
1612
|
console.log(" npm run dev");
|
|
405
1613
|
} else {
|
|
406
1614
|
console.log(" node --env-file=.env src/index.ts");
|