@stratum-hq/create 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -174,12 +174,22 @@ function postgresAppRole(dbName) {
174
174
  return `${dbName}_app`;
175
175
  }
176
176
  var POSTGRES_APP_PASSWORD = "dev_app_password";
177
+ function postgresStratumRole(dbName) {
178
+ return `${dbName}_stratum`;
179
+ }
180
+ var POSTGRES_STRATUM_PASSWORD = "dev_stratum_password";
177
181
  function postgresAppRoleSql(dbName, strategy) {
178
182
  const role = postgresAppRole(dbName);
183
+ const stratum = postgresStratumRole(dbName);
179
184
  let strategyGrant = "";
180
185
  if (strategy === "schema") {
181
186
  strategyGrant = `
182
- -- schema-per-tenant: the app creates one schema per tenant
187
+ -- schema-per-tenant: the app creates one schema per tenant. A schema named
188
+ -- like a login comes first on that login's default search path ("$user",
189
+ -- public), so a schema the app creates could come before public for the
190
+ -- Stratum login or the bootstrap superuser. Both search only public.
191
+ ALTER ROLE ${stratum} IN DATABASE ${dbName} SET search_path = public;
192
+ ALTER ROLE CURRENT_USER IN DATABASE ${dbName} SET search_path = public;
183
193
  GRANT CREATE ON DATABASE ${dbName} TO ${role};
184
194
  `;
185
195
  } else if (strategy === "database") {
@@ -189,15 +199,40 @@ ALTER ROLE ${role} CREATEDB;
189
199
  `;
190
200
  }
191
201
  return `
202
+ -- Stratum's control role (migration 032 of @stratum-hq/lib). Every Stratum
203
+ -- table gets a policy for it, and only its members reach rows across
204
+ -- tenants.
205
+ CREATE ROLE stratum_control NOLOGIN NOSUPERUSER NOBYPASSRLS;
206
+ GRANT USAGE, CREATE ON SCHEMA public TO stratum_control;
207
+
208
+ -- Stratum's own login: pass it to the library as adminPool, which runs the
209
+ -- Stratum migrations, so it owns the Stratum tables. Not a superuser and no
210
+ -- BYPASSRLS: it reaches them as a member of stratum_control. Do not run the
211
+ -- Stratum migrations as the bootstrap superuser: the default privileges
212
+ -- below would give the app role write access to the Stratum tables. If that
213
+ -- happened, "stratum db roles --apply --app-role ${role}" removes it.
214
+ CREATE ROLE ${stratum} WITH LOGIN PASSWORD '${POSTGRES_STRATUM_PASSWORD}' NOSUPERUSER NOBYPASSRLS;
215
+ GRANT stratum_control TO ${stratum} WITH INHERIT TRUE, SET TRUE;
216
+ GRANT CONNECT ON DATABASE ${dbName} TO ${stratum};
217
+ GRANT USAGE, CREATE ON SCHEMA public TO ${stratum};
218
+
192
219
  -- Application role. The app connects as ${role} (DATABASE_URL), never as the
193
- -- bootstrap superuser (DATABASE_ADMIN_URL): a superuser or BYPASSRLS role
194
- -- ignores every row-level security policy, FORCE included. Use the superuser
195
- -- only for bootstrap and migrations.
220
+ -- bootstrap superuser (DATABASE_SUPERUSER_URL): a superuser or BYPASSRLS role
221
+ -- ignores every row-level security policy, FORCE included. It creates no
222
+ -- objects in public: the superuser creates the application's tables (its
223
+ -- migrations), and the app role reads and writes the ones the superuser
224
+ -- creates. It gets nothing on the tables ${stratum} creates; to let it read
225
+ -- Stratum's read list once Stratum has migrated, run:
226
+ -- stratum db roles --apply --admin-role ${stratum} --app-role ${role}
196
227
  CREATE ROLE ${role} WITH LOGIN PASSWORD '${POSTGRES_APP_PASSWORD}' NOSUPERUSER NOBYPASSRLS NOCREATEROLE;
197
228
  GRANT CONNECT ON DATABASE ${dbName} TO ${role};
198
- GRANT USAGE, CREATE ON SCHEMA public TO ${role};
199
- ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${role};
200
- ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO ${role};
229
+ GRANT USAGE ON SCHEMA public TO ${role};
230
+ ALTER DEFAULT PRIVILEGES FOR ROLE ${dbName} IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${role};
231
+ ALTER DEFAULT PRIVILEGES FOR ROLE ${dbName} IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO ${role};
232
+
233
+ -- Only roles granted CREATE by name create objects in public. PostgreSQL 15
234
+ -- and later already start this way; older versions grant it to PUBLIC.
235
+ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
201
236
  ${strategyGrant}`;
202
237
  }
203
238
  function generatePresetInitSql(projectName, preset) {
@@ -376,6 +411,10 @@ export { pool };
376
411
  `
377
412
  });
378
413
  }
414
+ files.push({
415
+ filename: "src/schema.ts",
416
+ content: drizzleSchema(preset)
417
+ });
379
418
  files.push({
380
419
  filename: "drizzle.config.ts",
381
420
  content: `import type { Config } from "drizzle-kit";
@@ -385,13 +424,44 @@ export default {
385
424
  out: "./drizzle",
386
425
  ${preset.database === "mysql" ? 'dialect: "mysql",' : 'dialect: "postgresql",'}
387
426
  dbCredentials: {
388
- url: process.env.DATABASE_URL!,
427
+ url: ${preset.database === "mysql" ? "process.env.DATABASE_URL!" : "(process.env.DATABASE_SUPERUSER_URL ?? process.env.DATABASE_URL)!"},
389
428
  },
390
429
  } satisfies Config;
391
430
  `
392
431
  });
393
432
  return files;
394
433
  }
434
+ function drizzleSchema(preset) {
435
+ if (preset.database === "mysql") {
436
+ return `// Drizzle table definitions. drizzle.config.ts reads this file.
437
+ import { mysqlTable, varchar, text, timestamp } from "drizzle-orm/mysql-core";
438
+
439
+ // An example tenant-scoped table: every row carries the tenant it belongs to.
440
+ export const notes = mysqlTable("notes", {
441
+ id: varchar("id", { length: 36 }).primaryKey(),
442
+ tenantId: varchar("tenant_id", { length: 36 }).notNull(),
443
+ body: text("body").notNull(),
444
+ createdAt: timestamp("created_at").notNull().defaultNow(),
445
+ });
446
+ `;
447
+ }
448
+ return `// Drizzle table definitions. drizzle.config.ts reads this file.
449
+ import { pgTable, uuid, text, timestamp, index } from "drizzle-orm/pg-core";
450
+
451
+ // An example tenant-scoped table: every row carries the tenant it belongs to.
452
+ // Stratum's own tables, such as tenants, are created by Stratum, not here.
453
+ export const notes = pgTable(
454
+ "notes",
455
+ {
456
+ id: uuid("id").primaryKey().defaultRandom(),
457
+ tenantId: uuid("tenant_id").notNull(),
458
+ body: text("body").notNull(),
459
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
460
+ },
461
+ (table) => [index("notes_tenant_id_idx").on(table.tenantId)],
462
+ );
463
+ `;
464
+ }
395
465
  function generateSequelizeSetup(preset) {
396
466
  const dialect = preset.database === "mysql" ? "mysql" : "postgres";
397
467
  return [
@@ -642,8 +712,10 @@ async function verifiedTenant(
642
712
  var INVALID_TOKEN = `{ error: "Bearer token is invalid or has no tenant_id claim" }`;
643
713
  var TENANT_REQUIRED = `{ error: "A bearer token with a tenant_id claim is required" }`;
644
714
  function nextjsTenantMiddleware() {
645
- return `// middleware.ts (place in project root)
646
- // Next.js middleware for Stratum tenant resolution
715
+ return `// src/middleware.ts: Next.js middleware for Stratum tenant resolution
716
+ //
717
+ // Next.js runs middleware only from the directory that holds the app
718
+ // directory: src/middleware.ts for src/app, middleware.ts for app.
647
719
  //
648
720
  // The tenant ID comes only from the tenant_id claim of a bearer token that
649
721
  // verifies with JWT_SECRET, and is forwarded as x-tenant-id. Any copy of the
@@ -718,6 +790,19 @@ export const config = {
718
790
  };
719
791
  `;
720
792
  }
793
+ function nextjsRootLayout(projectName) {
794
+ return `// app/layout.tsx: ${projectName} root layout
795
+ import type { ReactNode } from "react";
796
+
797
+ export default function RootLayout({ children }: { children: ReactNode }) {
798
+ return (
799
+ <html lang="en">
800
+ <body>{children}</body>
801
+ </html>
802
+ );
803
+ }
804
+ `;
805
+ }
721
806
  function generateMiddleware(projectName, preset) {
722
807
  switch (preset.framework) {
723
808
  case "express":
@@ -735,10 +820,10 @@ function generateMiddleware(projectName, preset) {
735
820
  }
736
821
  }
737
822
  function generateExpressMiddleware(projectName) {
738
- return [
739
- {
740
- filename: "src/index.ts",
741
- content: `import express from "express";
823
+ return [{ filename: "src/index.ts", content: expressServer(projectName) }];
824
+ }
825
+ function expressServer(projectName) {
826
+ return `import express from "express";
742
827
  ${VERIFIED_TENANT}
743
828
 
744
829
  const app = express();
@@ -774,15 +859,13 @@ app.get("/tenants", async (req, res) => {
774
859
  app.listen(port, () => {
775
860
  console.log(\`${projectName} running on http://localhost:\${port}\`);
776
861
  });
777
- `
778
- }
779
- ];
862
+ `;
780
863
  }
781
864
  function generateFastifyMiddleware(projectName) {
782
- return [
783
- {
784
- filename: "src/index.ts",
785
- content: `import Fastify from "fastify";
865
+ return [{ filename: "src/index.ts", content: fastifyServer(projectName) }];
866
+ }
867
+ function fastifyServer(projectName) {
868
+ return `import Fastify from "fastify";
786
869
  ${VERIFIED_TENANT}
787
870
 
788
871
  const fastify = Fastify({ logger: true });
@@ -817,16 +900,18 @@ fastify.listen({ port, host: "0.0.0.0" }, (err) => {
817
900
  process.exit(1);
818
901
  }
819
902
  });
820
- `
821
- }
822
- ];
903
+ `;
823
904
  }
824
905
  function generateNextjsMiddleware(projectName) {
825
906
  return [
826
907
  {
827
- filename: "middleware.ts",
908
+ filename: "src/middleware.ts",
828
909
  content: nextjsTenantMiddleware()
829
910
  },
911
+ {
912
+ filename: "src/app/layout.tsx",
913
+ content: nextjsRootLayout(projectName)
914
+ },
830
915
  {
831
916
  filename: "src/app/page.tsx",
832
917
  content: `// app/page.tsx - ${projectName} root page
@@ -837,7 +922,7 @@ export default function Home() {
837
922
  <p>Multi-tenant app powered by Stratum.</p>
838
923
  <ul>
839
924
  <li>Configure tenants via the Stratum control plane</li>
840
- <li>The tenant comes from a verified JWT in <code>middleware.ts</code></li>
925
+ <li>The tenant comes from a verified JWT in <code>src/middleware.ts</code></li>
841
926
  <li>Use <code>@stratum-hq/lib</code> for tenant resolution</li>
842
927
  </ul>
843
928
  </main>
@@ -990,18 +1075,19 @@ function generateTsconfig(framework, extraSources = []) {
990
1075
  {
991
1076
  compilerOptions: {
992
1077
  target: "ESNext",
993
- module: "NodeNext",
994
- moduleResolution: "NodeNext",
1078
+ // Next.js resolves imports like a bundler: "next/server" has no file
1079
+ // extension, which NodeNext refuses in an ES module package.
1080
+ ...framework === "nextjs" ? { module: "ESNext", moduleResolution: "Bundler" } : { module: "NodeNext", moduleResolution: "NodeNext" },
995
1081
  strict: true,
996
- outDir: "dist",
997
- rootDir,
998
- declaration: true,
1082
+ // next build compiles a Next.js app itself and type-checks the files it
1083
+ // generates under .next/types, which a rootDir of src would reject.
1084
+ ...framework === "nextjs" ? { noEmit: true } : { outDir: "dist", rootDir, declaration: true },
999
1085
  skipLibCheck: true,
1000
1086
  esModuleInterop: true,
1001
1087
  ...framework === "nestjs" ? { experimentalDecorators: true, emitDecoratorMetadata: true } : {},
1002
1088
  ...framework === "nextjs" ? { jsx: "preserve", plugins: [{ name: "next" }] } : {}
1003
1089
  },
1004
- include: ["src", ...extraSources],
1090
+ include: framework === "nextjs" ? ["next-env.d.ts", "src", ...extraSources, ".next/types/**/*.ts"] : ["src", ...extraSources],
1005
1091
  exclude: ["node_modules", "dist"]
1006
1092
  },
1007
1093
  null,
@@ -1012,14 +1098,17 @@ function generateTsconfig(framework, extraSources = []) {
1012
1098
  // ../hono/package.json
1013
1099
  var package_default = {
1014
1100
  name: "@stratum-hq/hono",
1015
- version: "1.2.0",
1016
- description: "Stratum Hono integration \u2014 tenant extraction middleware with ALS context",
1101
+ version: "1.3.2",
1102
+ description: "Hono multi-tenancy middleware: tenant resolution with AsyncLocalStorage context",
1017
1103
  keywords: [
1018
1104
  "multi-tenancy",
1105
+ "multitenancy",
1019
1106
  "multi-tenant",
1020
1107
  "saas",
1021
1108
  "tenant",
1109
+ "tenant-isolation",
1022
1110
  "hono",
1111
+ "asynclocalstorage",
1023
1112
  "middleware",
1024
1113
  "typescript",
1025
1114
  "stratum"
@@ -1063,7 +1152,7 @@ var package_default = {
1063
1152
  },
1064
1153
  license: "MIT",
1065
1154
  author: "Christian Crank",
1066
- homepage: "https://github.com/stratum-hq/Stratum/tree/main/packages/hono#readme",
1155
+ homepage: "https://docs.stratum-hq.org/packages/hono/",
1067
1156
  bugs: "https://github.com/stratum-hq/Stratum/issues",
1068
1157
  engines: {
1069
1158
  node: ">=20.0.0"
@@ -1078,18 +1167,27 @@ var package_default = {
1078
1167
  // ../db-adapters/package.json
1079
1168
  var package_default2 = {
1080
1169
  name: "@stratum-hq/db-adapters",
1081
- version: "1.2.0",
1082
- description: "Stratum PostgreSQL adapters for tenant-scoped query isolation",
1170
+ version: "1.5.0",
1171
+ description: "Multi-tenant PostgreSQL row-level security for pg, Prisma, Drizzle, and Sequelize, plus schema-per-tenant and database-per-tenant isolation",
1083
1172
  keywords: [
1084
1173
  "multi-tenancy",
1174
+ "multitenancy",
1085
1175
  "multi-tenant",
1086
1176
  "saas",
1087
1177
  "tenant",
1088
1178
  "tenant-isolation",
1089
- "postgresql",
1090
- "postgres",
1091
1179
  "row-level-security",
1092
1180
  "rls",
1181
+ "postgresql",
1182
+ "prisma",
1183
+ "drizzle",
1184
+ "drizzle-orm",
1185
+ "sequelize",
1186
+ "pg",
1187
+ "node-postgres",
1188
+ "schema-per-tenant",
1189
+ "database-per-tenant",
1190
+ "postgres",
1093
1191
  "schema-isolation",
1094
1192
  "database",
1095
1193
  "stratum"
@@ -1100,6 +1198,10 @@ var package_default2 = {
1100
1198
  ".": {
1101
1199
  types: "./dist/index.d.ts",
1102
1200
  default: "./dist/index.js"
1201
+ },
1202
+ "./pglite": {
1203
+ types: "./dist/pglite/index.d.ts",
1204
+ default: "./dist/pglite/index.js"
1103
1205
  }
1104
1206
  },
1105
1207
  files: [
@@ -1119,18 +1221,23 @@ var package_default2 = {
1119
1221
  registry: "https://registry.npmjs.org/"
1120
1222
  },
1121
1223
  dependencies: {
1122
- "@stratum-hq/core": "^1.4.0",
1224
+ "@stratum-hq/core": "^1.6.0",
1123
1225
  pg: "^8.11.0"
1124
1226
  },
1125
1227
  peerDependencies: {
1228
+ "@electric-sql/pglite": "^0.4.2",
1126
1229
  "drizzle-orm": ">=0.29.0"
1127
1230
  },
1128
1231
  peerDependenciesMeta: {
1232
+ "@electric-sql/pglite": {
1233
+ optional: true
1234
+ },
1129
1235
  "drizzle-orm": {
1130
1236
  optional: true
1131
1237
  }
1132
1238
  },
1133
1239
  devDependencies: {
1240
+ "@electric-sql/pglite": "^0.4.6",
1134
1241
  "@prisma/client": "^5.10.0",
1135
1242
  "@types/pg": "^8.11.0",
1136
1243
  "drizzle-orm": "^0.45.3",
@@ -1139,7 +1246,7 @@ var package_default2 = {
1139
1246
  },
1140
1247
  license: "MIT",
1141
1248
  author: "Christian Crank",
1142
- homepage: "https://github.com/stratum-hq/Stratum/tree/main/packages/db-adapters#readme",
1249
+ homepage: "https://docs.stratum-hq.org/packages/db-adapters/",
1143
1250
  bugs: "https://github.com/stratum-hq/Stratum/issues",
1144
1251
  engines: {
1145
1252
  node: ">=20.0.0"
@@ -1154,21 +1261,28 @@ var package_default2 = {
1154
1261
  // ../lib/package.json
1155
1262
  var package_default3 = {
1156
1263
  name: "@stratum-hq/lib",
1157
- version: "1.5.0",
1158
- description: "Stratum tenant management library - framework-agnostic business logic",
1264
+ version: "1.8.0",
1265
+ description: "Multi-tenancy for Node.js and PostgreSQL: tenant hierarchy, config inheritance, row-level security, ABAC, audit log, and GDPR tooling",
1159
1266
  keywords: [
1160
1267
  "multi-tenancy",
1268
+ "multitenancy",
1161
1269
  "multi-tenant",
1162
1270
  "saas",
1163
- "b2b",
1164
1271
  "tenant",
1165
- "tenant-management",
1166
1272
  "tenant-isolation",
1167
- "node",
1168
- "typescript",
1169
- "postgresql",
1170
1273
  "row-level-security",
1171
1274
  "rls",
1275
+ "postgresql",
1276
+ "tenant-hierarchy",
1277
+ "ltree",
1278
+ "abac",
1279
+ "audit-log",
1280
+ "encryption",
1281
+ "webhooks",
1282
+ "b2b",
1283
+ "tenant-management",
1284
+ "node",
1285
+ "typescript",
1172
1286
  "schema-isolation",
1173
1287
  "config-inheritance",
1174
1288
  "gdpr",
@@ -1190,7 +1304,7 @@ var package_default3 = {
1190
1304
  scripts: {
1191
1305
  build: "tsc && node ../../scripts/copy-dir.mjs src/migrations dist/migrations",
1192
1306
  test: "vitest run",
1193
- typecheck: "tsc --noEmit",
1307
+ typecheck: "tsc --noEmit && tsc -p tsconfig.types.json",
1194
1308
  lint: "eslint .",
1195
1309
  clean: "rm -rf dist"
1196
1310
  },
@@ -1199,10 +1313,10 @@ var package_default3 = {
1199
1313
  registry: "https://registry.npmjs.org/"
1200
1314
  },
1201
1315
  dependencies: {
1202
- "@opentelemetry/api": "^1.9.0",
1203
- "@stratum-hq/core": "^1.5.0",
1204
- "@stratum-hq/db-adapters": "^1.2.0",
1205
- "@stratum-hq/sdk": "^1.3.0",
1316
+ "@opentelemetry/api": "^1.0.0",
1317
+ "@stratum-hq/core": "^1.6.0",
1318
+ "@stratum-hq/db-adapters": "^1.5.0",
1319
+ "@stratum-hq/sdk": "^1.4.0",
1206
1320
  pg: "^8.11.0"
1207
1321
  },
1208
1322
  peerDependencies: {
@@ -1214,13 +1328,14 @@ var package_default3 = {
1214
1328
  }
1215
1329
  },
1216
1330
  devDependencies: {
1331
+ "@electric-sql/pglite": "^0.4.6",
1217
1332
  "@types/pg": "^8.11.0",
1218
1333
  typescript: "^5.4.0",
1219
1334
  vitest: "^4.1.11"
1220
1335
  },
1221
1336
  license: "MIT",
1222
1337
  author: "Christian Crank",
1223
- homepage: "https://github.com/stratum-hq/Stratum/tree/main/packages/lib#readme",
1338
+ homepage: "https://docs.stratum-hq.org/packages/lib/",
1224
1339
  bugs: "https://github.com/stratum-hq/Stratum/issues",
1225
1340
  engines: {
1226
1341
  node: ">=20.0.0"
@@ -1235,10 +1350,11 @@ var package_default3 = {
1235
1350
  // ../mongodb/package.json
1236
1351
  var package_default4 = {
1237
1352
  name: "@stratum-hq/mongodb",
1238
- version: "0.5.0",
1239
- description: "MongoDB tenant isolation for Stratum \u2014 shared collection, collection-per-tenant, and database-per-tenant strategies",
1353
+ version: "0.7.0",
1354
+ description: "MongoDB tenant isolation for Stratum: shared collection, collection-per-tenant, and database-per-tenant strategies",
1240
1355
  keywords: [
1241
1356
  "multi-tenancy",
1357
+ "multitenancy",
1242
1358
  "multi-tenant",
1243
1359
  "saas",
1244
1360
  "tenant",
@@ -1265,17 +1381,18 @@ var package_default4 = {
1265
1381
  build: "tsc",
1266
1382
  test: "vitest run",
1267
1383
  "test:watch": "vitest",
1268
- "test:integration": "vitest run --config vitest.integration.config.ts",
1269
- typecheck: "tsc --noEmit",
1384
+ "test:integration": "vitest run --config vitest.integration.config.ts && vitest run --config vitest.integration.mongoose9.config.ts",
1385
+ typecheck: "tsc --noEmit && tsc -p tsconfig.types.json",
1270
1386
  lint: "eslint .",
1271
1387
  clean: "rm -rf dist"
1272
1388
  },
1273
1389
  dependencies: {
1274
- "@stratum-hq/core": "^1.5.0",
1275
- "@stratum-hq/sdk": "^1.3.0"
1390
+ "@stratum-hq/core": "^1.6.0",
1391
+ "@stratum-hq/sdk": "^1.4.0"
1276
1392
  },
1277
1393
  peerDependencies: {
1278
- mongodb: "^6.0.0 || ^7.0.0"
1394
+ mongodb: "^6.0.0 || ^7.0.0",
1395
+ mongoose: "^8.0.0 || ^9.0.0"
1279
1396
  },
1280
1397
  peerDependenciesMeta: {
1281
1398
  mongoose: {
@@ -1283,9 +1400,14 @@ var package_default4 = {
1283
1400
  }
1284
1401
  },
1285
1402
  devDependencies: {
1403
+ "@stratum-hq/lib": "*",
1286
1404
  "@stratum-hq/test-utils": "*",
1405
+ "@types/express": "^4.17.0",
1406
+ "@types/jsonwebtoken": "^9.0.0",
1407
+ "@types/pg": "^8.11.0",
1287
1408
  mongodb: "^7.6.0",
1288
1409
  mongoose: "^8.0.0",
1410
+ mongoose9: "npm:mongoose@^9.10.3",
1289
1411
  typescript: "^5.9.0",
1290
1412
  vitest: "^4.1.11"
1291
1413
  },
@@ -1294,7 +1416,7 @@ var package_default4 = {
1294
1416
  },
1295
1417
  license: "MIT",
1296
1418
  author: "Christian Crank",
1297
- homepage: "https://github.com/stratum-hq/Stratum/tree/main/packages/mongodb#readme",
1419
+ homepage: "https://docs.stratum-hq.org/packages/mongodb/",
1298
1420
  bugs: "https://github.com/stratum-hq/Stratum/issues",
1299
1421
  engines: {
1300
1422
  node: ">=20.0.0"
@@ -1309,10 +1431,11 @@ var package_default4 = {
1309
1431
  // ../mysql/package.json
1310
1432
  var package_default5 = {
1311
1433
  name: "@stratum-hq/mysql",
1312
- version: "0.5.0",
1313
- description: "MySQL tenant isolation for Stratum \u2014 shared table, table-per-tenant, and database-per-tenant strategies",
1434
+ version: "0.7.0",
1435
+ description: "MySQL tenant isolation for Stratum: shared table, table-per-tenant, and database-per-tenant strategies",
1314
1436
  keywords: [
1315
1437
  "multi-tenancy",
1438
+ "multitenancy",
1316
1439
  "multi-tenant",
1317
1440
  "saas",
1318
1441
  "tenant",
@@ -1342,13 +1465,13 @@ var package_default5 = {
1342
1465
  test: "vitest run",
1343
1466
  "test:watch": "vitest",
1344
1467
  "test:integration": "vitest run --config vitest.integration.config.ts",
1345
- typecheck: "tsc --noEmit",
1468
+ typecheck: "tsc --noEmit && tsc -p tsconfig.types.json",
1346
1469
  lint: "eslint .",
1347
1470
  clean: "rm -rf dist"
1348
1471
  },
1349
1472
  dependencies: {
1350
- "@stratum-hq/core": "^1.5.0",
1351
- "@stratum-hq/sdk": "^1.3.0"
1473
+ "@stratum-hq/core": "^1.6.0",
1474
+ "@stratum-hq/sdk": "^1.4.0"
1352
1475
  },
1353
1476
  peerDependencies: {
1354
1477
  mysql2: "^3.0.0",
@@ -1368,8 +1491,11 @@ var package_default5 = {
1368
1491
  }
1369
1492
  },
1370
1493
  devDependencies: {
1494
+ "@stratum-hq/lib": "*",
1495
+ "@types/pg": "^8.11.0",
1371
1496
  knex: "^3.3.0",
1372
1497
  mysql2: "^3.24.4",
1498
+ sequelize: "^6.37.8",
1373
1499
  typeorm: "^1.1.1",
1374
1500
  typescript: "^5.9.0",
1375
1501
  vitest: "^4.1.11"
@@ -1379,7 +1505,7 @@ var package_default5 = {
1379
1505
  },
1380
1506
  license: "MIT",
1381
1507
  author: "Christian Crank",
1382
- homepage: "https://github.com/stratum-hq/Stratum/tree/main/packages/mysql#readme",
1508
+ homepage: "https://docs.stratum-hq.org/packages/mysql/",
1383
1509
  bugs: "https://github.com/stratum-hq/Stratum/issues",
1384
1510
  engines: {
1385
1511
  node: ">=20.0.0"
@@ -1394,13 +1520,15 @@ var package_default5 = {
1394
1520
  // ../nestjs/package.json
1395
1521
  var package_default6 = {
1396
1522
  name: "@stratum-hq/nestjs",
1397
- version: "1.3.0",
1398
- description: "Stratum NestJS integration \u2014 StratumGuard, @Tenant() decorator, and StratumModule",
1523
+ version: "1.3.2",
1524
+ description: "NestJS multi-tenancy: tenant guard, @Tenant() decorator, and module for Stratum",
1399
1525
  keywords: [
1400
1526
  "multi-tenancy",
1527
+ "multitenancy",
1401
1528
  "multi-tenant",
1402
1529
  "saas",
1403
1530
  "tenant",
1531
+ "tenant-isolation",
1404
1532
  "nestjs",
1405
1533
  "nest",
1406
1534
  "guard",
@@ -1444,6 +1572,7 @@ var package_default6 = {
1444
1572
  devDependencies: {
1445
1573
  "@nestjs/common": "^12.1.2",
1446
1574
  "@nestjs/core": "^12.1.2",
1575
+ "@nestjs/platform-express": "^12.1.2",
1447
1576
  "@nestjs/testing": "^12.1.2",
1448
1577
  "@stratum-hq/core": "*",
1449
1578
  "@stratum-hq/sdk": "*",
@@ -1453,7 +1582,7 @@ var package_default6 = {
1453
1582
  },
1454
1583
  license: "MIT",
1455
1584
  author: "Christian Crank",
1456
- homepage: "https://github.com/stratum-hq/Stratum/tree/main/packages/nestjs#readme",
1585
+ homepage: "https://docs.stratum-hq.org/packages/nestjs/",
1457
1586
  bugs: "https://github.com/stratum-hq/Stratum/issues",
1458
1587
  engines: {
1459
1588
  node: ">=20.0.0"
@@ -1476,6 +1605,9 @@ var STRATUM_RANGES = {
1476
1605
  };
1477
1606
 
1478
1607
  // src/generators/package-json.ts
1608
+ var DRIZZLE_KIT_OVERRIDES = {
1609
+ "@esbuild-kit/core-utils": { esbuild: "^0.25.4" }
1610
+ };
1479
1611
  function generatePresetPackageJson(projectName, preset) {
1480
1612
  const deps = {
1481
1613
  "@stratum-hq/lib": STRATUM_RANGES["@stratum-hq/lib"]
@@ -1487,7 +1619,7 @@ function generatePresetPackageJson(projectName, preset) {
1487
1619
  if (preset.framework === "nestjs") {
1488
1620
  devDeps["tsc-watch"] = "^7.2.0";
1489
1621
  } else if (preset.framework !== "nextjs") {
1490
- devDeps["tsx"] = "^4.7.0";
1622
+ devDeps["tsx"] = "^4.19.3";
1491
1623
  }
1492
1624
  addDatabaseDeps(deps, devDeps, preset);
1493
1625
  addOrmDeps(deps, devDeps, preset);
@@ -1503,6 +1635,7 @@ function generatePresetPackageJson(projectName, preset) {
1503
1635
  scripts,
1504
1636
  dependencies: sortKeys(deps),
1505
1637
  devDependencies: sortKeys(devDeps),
1638
+ ...preset.orm === "drizzle" ? { overrides: DRIZZLE_KIT_OVERRIDES } : {},
1506
1639
  engines: {
1507
1640
  node: ">=20.0.0"
1508
1641
  }
@@ -1534,7 +1667,7 @@ function addDatabaseDeps(deps, devDeps, preset) {
1534
1667
  break;
1535
1668
  case "mysql":
1536
1669
  if (preset.orm === "drizzle" || preset.orm === "pg") {
1537
- deps["mysql2"] = "^3.9.0";
1670
+ deps["mysql2"] = "^3.23.1";
1538
1671
  }
1539
1672
  break;
1540
1673
  }
@@ -1549,8 +1682,8 @@ function addOrmDeps(deps, devDeps, preset) {
1549
1682
  }
1550
1683
  break;
1551
1684
  case "drizzle":
1552
- deps["drizzle-orm"] = "^0.30.0";
1553
- devDeps["drizzle-kit"] = "^0.21.0";
1685
+ deps["drizzle-orm"] = "^0.45.3";
1686
+ devDeps["drizzle-kit"] = "^0.31.11";
1554
1687
  if (preset.database === "postgres") {
1555
1688
  deps["pg"] = "^8.11.0";
1556
1689
  }
@@ -1558,7 +1691,7 @@ function addOrmDeps(deps, devDeps, preset) {
1558
1691
  case "sequelize":
1559
1692
  deps["sequelize"] = "^6.37.0";
1560
1693
  if (preset.database === "mysql") {
1561
- deps["mysql2"] = "^3.9.0";
1694
+ deps["mysql2"] = "^3.23.1";
1562
1695
  } else {
1563
1696
  deps["pg"] = "^8.11.0";
1564
1697
  }
@@ -1567,7 +1700,7 @@ function addOrmDeps(deps, devDeps, preset) {
1567
1700
  deps["knex"] = "^3.1.0";
1568
1701
  break;
1569
1702
  case "mongoose":
1570
- deps["mongoose"] = "^8.2.0";
1703
+ deps["mongoose"] = "^8.24.1";
1571
1704
  break;
1572
1705
  case "pg":
1573
1706
  break;
@@ -1579,11 +1712,11 @@ function addFrameworkDeps(deps, devDeps, preset) {
1579
1712
  }
1580
1713
  switch (preset.framework) {
1581
1714
  case "express":
1582
- deps["express"] = "^4.18.0";
1715
+ deps["express"] = "^4.22.3";
1583
1716
  devDeps["@types/express"] = "^4.17.21";
1584
1717
  break;
1585
1718
  case "fastify":
1586
- deps["fastify"] = "^4.26.0";
1719
+ deps["fastify"] = "^5.12.5";
1587
1720
  break;
1588
1721
  case "nextjs":
1589
1722
  deps["next"] = "^15.5.16";
@@ -1593,13 +1726,13 @@ function addFrameworkDeps(deps, devDeps, preset) {
1593
1726
  devDeps["@types/react-dom"] = "^19.0.0";
1594
1727
  break;
1595
1728
  case "hono":
1596
- deps["hono"] = "^4.1.0";
1597
- deps["@hono/node-server"] = "^1.8.0";
1729
+ deps["hono"] = "^4.13.7";
1730
+ deps["@hono/node-server"] = "^1.19.15";
1598
1731
  break;
1599
1732
  case "nestjs":
1600
- deps["@nestjs/core"] = "^11.0.0";
1601
- deps["@nestjs/common"] = "^11.0.0";
1602
- deps["@nestjs/platform-express"] = "^11.0.0";
1733
+ deps["@nestjs/core"] = "^11.1.18";
1734
+ deps["@nestjs/common"] = "^11.1.18";
1735
+ deps["@nestjs/platform-express"] = "^11.1.18";
1603
1736
  deps["reflect-metadata"] = "^0.2.0";
1604
1737
  deps["rxjs"] = "^7.8.0";
1605
1738
  break;
@@ -1712,35 +1845,38 @@ npx prisma db push
1712
1845
  }
1713
1846
  if (preset.orm === "drizzle") {
1714
1847
  return `
1715
- ### 3b. Run Drizzle migrations
1848
+ ### 3b. Create the tables
1849
+
1850
+ The tables are defined in \`src/schema.ts\`, which \`drizzle.config.ts\` points at.
1851
+ drizzle-kit does not read \`.env\`, so load it first:
1716
1852
 
1717
1853
  \`\`\`bash
1718
- npx drizzle-kit push
1854
+ node --env-file=.env node_modules/drizzle-kit/bin.cjs push
1719
1855
  \`\`\`
1720
- `;
1856
+ ${preset.database === "postgres" ? "\nOn PostgreSQL drizzle-kit connects with `DATABASE_SUPERUSER_URL`, the superuser kept for migrations. The app role still reads and writes the new tables, and because it does not own them, their row-level security policies apply to it.\n" : ""}`;
1721
1857
  }
1722
1858
  return "";
1723
1859
  }
1724
1860
  function getStrategyDescription(strategy) {
1725
1861
  switch (strategy) {
1726
1862
  case "rls":
1727
- return `- **Row-Level Security** -- PostgreSQL RLS policies filter rows by tenant automatically
1863
+ return `- **Row-Level Security**: PostgreSQL RLS policies filter rows by tenant automatically
1728
1864
  - Each query sets \`app.current_tenant_id\` and RLS enforces isolation
1729
1865
  - All tenants share one database and schema`;
1730
1866
  case "schema":
1731
- return `- **Schema-per-tenant** -- each tenant gets a dedicated PostgreSQL schema
1867
+ return `- **Schema-per-tenant**: each tenant gets a dedicated PostgreSQL schema
1732
1868
  - Queries are routed to the correct schema via search_path
1733
1869
  - Shared database, isolated schemas`;
1734
1870
  case "database":
1735
- return `- **Database-per-tenant** -- each tenant gets a fully isolated database
1871
+ return `- **Database-per-tenant**: each tenant gets a fully isolated database
1736
1872
  - Connection routing directs queries to the correct database
1737
1873
  - Maximum isolation at the cost of more resource usage`;
1738
1874
  case "collection":
1739
- return `- **Collection-per-tenant** -- each tenant gets dedicated MongoDB collections
1875
+ return `- **Collection-per-tenant**: each tenant gets dedicated MongoDB collections
1740
1876
  - Collection names are prefixed or namespaced by tenant ID
1741
1877
  - Shared database, isolated collections`;
1742
1878
  case "table-prefix":
1743
- return `- **Table-prefix** -- tenant-specific tables with a naming prefix
1879
+ return `- **Table-prefix**: tenant-specific tables with a naming prefix
1744
1880
  - Tables are prefixed with the tenant identifier
1745
1881
  - Shared database, prefixed table names`;
1746
1882
  default:
@@ -1767,7 +1903,10 @@ function generatePresetEnv(projectName, preset) {
1767
1903
  dbUrl = `postgres://${postgresAppRole(dbName)}:${POSTGRES_APP_PASSWORD}@localhost:5432/${dbName}`;
1768
1904
  adminUrlLine = `
1769
1905
  # Superuser: bootstrap and migrations only. It bypasses row-level security.
1770
- DATABASE_ADMIN_URL=postgres://${dbName}:dev_password@localhost:5432/${dbName}
1906
+ DATABASE_SUPERUSER_URL=postgres://${dbName}:dev_password@localhost:5432/${dbName}
1907
+
1908
+ # Stratum's own login: the library's adminPool, which runs the Stratum migrations (see init.sql).
1909
+ STRATUM_ADMIN_DATABASE_URL=postgres://${postgresStratumRole(dbName)}:${POSTGRES_STRATUM_PASSWORD}@localhost:5432/${dbName}
1771
1910
  `;
1772
1911
  break;
1773
1912
  case "mongodb":
@@ -1917,25 +2056,25 @@ function writeFile2(filePath, content) {
1917
2056
  function generatePackageJson(projectName, template) {
1918
2057
  const frameworkDeps = {
1919
2058
  express: {
1920
- express: "^4.18.0",
2059
+ express: "^4.22.3",
1921
2060
  "@types/express": "^4.17.21"
1922
2061
  },
1923
2062
  fastify: {
1924
- fastify: "^4.26.0"
2063
+ fastify: "^5.12.5"
1925
2064
  },
1926
2065
  nextjs: {
1927
2066
  next: "^15.5.16",
1928
2067
  react: "^19.0.0",
1929
2068
  "react-dom": "^19.0.0",
1930
2069
  "@types/react": "^19.0.0",
1931
- "@types/react-dom": "^19.0.0",
1932
- // The middleware verifies the tenant JWT with jose.
1933
- jose: "^6.2.12"
2070
+ "@types/react-dom": "^19.0.0"
1934
2071
  }
1935
2072
  };
1936
2073
  const deps = {
1937
2074
  "@stratum-hq/lib": STRATUM_RANGES["@stratum-hq/lib"],
1938
2075
  pg: "^8.11.0",
2076
+ // Every template verifies the tenant JWT with jose.
2077
+ jose: "^6.2.12",
1939
2078
  ...frameworkDeps[template]
1940
2079
  };
1941
2080
  return JSON.stringify(
@@ -1952,7 +2091,7 @@ function generatePackageJson(projectName, template) {
1952
2091
  devDependencies: {
1953
2092
  typescript: "^5.3.0",
1954
2093
  "@types/node": "^20.11.0",
1955
- ...template === "nextjs" ? {} : { tsx: "^4.7.0", "@types/pg": "^8.11.0" }
2094
+ ...template === "nextjs" ? {} : { tsx: "^4.19.3", "@types/pg": "^8.11.0" }
1956
2095
  },
1957
2096
  engines: {
1958
2097
  node: ">=20.0.0"
@@ -2013,7 +2152,10 @@ function generateEnv(projectName) {
2013
2152
  DATABASE_URL=postgres://${postgresAppRole(dbName)}:${POSTGRES_APP_PASSWORD}@localhost:5432/${dbName}
2014
2153
 
2015
2154
  # Superuser: bootstrap and migrations only. It bypasses row-level security.
2016
- DATABASE_ADMIN_URL=postgres://${dbName}:dev_password@localhost:5432/${dbName}
2155
+ DATABASE_SUPERUSER_URL=postgres://${dbName}:dev_password@localhost:5432/${dbName}
2156
+
2157
+ # Stratum's own login: the library's adminPool, which runs the Stratum migrations (see init.sql).
2158
+ STRATUM_ADMIN_DATABASE_URL=postgres://${postgresStratumRole(dbName)}:${POSTGRES_STRATUM_PASSWORD}@localhost:5432/${dbName}
2017
2159
 
2018
2160
  # Authentication
2019
2161
  JWT_SECRET=${jwtSecret}
@@ -2025,75 +2167,8 @@ STRATUM_API_KEY=sk_test_your_key_here
2025
2167
  NODE_ENV=development
2026
2168
  `;
2027
2169
  }
2028
- function generateExpressServer(projectName) {
2029
- return `import express from "express";
2030
- import { Pool } from "pg";
2031
-
2032
- const app = express();
2033
- const port = process.env.PORT || 3000;
2034
-
2035
- const pool = new Pool({
2036
- connectionString: process.env.DATABASE_URL,
2037
- });
2038
-
2039
- app.use(express.json());
2040
-
2041
- app.get("/health", (_req, res) => {
2042
- res.json({ status: "ok", project: "${projectName}" });
2043
- });
2044
-
2045
- app.get("/tenants", async (_req, res) => {
2046
- try {
2047
- // Example: query your tenant table
2048
- // const { rows } = await pool.query("SELECT * FROM tenants LIMIT 20");
2049
- // res.json(rows);
2050
- res.json({ message: "Replace this with your tenant queries", pool: !!pool });
2051
- } catch (err) {
2052
- res.status(500).json({ error: String(err) });
2053
- }
2054
- });
2055
-
2056
- app.listen(port, () => {
2057
- console.log(\`${projectName} server running on http://localhost:\${port}\`);
2058
- });
2059
- `;
2060
- }
2061
- function generateFastifyServer(projectName) {
2062
- return `import Fastify from "fastify";
2063
- import { Pool } from "pg";
2064
-
2065
- const fastify = Fastify({ logger: true });
2066
- const port = Number(process.env.PORT) || 3000;
2067
-
2068
- const pool = new Pool({
2069
- connectionString: process.env.DATABASE_URL,
2070
- });
2071
-
2072
- fastify.get("/health", async () => {
2073
- return { status: "ok", project: "${projectName}" };
2074
- });
2075
-
2076
- fastify.get("/tenants", async (_request, reply) => {
2077
- try {
2078
- // Example: query your tenant table
2079
- // const { rows } = await pool.query("SELECT * FROM tenants LIMIT 20");
2080
- // return rows;
2081
- return { message: "Replace this with your tenant queries", pool: !!pool };
2082
- } catch (err) {
2083
- return reply.status(500).send({ error: String(err) });
2084
- }
2085
- });
2086
-
2087
- fastify.listen({ port, host: "0.0.0.0" }, (err) => {
2088
- if (err) {
2089
- fastify.log.error(err);
2090
- process.exit(1);
2091
- }
2092
- });
2093
- `;
2094
- }
2095
2170
  function generateNextjsPage(projectName) {
2096
- return `// app/page.tsx \u2014 ${projectName} root page
2171
+ return `// app/page.tsx: ${projectName} root page
2097
2172
  export default function Home() {
2098
2173
  return (
2099
2174
  <main style={{ padding: "2rem", fontFamily: "sans-serif" }}>
@@ -2101,7 +2176,7 @@ export default function Home() {
2101
2176
  <p>Multi-tenant app powered by Stratum.</p>
2102
2177
  <ul>
2103
2178
  <li>Configure tenants via the Stratum control plane</li>
2104
- <li>The tenant comes from a verified JWT in <code>middleware.ts</code></li>
2179
+ <li>The tenant comes from a verified JWT in <code>src/middleware.ts</code></li>
2105
2180
  <li>Use <code>@stratum-hq/lib</code> for tenant resolution</li>
2106
2181
  </ul>
2107
2182
  </main>
@@ -2126,7 +2201,7 @@ docker compose up -d
2126
2201
 
2127
2202
  \`\`\`bash
2128
2203
  cp .env.example .env
2129
- # Edit .env \u2014 update DATABASE_URL, JWT_SECRET, and STRATUM_API_KEY
2204
+ # Edit .env: update DATABASE_URL, JWT_SECRET, and STRATUM_API_KEY
2130
2205
  \`\`\`
2131
2206
 
2132
2207
  ### 3. Install dependencies
@@ -2156,9 +2231,9 @@ ${template === "nextjs" ? "" : "\u251C\u2500\u2500 tsconfig.json\n"}\u2514\u2500
2156
2231
 
2157
2232
  This project uses Stratum for hierarchical multi-tenancy:
2158
2233
 
2159
- - **Tenant resolution** \u2014 from the \`tenant_id\` claim of a bearer token verified with \`JWT_SECRET\` (see \`middleware.ts\`); the subdomain is only a display slug
2160
- - **Config inheritance** \u2014 settings flow down the tenant tree with override support
2161
- - **Permission ABAC** \u2014 role-based permissions with tenant-scoped enforcement
2234
+ ${template === "nextjs" ? "- **Tenant resolution**: from the `tenant_id` claim of a bearer token verified with `JWT_SECRET` (see `src/middleware.ts`); the subdomain is only a display slug" : "- **Tenant resolution**: the tenant middleware in `src/index.ts` takes the tenant from the `tenant_id` claim of a bearer token verified with `JWT_SECRET` (HS256, using `jose`). A token that does not verify is rejected with 401, and `GET /tenants` answers 401 without a tenant. The hostname and headers such as `x-tenant-id` are never used"}
2235
+ - **Config inheritance**: settings flow down the tenant tree with override support
2236
+ - **Permission ABAC**: role-based permissions with tenant-scoped enforcement
2162
2237
 
2163
2238
  See the [Stratum docs](https://github.com/stratum-hq/Stratum) for full reference.
2164
2239
  `;
@@ -2175,14 +2250,15 @@ Creating ${projectName} with ${template} template...
2175
2250
  writeFile2(path2.join(targetDir, "init.sql"), generateInitSql(projectName));
2176
2251
  writeFile2(path2.join(targetDir, ".env.example"), generateEnv(projectName));
2177
2252
  if (template === "express") {
2178
- writeFile2(path2.join(targetDir, "src", "index.ts"), generateExpressServer(projectName));
2253
+ writeFile2(path2.join(targetDir, "src", "index.ts"), expressServer(projectName));
2179
2254
  writeFile2(path2.join(targetDir, "tsconfig.json"), generateTsconfig(template));
2180
2255
  } else if (template === "fastify") {
2181
- writeFile2(path2.join(targetDir, "src", "index.ts"), generateFastifyServer(projectName));
2256
+ writeFile2(path2.join(targetDir, "src", "index.ts"), fastifyServer(projectName));
2182
2257
  writeFile2(path2.join(targetDir, "tsconfig.json"), generateTsconfig(template));
2183
2258
  } else if (template === "nextjs") {
2259
+ writeFile2(path2.join(targetDir, "src", "app", "layout.tsx"), nextjsRootLayout(projectName));
2184
2260
  writeFile2(path2.join(targetDir, "src", "app", "page.tsx"), generateNextjsPage(projectName));
2185
- writeFile2(path2.join(targetDir, "middleware.ts"), nextjsTenantMiddleware());
2261
+ writeFile2(path2.join(targetDir, "src", "middleware.ts"), nextjsTenantMiddleware());
2186
2262
  }
2187
2263
  writeFile2(path2.join(targetDir, "README.md"), generateReadme(projectName, template));
2188
2264
  if (!skipInstall) {
@@ -2219,31 +2295,34 @@ function main(argv) {
2219
2295
  console.error(`Error: ${validationError}`);
2220
2296
  process.exit(1);
2221
2297
  }
2222
- const targetDir = path2.resolve(process.cwd(), projectName);
2223
- if (fs2.existsSync(targetDir) && !force) {
2224
- console.error(`Error: Directory "${projectName}" already exists.`);
2225
- console.error(`Use --force to overwrite.`);
2226
- process.exit(1);
2227
- }
2228
- if (fs2.existsSync(targetDir) && force) {
2229
- fs2.rmSync(targetDir, { recursive: true, force: true });
2230
- }
2231
- fs2.mkdirSync(targetDir, { recursive: true });
2298
+ let parsedPreset = null;
2232
2299
  if (preset) {
2233
- const parsed = parsePresetString(preset);
2234
- if (!parsed) {
2300
+ parsedPreset = parsePresetString(preset);
2301
+ if (!parsedPreset) {
2235
2302
  console.error(`Error: Invalid preset string "${preset}".`);
2236
2303
  console.error("Format: {database}-{strategy}-{orm}-{framework}");
2237
2304
  console.error("Example: postgres-rls-prisma-express, mongodb-database-mongoose-hono");
2238
2305
  process.exit(1);
2239
2306
  }
2240
- if (!isValidPreset(parsed)) {
2307
+ if (!isValidPreset(parsedPreset)) {
2241
2308
  console.error(`Error: Invalid preset combination "${preset}".`);
2242
- console.error(`${parsed.database} does not support ${parsed.strategy}/${parsed.orm} together.`);
2309
+ console.error(`${parsedPreset.database} does not support ${parsedPreset.strategy}/${parsedPreset.orm} together.`);
2243
2310
  console.error("Run with --help to see valid combinations.");
2244
2311
  process.exit(1);
2245
2312
  }
2246
- createPresetProject(projectName, parsed, targetDir, skipInstall);
2313
+ }
2314
+ const targetDir = path2.resolve(process.cwd(), projectName);
2315
+ if (fs2.existsSync(targetDir) && !force) {
2316
+ console.error(`Error: Directory "${projectName}" already exists.`);
2317
+ console.error(`Use --force to overwrite.`);
2318
+ process.exit(1);
2319
+ }
2320
+ if (fs2.existsSync(targetDir) && force) {
2321
+ fs2.rmSync(targetDir, { recursive: true, force: true });
2322
+ }
2323
+ fs2.mkdirSync(targetDir, { recursive: true });
2324
+ if (parsedPreset) {
2325
+ createPresetProject(projectName, parsedPreset, targetDir, skipInstall);
2247
2326
  } else {
2248
2327
  createProject(projectName, template, targetDir, skipInstall);
2249
2328
  }