@stratum-hq/create 0.4.0 → 0.4.1

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
@@ -1,11 +1,8 @@
1
- #!/usr/bin/env node
2
-
3
1
  // src/index.ts
4
2
  import * as fs2 from "fs";
5
3
  import * as path2 from "path";
6
4
  import crypto2 from "node:crypto";
7
5
  import { execSync as execSync2 } from "child_process";
8
- import { fileURLToPath } from "url";
9
6
 
10
7
  // src/matrix.ts
11
8
  var POSTGRES_STRATEGIES = ["rls", "schema", "database"];
@@ -224,8 +221,11 @@ function generatePostgresInit(projectName, dbName, strategy) {
224
221
  -- ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;
225
222
  -- ALTER TABLE your_table FORCE ROW LEVEL SECURITY;
226
223
  -- CREATE POLICY tenant_isolation ON your_table
227
- -- USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
224
+ -- USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid);
228
225
  --
226
+ -- A pooled connection reads the setting as '' after a tenant transaction ends,
227
+ -- or as NULL before the first one. NULLIF makes both return no rows; a bare
228
+ -- ::uuid cast of '' raises an error.
229
229
  -- FORCE makes the policy apply to the table owner too; without it, a table
230
230
  -- created by the application role is not isolated for that role.
231
231
  -- The Stratum db-adapters package sets app.current_tenant_id automatically.
@@ -446,59 +446,106 @@ export default config;
446
446
  },
447
447
  {
448
448
  filename: "src/stratum-knex.ts",
449
- content: `// Knex with Stratum tenant-scoped queries
449
+ content: preset.database === "postgres" ? KNEX_POSTGRES_SCOPE : knexColumnScope(preset.database)
450
+ }
451
+ ];
452
+ }
453
+ var KNEX_POSTGRES_SCOPE = `// Knex with Stratum tenant-scoped queries
454
+ import createKnex, { type Knex } from "knex";
455
+ import config from "../knexfile.js";
456
+
457
+ const knex = createKnex(config);
458
+
459
+ // Run fn in a transaction that sets app.current_tenant_id, the setting that
460
+ // the RLS policies read. The setting ends with the transaction, so run every
461
+ // tenant query through trx, not through knex.
462
+ export async function withTenantScope<T>(
463
+ tenantId: string,
464
+ fn: (trx: Knex.Transaction) => Promise<T>,
465
+ ): Promise<T> {
466
+ return knex.transaction(async (trx) => {
467
+ await trx.raw("SELECT set_config('app.current_tenant_id', ?, true)", [tenantId]);
468
+ return fn(trx);
469
+ });
470
+ }
471
+
472
+ // Usage:
473
+ // const orders = await withTenantScope(currentTenantId, (trx) => trx("orders").select());
474
+
475
+ export { knex };
476
+ `;
477
+ function knexColumnScope(database) {
478
+ return `// Knex with Stratum tenant-scoped queries
450
479
  import Knex from "knex";
451
480
  import config from "../knexfile.js";
452
481
 
453
482
  const knex = Knex(config);
454
483
 
455
484
  // Create a tenant-scoped query builder.
456
- // For RLS strategy, set the session variable before queries.
457
485
  export async function withTenantScope(tenantId: string, fn: (db: typeof knex) => Promise<void>) {
458
- ${preset.database === "postgres" ? ` await knex.raw("SET app.current_tenant = ?", [tenantId]);` : ` // For ${preset.database}, scope queries by tenant_id column`}
459
- try {
460
- await fn(knex);
461
- } finally {
462
- ${preset.database === "postgres" ? ` await knex.raw("RESET app.current_tenant");` : ` // Scope cleanup not needed for column-based isolation`}
463
- }
486
+ // For ${database}, scope queries by tenant_id column
487
+ await fn(knex);
464
488
  }
465
489
 
466
490
  export { knex };
467
- `
468
- }
469
- ];
491
+ `;
470
492
  }
471
- function generateMongooseSetup(_preset) {
493
+ function generateMongooseSetup(preset) {
494
+ const tenantAccess = preset.strategy === "collection" ? `// Return the tenant's model for a base collection. The tenant's documents are
495
+ // in the collection {baseCollection}_{tenantSlug}, the name that
496
+ // MongoCollectionAdapter from @stratum-hq/mongodb uses.
497
+ export function getTenantModel<T>(
498
+ baseCollection: string,
499
+ schema: mongoose.Schema<T>,
500
+ tenantSlug: string,
501
+ ) {
502
+ const name = \`\${baseCollection}_\${assertSlug(tenantSlug)}\`;
503
+ return mainConnection.models[name] ?? mainConnection.model(name, schema, name);
504
+ }
505
+
506
+ // Usage:
507
+ // const Order = getTenantModel("orders", OrderSchema, "tenant_abc");
508
+ // const orders = await Order.find();` : `// Return a connection to the tenant's own database. The database name is
509
+ // stratum_tenant_{tenantSlug}, the name that MongoDatabaseAdapter from
510
+ // @stratum-hq/mongodb uses. useCache returns the same connection on each call.
511
+ export function getTenantConnection(tenantSlug: string) {
512
+ return mainConnection.useDb(\`stratum_tenant_\${assertSlug(tenantSlug)}\`, { useCache: true });
513
+ }
514
+
515
+ // Usage:
516
+ // const conn = getTenantConnection("tenant_abc");
517
+ // const Order = conn.model("Order", OrderSchema);
518
+ // const orders = await Order.find();`;
472
519
  return [
473
520
  {
474
521
  filename: "src/stratum-mongoose.ts",
475
522
  content: `// Mongoose with Stratum multi-tenant support
476
523
  import mongoose from "mongoose";
477
- import { createTenantConnection } from "@stratum-hq/mongodb";
478
524
 
479
525
  // Main connection (used for tenant metadata)
480
526
  const mainConnection = mongoose.createConnection(
481
527
  process.env.MONGODB_URI || "mongodb://localhost:27017/main",
482
528
  );
483
529
 
484
- // Create a tenant-scoped connection.
485
- // Each tenant gets its own database (database-per-tenant strategy)
486
- // or collection prefix (collection strategy).
487
- export function getTenantConnection(tenantId: string) {
488
- return createTenantConnection(mainConnection, tenantId);
530
+ // A tenant slug becomes part of a MongoDB name, so it must match the Stratum
531
+ // slug rule: a lowercase letter, then lowercase letters, digits or underscores.
532
+ const SLUG_PATTERN = /^[a-z][a-z0-9_]{0,62}$/;
533
+
534
+ function assertSlug(tenantSlug: string): string {
535
+ if (!SLUG_PATTERN.test(tenantSlug)) {
536
+ throw new Error(\`Invalid tenant slug: "\${tenantSlug}"\`);
537
+ }
538
+ return tenantSlug;
489
539
  }
490
540
 
541
+ ${tenantAccess}
542
+
491
543
  // Define schemas that work across tenant connections
492
544
  export const TenantSchema = new mongoose.Schema({
493
545
  name: { type: String, required: true },
494
546
  createdAt: { type: Date, default: Date.now },
495
547
  });
496
548
 
497
- // Usage:
498
- // const conn = getTenantConnection("tenant-abc");
499
- // const Order = conn.model("Order", OrderSchema);
500
- // const orders = await Order.find();
501
-
502
549
  export { mainConnection };
503
550
  `
504
551
  }
@@ -517,7 +564,11 @@ const pool = mysql.createPool({
517
564
  });
518
565
 
519
566
  // Execute a query scoped to a tenant by filtering on tenant_id
520
- export async function tenantQuery(tenantId: string, sql: string, params: unknown[] = []) {
567
+ export async function tenantQuery(
568
+ tenantId: string,
569
+ sql: string,
570
+ params: (string | number | bigint | boolean | Date | null)[] = [],
571
+ ) {
521
572
  const [rows] = await pool.execute(sql, [...params, tenantId]);
522
573
  return rows;
523
574
  }
@@ -542,9 +593,10 @@ const pool = new Pool({
542
593
  });
543
594
 
544
595
  // Create a tenant-scoped pool.
545
- // Sets app.current_tenant on each connection for RLS enforcement.
596
+ // Each query runs in a transaction that sets app.current_tenant_id, the
597
+ // setting that the RLS policies read.
546
598
  export function getTenantPool(tenantId: string) {
547
- return createTenantPool(pool, tenantId);
599
+ return createTenantPool(pool, () => tenantId);
548
600
  }
549
601
 
550
602
  // Usage:
@@ -822,16 +874,516 @@ console.log("Add tenant resolution logic for your use case");
822
874
  ];
823
875
  }
824
876
 
877
+ // src/generators/tsconfig.ts
878
+ function rootSources(preset) {
879
+ return preset.orm === "knex" ? ["knexfile.ts"] : [];
880
+ }
881
+ function generateTsconfig(framework, extraSources = []) {
882
+ const rootDir = extraSources.length > 0 ? "." : "src";
883
+ return JSON.stringify(
884
+ {
885
+ compilerOptions: {
886
+ target: "ESNext",
887
+ module: "NodeNext",
888
+ moduleResolution: "NodeNext",
889
+ strict: true,
890
+ outDir: "dist",
891
+ rootDir,
892
+ declaration: true,
893
+ skipLibCheck: true,
894
+ esModuleInterop: true,
895
+ ...framework === "nestjs" ? { experimentalDecorators: true, emitDecoratorMetadata: true } : {},
896
+ ...framework === "nextjs" ? { jsx: "preserve", plugins: [{ name: "next" }] } : {}
897
+ },
898
+ include: ["src", ...extraSources],
899
+ exclude: ["node_modules", "dist"]
900
+ },
901
+ null,
902
+ 2
903
+ );
904
+ }
905
+
906
+ // ../hono/package.json
907
+ var package_default = {
908
+ name: "@stratum-hq/hono",
909
+ version: "1.1.0",
910
+ description: "Stratum Hono integration \u2014 tenant extraction middleware with ALS context",
911
+ keywords: [
912
+ "multi-tenancy",
913
+ "multi-tenant",
914
+ "saas",
915
+ "tenant",
916
+ "hono",
917
+ "middleware",
918
+ "typescript",
919
+ "stratum"
920
+ ],
921
+ type: "module",
922
+ main: "./dist/index.js",
923
+ types: "./dist/index.d.ts",
924
+ exports: {
925
+ ".": {
926
+ types: "./dist/index.d.ts",
927
+ default: "./dist/index.js"
928
+ }
929
+ },
930
+ files: [
931
+ "dist",
932
+ "README.md",
933
+ "LICENSE"
934
+ ],
935
+ scripts: {
936
+ build: "tsc",
937
+ test: "vitest run",
938
+ typecheck: "tsc --noEmit",
939
+ lint: "eslint .",
940
+ clean: "rm -rf dist"
941
+ },
942
+ publishConfig: {
943
+ access: "public",
944
+ registry: "https://registry.npmjs.org/"
945
+ },
946
+ peerDependencies: {
947
+ "@stratum-hq/core": "^1.0.0",
948
+ "@stratum-hq/sdk": "^1.2.0",
949
+ hono: ">=4.0.0"
950
+ },
951
+ devDependencies: {
952
+ "@stratum-hq/core": "*",
953
+ "@stratum-hq/sdk": "*",
954
+ hono: "^4.13.9",
955
+ typescript: "^5.4.0",
956
+ vitest: "^4.1.11"
957
+ },
958
+ license: "MIT",
959
+ author: "Christian Crank",
960
+ homepage: "https://github.com/stratum-hq/Stratum/tree/main/packages/hono#readme",
961
+ bugs: "https://github.com/stratum-hq/Stratum/issues",
962
+ engines: {
963
+ node: ">=20.0.0"
964
+ },
965
+ repository: {
966
+ type: "git",
967
+ url: "https://github.com/stratum-hq/Stratum.git",
968
+ directory: "packages/hono"
969
+ }
970
+ };
971
+
972
+ // ../db-adapters/package.json
973
+ var package_default2 = {
974
+ name: "@stratum-hq/db-adapters",
975
+ version: "1.2.0",
976
+ description: "Stratum PostgreSQL adapters for tenant-scoped query isolation",
977
+ keywords: [
978
+ "multi-tenancy",
979
+ "multi-tenant",
980
+ "saas",
981
+ "tenant",
982
+ "tenant-isolation",
983
+ "postgresql",
984
+ "postgres",
985
+ "row-level-security",
986
+ "rls",
987
+ "schema-isolation",
988
+ "database",
989
+ "stratum"
990
+ ],
991
+ main: "./dist/index.js",
992
+ types: "./dist/index.d.ts",
993
+ exports: {
994
+ ".": {
995
+ types: "./dist/index.d.ts",
996
+ default: "./dist/index.js"
997
+ }
998
+ },
999
+ files: [
1000
+ "dist",
1001
+ "README.md",
1002
+ "LICENSE"
1003
+ ],
1004
+ scripts: {
1005
+ build: "tsc",
1006
+ test: "vitest run",
1007
+ typecheck: "tsc --noEmit",
1008
+ lint: "eslint .",
1009
+ clean: "rm -rf dist"
1010
+ },
1011
+ publishConfig: {
1012
+ access: "public",
1013
+ registry: "https://registry.npmjs.org/"
1014
+ },
1015
+ dependencies: {
1016
+ "@stratum-hq/core": "^1.4.0",
1017
+ pg: "^8.11.0"
1018
+ },
1019
+ peerDependencies: {
1020
+ "drizzle-orm": ">=0.29.0"
1021
+ },
1022
+ peerDependenciesMeta: {
1023
+ "drizzle-orm": {
1024
+ optional: true
1025
+ }
1026
+ },
1027
+ devDependencies: {
1028
+ "@prisma/client": "^5.10.0",
1029
+ "@types/pg": "^8.11.0",
1030
+ "drizzle-orm": "^0.45.3",
1031
+ typescript: "^5.4.0",
1032
+ vitest: "^4.1.11"
1033
+ },
1034
+ license: "MIT",
1035
+ author: "Christian Crank",
1036
+ homepage: "https://github.com/stratum-hq/Stratum/tree/main/packages/db-adapters#readme",
1037
+ bugs: "https://github.com/stratum-hq/Stratum/issues",
1038
+ engines: {
1039
+ node: ">=20.0.0"
1040
+ },
1041
+ repository: {
1042
+ type: "git",
1043
+ url: "https://github.com/stratum-hq/Stratum.git",
1044
+ directory: "packages/db-adapters"
1045
+ }
1046
+ };
1047
+
1048
+ // ../lib/package.json
1049
+ var package_default3 = {
1050
+ name: "@stratum-hq/lib",
1051
+ version: "1.4.0",
1052
+ description: "Stratum tenant management library - framework-agnostic business logic",
1053
+ keywords: [
1054
+ "multi-tenancy",
1055
+ "multi-tenant",
1056
+ "saas",
1057
+ "b2b",
1058
+ "tenant",
1059
+ "tenant-management",
1060
+ "tenant-isolation",
1061
+ "node",
1062
+ "typescript",
1063
+ "postgresql",
1064
+ "row-level-security",
1065
+ "rls",
1066
+ "schema-isolation",
1067
+ "config-inheritance",
1068
+ "gdpr",
1069
+ "stratum"
1070
+ ],
1071
+ main: "./dist/index.js",
1072
+ types: "./dist/index.d.ts",
1073
+ exports: {
1074
+ ".": {
1075
+ types: "./dist/index.d.ts",
1076
+ default: "./dist/index.js"
1077
+ }
1078
+ },
1079
+ files: [
1080
+ "dist",
1081
+ "README.md",
1082
+ "LICENSE"
1083
+ ],
1084
+ scripts: {
1085
+ build: "tsc && node ../../scripts/copy-dir.mjs src/migrations dist/migrations",
1086
+ test: "vitest run",
1087
+ typecheck: "tsc --noEmit",
1088
+ lint: "eslint .",
1089
+ clean: "rm -rf dist"
1090
+ },
1091
+ publishConfig: {
1092
+ access: "public",
1093
+ registry: "https://registry.npmjs.org/"
1094
+ },
1095
+ dependencies: {
1096
+ "@opentelemetry/api": "^1.9.0",
1097
+ "@stratum-hq/core": "^1.4.0",
1098
+ "@stratum-hq/db-adapters": "^1.2.0",
1099
+ "@stratum-hq/sdk": "^1.2.0",
1100
+ pg: "^8.11.0"
1101
+ },
1102
+ peerDependencies: {
1103
+ "@opentelemetry/api": "^1.0.0"
1104
+ },
1105
+ peerDependenciesMeta: {
1106
+ "@opentelemetry/api": {
1107
+ optional: true
1108
+ }
1109
+ },
1110
+ devDependencies: {
1111
+ "@types/pg": "^8.11.0",
1112
+ typescript: "^5.4.0",
1113
+ vitest: "^4.1.11"
1114
+ },
1115
+ license: "MIT",
1116
+ author: "Christian Crank",
1117
+ homepage: "https://github.com/stratum-hq/Stratum/tree/main/packages/lib#readme",
1118
+ bugs: "https://github.com/stratum-hq/Stratum/issues",
1119
+ engines: {
1120
+ node: ">=20.0.0"
1121
+ },
1122
+ repository: {
1123
+ type: "git",
1124
+ url: "https://github.com/stratum-hq/Stratum.git",
1125
+ directory: "packages/lib"
1126
+ }
1127
+ };
1128
+
1129
+ // ../mongodb/package.json
1130
+ var package_default4 = {
1131
+ name: "@stratum-hq/mongodb",
1132
+ version: "0.4.0",
1133
+ description: "MongoDB tenant isolation for Stratum \u2014 shared collection, collection-per-tenant, and database-per-tenant strategies",
1134
+ keywords: [
1135
+ "multi-tenancy",
1136
+ "multi-tenant",
1137
+ "saas",
1138
+ "tenant",
1139
+ "tenant-isolation",
1140
+ "mongodb",
1141
+ "mongoose",
1142
+ "database",
1143
+ "stratum"
1144
+ ],
1145
+ main: "./dist/index.js",
1146
+ types: "./dist/index.d.ts",
1147
+ exports: {
1148
+ ".": {
1149
+ types: "./dist/index.d.ts",
1150
+ default: "./dist/index.js"
1151
+ }
1152
+ },
1153
+ files: [
1154
+ "dist",
1155
+ "README.md",
1156
+ "LICENSE"
1157
+ ],
1158
+ scripts: {
1159
+ build: "tsc",
1160
+ test: "vitest run",
1161
+ "test:watch": "vitest",
1162
+ "test:integration": "vitest run --config vitest.integration.config.ts",
1163
+ typecheck: "tsc --noEmit",
1164
+ lint: "eslint .",
1165
+ clean: "rm -rf dist"
1166
+ },
1167
+ dependencies: {
1168
+ "@stratum-hq/core": "^1.4.0",
1169
+ "@stratum-hq/sdk": "^1.2.0"
1170
+ },
1171
+ peerDependencies: {
1172
+ mongodb: "^6.0.0 || ^7.0.0"
1173
+ },
1174
+ peerDependenciesMeta: {
1175
+ mongoose: {
1176
+ optional: true
1177
+ }
1178
+ },
1179
+ devDependencies: {
1180
+ "@stratum-hq/test-utils": "*",
1181
+ mongodb: "^7.6.0",
1182
+ mongoose: "^8.0.0",
1183
+ typescript: "^5.9.0",
1184
+ vitest: "^4.1.11"
1185
+ },
1186
+ publishConfig: {
1187
+ access: "public"
1188
+ },
1189
+ license: "MIT",
1190
+ author: "Christian Crank",
1191
+ homepage: "https://github.com/stratum-hq/Stratum/tree/main/packages/mongodb#readme",
1192
+ bugs: "https://github.com/stratum-hq/Stratum/issues",
1193
+ engines: {
1194
+ node: ">=20.0.0"
1195
+ },
1196
+ repository: {
1197
+ type: "git",
1198
+ url: "https://github.com/stratum-hq/Stratum.git",
1199
+ directory: "packages/mongodb"
1200
+ }
1201
+ };
1202
+
1203
+ // ../mysql/package.json
1204
+ var package_default5 = {
1205
+ name: "@stratum-hq/mysql",
1206
+ version: "0.4.0",
1207
+ description: "MySQL tenant isolation for Stratum \u2014 shared table, table-per-tenant, and database-per-tenant strategies",
1208
+ keywords: [
1209
+ "multi-tenancy",
1210
+ "multi-tenant",
1211
+ "saas",
1212
+ "tenant",
1213
+ "tenant-isolation",
1214
+ "mysql",
1215
+ "database",
1216
+ "typeorm",
1217
+ "knex",
1218
+ "sequelize",
1219
+ "stratum"
1220
+ ],
1221
+ main: "./dist/index.js",
1222
+ types: "./dist/index.d.ts",
1223
+ exports: {
1224
+ ".": {
1225
+ types: "./dist/index.d.ts",
1226
+ default: "./dist/index.js"
1227
+ }
1228
+ },
1229
+ files: [
1230
+ "dist",
1231
+ "README.md",
1232
+ "LICENSE"
1233
+ ],
1234
+ scripts: {
1235
+ build: "tsc",
1236
+ test: "vitest run",
1237
+ "test:watch": "vitest",
1238
+ "test:integration": "vitest run --config vitest.integration.config.ts",
1239
+ typecheck: "tsc --noEmit",
1240
+ lint: "eslint .",
1241
+ clean: "rm -rf dist"
1242
+ },
1243
+ dependencies: {
1244
+ "@stratum-hq/core": "^1.4.0",
1245
+ "@stratum-hq/sdk": "^1.2.0"
1246
+ },
1247
+ peerDependencies: {
1248
+ mysql2: "^3.0.0",
1249
+ knex: ">=3.0.0",
1250
+ typeorm: "^1.0.0",
1251
+ sequelize: "^6.0.0"
1252
+ },
1253
+ peerDependenciesMeta: {
1254
+ typeorm: {
1255
+ optional: true
1256
+ },
1257
+ knex: {
1258
+ optional: true
1259
+ },
1260
+ sequelize: {
1261
+ optional: true
1262
+ }
1263
+ },
1264
+ devDependencies: {
1265
+ knex: "^3.3.0",
1266
+ mysql2: "^3.24.4",
1267
+ typeorm: "^1.1.1",
1268
+ typescript: "^5.9.0",
1269
+ vitest: "^4.1.11"
1270
+ },
1271
+ publishConfig: {
1272
+ access: "public"
1273
+ },
1274
+ license: "MIT",
1275
+ author: "Christian Crank",
1276
+ homepage: "https://github.com/stratum-hq/Stratum/tree/main/packages/mysql#readme",
1277
+ bugs: "https://github.com/stratum-hq/Stratum/issues",
1278
+ engines: {
1279
+ node: ">=20.0.0"
1280
+ },
1281
+ repository: {
1282
+ type: "git",
1283
+ url: "https://github.com/stratum-hq/Stratum.git",
1284
+ directory: "packages/mysql"
1285
+ }
1286
+ };
1287
+
1288
+ // ../nestjs/package.json
1289
+ var package_default6 = {
1290
+ name: "@stratum-hq/nestjs",
1291
+ version: "1.2.0",
1292
+ description: "Stratum NestJS integration \u2014 StratumGuard, @Tenant() decorator, and StratumModule",
1293
+ keywords: [
1294
+ "multi-tenancy",
1295
+ "multi-tenant",
1296
+ "saas",
1297
+ "tenant",
1298
+ "nestjs",
1299
+ "nest",
1300
+ "guard",
1301
+ "decorator",
1302
+ "middleware",
1303
+ "typescript",
1304
+ "stratum"
1305
+ ],
1306
+ type: "module",
1307
+ main: "./dist/index.js",
1308
+ types: "./dist/index.d.ts",
1309
+ exports: {
1310
+ ".": {
1311
+ types: "./dist/index.d.ts",
1312
+ default: "./dist/index.js"
1313
+ }
1314
+ },
1315
+ files: [
1316
+ "dist",
1317
+ "README.md",
1318
+ "LICENSE"
1319
+ ],
1320
+ scripts: {
1321
+ build: "tsc",
1322
+ test: "vitest run",
1323
+ typecheck: "tsc --noEmit",
1324
+ lint: "eslint .",
1325
+ clean: "rm -rf dist"
1326
+ },
1327
+ publishConfig: {
1328
+ access: "public",
1329
+ registry: "https://registry.npmjs.org/"
1330
+ },
1331
+ peerDependencies: {
1332
+ "@nestjs/common": ">=10.0.0",
1333
+ "@nestjs/core": ">=10.0.0",
1334
+ "@stratum-hq/core": "^1.0.0",
1335
+ "@stratum-hq/sdk": "^1.2.0",
1336
+ "reflect-metadata": ">=0.1.13"
1337
+ },
1338
+ devDependencies: {
1339
+ "@nestjs/common": "^12.1.2",
1340
+ "@nestjs/core": "^12.1.2",
1341
+ "@nestjs/testing": "^12.1.2",
1342
+ "@stratum-hq/core": "*",
1343
+ "@stratum-hq/sdk": "*",
1344
+ "reflect-metadata": "^0.2.2",
1345
+ typescript: "^5.4.0",
1346
+ vitest: "^4.1.11"
1347
+ },
1348
+ license: "MIT",
1349
+ author: "Christian Crank",
1350
+ homepage: "https://github.com/stratum-hq/Stratum/tree/main/packages/nestjs#readme",
1351
+ bugs: "https://github.com/stratum-hq/Stratum/issues",
1352
+ engines: {
1353
+ node: ">=20.0.0"
1354
+ },
1355
+ repository: {
1356
+ type: "git",
1357
+ url: "https://github.com/stratum-hq/Stratum.git",
1358
+ directory: "packages/nestjs"
1359
+ }
1360
+ };
1361
+
1362
+ // src/stratum-versions.ts
1363
+ var STRATUM_RANGES = {
1364
+ "@stratum-hq/db-adapters": `^${package_default2.version}`,
1365
+ "@stratum-hq/hono": `^${package_default.version}`,
1366
+ "@stratum-hq/lib": `^${package_default3.version}`,
1367
+ "@stratum-hq/mongodb": `^${package_default4.version}`,
1368
+ "@stratum-hq/mysql": `^${package_default5.version}`,
1369
+ "@stratum-hq/nestjs": `^${package_default6.version}`
1370
+ };
1371
+
825
1372
  // src/generators/package-json.ts
826
1373
  function generatePresetPackageJson(projectName, preset) {
827
1374
  const deps = {
828
- "@stratum-hq/lib": "^0.2.0"
1375
+ "@stratum-hq/lib": STRATUM_RANGES["@stratum-hq/lib"]
829
1376
  };
830
1377
  const devDeps = {
831
1378
  typescript: "^5.3.0",
832
1379
  "@types/node": "^20.11.0"
833
1380
  };
834
- addDatabaseDeps(deps, preset);
1381
+ if (preset.framework === "nestjs") {
1382
+ devDeps["tsc-watch"] = "^7.2.0";
1383
+ } else if (preset.framework !== "nextjs") {
1384
+ devDeps["tsx"] = "^4.7.0";
1385
+ }
1386
+ addDatabaseDeps(deps, devDeps, preset);
835
1387
  addOrmDeps(deps, devDeps, preset);
836
1388
  addFrameworkDeps(deps, devDeps, preset);
837
1389
  addStratumDeps(deps, preset);
@@ -853,9 +1405,12 @@ function generatePresetPackageJson(projectName, preset) {
853
1405
  2
854
1406
  );
855
1407
  }
856
- function addDatabaseDeps(deps, preset) {
1408
+ function addDatabaseDeps(deps, devDeps, preset) {
857
1409
  switch (preset.database) {
858
1410
  case "postgres":
1411
+ if (preset.orm === "pg" || preset.orm === "prisma" || preset.orm === "drizzle") {
1412
+ devDeps["@types/pg"] = "^8.11.0";
1413
+ }
859
1414
  if (preset.orm !== "prisma" && preset.orm !== "drizzle" && preset.orm !== "sequelize") {
860
1415
  deps["pg"] = "^8.11.0";
861
1416
  }
@@ -945,29 +1500,29 @@ function addFrameworkDeps(deps, devDeps, preset) {
945
1500
  }
946
1501
  function addStratumDeps(deps, preset) {
947
1502
  if (preset.database === "postgres" && preset.orm !== "mongoose") {
948
- deps["@stratum-hq/db-adapters"] = "^0.2.0";
1503
+ deps["@stratum-hq/db-adapters"] = STRATUM_RANGES["@stratum-hq/db-adapters"];
949
1504
  }
950
1505
  if (preset.database === "mongodb") {
951
- deps["@stratum-hq/mongodb"] = "^0.2.0";
1506
+ deps["@stratum-hq/mongodb"] = STRATUM_RANGES["@stratum-hq/mongodb"];
952
1507
  }
953
1508
  if (preset.database === "mysql") {
954
- deps["@stratum-hq/mysql"] = "^0.2.0";
1509
+ deps["@stratum-hq/mysql"] = STRATUM_RANGES["@stratum-hq/mysql"];
955
1510
  }
956
1511
  if (preset.framework === "hono") {
957
- deps["@stratum-hq/hono"] = "^0.2.0";
1512
+ deps["@stratum-hq/hono"] = STRATUM_RANGES["@stratum-hq/hono"];
958
1513
  }
959
1514
  if (preset.framework === "nestjs") {
960
- deps["@stratum-hq/nestjs"] = "^0.2.0";
1515
+ deps["@stratum-hq/nestjs"] = STRATUM_RANGES["@stratum-hq/nestjs"];
961
1516
  }
962
1517
  }
963
1518
  function getScripts(preset) {
964
1519
  if (preset.framework === "nextjs") {
965
1520
  return { dev: "next dev", build: "next build", start: "next start" };
966
1521
  }
967
- if (preset.framework === "nestjs") {
968
- return { dev: "node --watch src/main.ts", build: "tsc", start: "node dist/main.js" };
969
- }
970
- return { dev: "node --watch src/index.js", build: "tsc", start: "node dist/index.js" };
1522
+ const entry = preset.framework === "nestjs" ? "main" : "index";
1523
+ const emittedDir = rootSources(preset).length > 0 ? "dist/src" : "dist";
1524
+ const dev = preset.framework === "nestjs" ? `tsc-watch --onSuccess "node --env-file=.env ${emittedDir}/${entry}.js"` : `tsx watch --env-file=.env src/${entry}.ts`;
1525
+ return { dev, build: "tsc", start: `node ${emittedDir}/${entry}.js` };
971
1526
  }
972
1527
  function sortKeys(obj) {
973
1528
  const sorted = {};
@@ -979,7 +1534,6 @@ function sortKeys(obj) {
979
1534
 
980
1535
  // src/generators/readme.ts
981
1536
  function generatePresetReadme(projectName, preset) {
982
- const devCmd = getDevCommand(preset);
983
1537
  const dbStartCmd = getDbStartInfo(preset);
984
1538
  const dbSetupNote = getDbSetupNote(preset);
985
1539
  return `# ${projectName}
@@ -1014,7 +1568,7 @@ ${dbSetupNote}
1014
1568
  ### 4. Run the app
1015
1569
 
1016
1570
  \`\`\`bash
1017
- ${devCmd}
1571
+ npm run dev
1018
1572
  \`\`\`
1019
1573
 
1020
1574
  ## Multi-tenancy
@@ -1026,12 +1580,6 @@ ${getStrategyDescription(preset.strategy)}
1026
1580
  See the [Stratum docs](https://github.com/stratum-hq/Stratum) for full reference.
1027
1581
  `;
1028
1582
  }
1029
- function getDevCommand(preset) {
1030
- if (preset.framework === "nextjs") return "npm run dev";
1031
- if (preset.framework === "nestjs") return "node --env-file=.env src/main.ts";
1032
- if (preset.framework === "none") return "node --env-file=.env src/index.ts";
1033
- return "node --env-file=.env src/index.ts";
1034
- }
1035
1583
  function getDbStartInfo(preset) {
1036
1584
  switch (preset.database) {
1037
1585
  case "postgres":
@@ -1068,7 +1616,7 @@ function getStrategyDescription(strategy) {
1068
1616
  switch (strategy) {
1069
1617
  case "rls":
1070
1618
  return `- **Row-Level Security** -- PostgreSQL RLS policies filter rows by tenant automatically
1071
- - Each query sets \`app.current_tenant\` and RLS enforces isolation
1619
+ - Each query sets \`app.current_tenant_id\` and RLS enforces isolation
1072
1620
  - All tenants share one database and schema`;
1073
1621
  case "schema":
1074
1622
  return `- **Schema-per-tenant** -- each tenant gets a dedicated PostgreSQL schema
@@ -1172,27 +1720,7 @@ Creating ${projectName} with preset ${presetLabel}...
1172
1720
  );
1173
1721
  writeFile(
1174
1722
  path.join(targetDir, "tsconfig.json"),
1175
- JSON.stringify(
1176
- {
1177
- compilerOptions: {
1178
- target: "ESNext",
1179
- module: "NodeNext",
1180
- moduleResolution: "NodeNext",
1181
- strict: true,
1182
- outDir: "dist",
1183
- rootDir: "src",
1184
- declaration: true,
1185
- skipLibCheck: true,
1186
- esModuleInterop: true,
1187
- ...preset.framework === "nestjs" ? { experimentalDecorators: true, emitDecoratorMetadata: true } : {},
1188
- ...preset.framework === "nextjs" ? { jsx: "preserve", plugins: [{ name: "next" }] } : {}
1189
- },
1190
- include: ["src"],
1191
- exclude: ["node_modules", "dist"]
1192
- },
1193
- null,
1194
- 2
1195
- )
1723
+ generateTsconfig(preset.framework, rootSources(preset))
1196
1724
  );
1197
1725
  if (!skipInstall) {
1198
1726
  console.log("\nInstalling dependencies...\n");
@@ -1295,7 +1823,7 @@ function generatePackageJson(projectName, template) {
1295
1823
  }
1296
1824
  };
1297
1825
  const deps = {
1298
- "@stratum-hq/lib": "^0.2.0",
1826
+ "@stratum-hq/lib": STRATUM_RANGES["@stratum-hq/lib"],
1299
1827
  pg: "^8.11.0",
1300
1828
  ...frameworkDeps[template]
1301
1829
  };
@@ -1305,11 +1833,15 @@ function generatePackageJson(projectName, template) {
1305
1833
  version: "0.1.0",
1306
1834
  private: true,
1307
1835
  type: "module",
1308
- scripts: template === "nextjs" ? { dev: "next dev", build: "next build", start: "next start" } : { dev: "node --watch src/index.js", build: "tsc", start: "node dist/index.js" },
1836
+ scripts: template === "nextjs" ? { dev: "next dev", build: "next build", start: "next start" } : (
1837
+ // Node 20 cannot run a .ts file, so dev runs the source through tsx.
1838
+ { dev: "tsx watch --env-file=.env src/index.ts", build: "tsc", start: "node dist/index.js" }
1839
+ ),
1309
1840
  dependencies: deps,
1310
1841
  devDependencies: {
1311
1842
  typescript: "^5.3.0",
1312
- "@types/node": "^20.11.0"
1843
+ "@types/node": "^20.11.0",
1844
+ ...template === "nextjs" ? {} : { tsx: "^4.7.0", "@types/pg": "^8.11.0" }
1313
1845
  },
1314
1846
  engines: {
1315
1847
  node: ">=20.0.0"
@@ -1493,7 +2025,6 @@ export const config = {
1493
2025
  `;
1494
2026
  }
1495
2027
  function generateReadme(projectName, template) {
1496
- const devCmd = template === "nextjs" ? "npm run dev" : "node --env-file=.env src/index.ts";
1497
2028
  return `# ${projectName}
1498
2029
 
1499
2030
  A multi-tenant application built with [Stratum](https://github.com/stratum-hq/Stratum).
@@ -1522,7 +2053,7 @@ npm install
1522
2053
  ### 4. Run the app
1523
2054
 
1524
2055
  \`\`\`bash
1525
- ${devCmd}
2056
+ npm run dev
1526
2057
  \`\`\`
1527
2058
 
1528
2059
  ## Project structure
@@ -1533,7 +2064,7 @@ ${projectName}/
1533
2064
  \u251C\u2500\u2500 docker-compose.yml
1534
2065
  \u251C\u2500\u2500 init.sql # DB extensions (uuid-ossp, ltree)
1535
2066
  \u251C\u2500\u2500 .env.example
1536
- \u2514\u2500\u2500 package.json
2067
+ ${template === "nextjs" ? "" : "\u251C\u2500\u2500 tsconfig.json\n"}\u2514\u2500\u2500 package.json
1537
2068
  \`\`\`
1538
2069
 
1539
2070
  ## Multi-tenancy
@@ -1560,8 +2091,10 @@ Creating ${projectName} with ${template} template...
1560
2091
  writeFile2(path2.join(targetDir, ".env.example"), generateEnv(projectName));
1561
2092
  if (template === "express") {
1562
2093
  writeFile2(path2.join(targetDir, "src", "index.ts"), generateExpressServer(projectName));
2094
+ writeFile2(path2.join(targetDir, "tsconfig.json"), generateTsconfig(template));
1563
2095
  } else if (template === "fastify") {
1564
2096
  writeFile2(path2.join(targetDir, "src", "index.ts"), generateFastifyServer(projectName));
2097
+ writeFile2(path2.join(targetDir, "tsconfig.json"), generateTsconfig(template));
1565
2098
  } else if (template === "nextjs") {
1566
2099
  writeFile2(path2.join(targetDir, "src", "app", "page.tsx"), generateNextjsPage(projectName));
1567
2100
  writeFile2(path2.join(targetDir, "middleware.ts"), generateNextjsMiddleware2());
@@ -1639,26 +2172,9 @@ Success! Created ${projectName} at ${targetDir}
1639
2172
  if (skipInstall) {
1640
2173
  console.log(" npm install");
1641
2174
  }
1642
- if (preset) {
1643
- const parsed = parsePresetString(preset);
1644
- if (parsed.framework === "nextjs") {
1645
- console.log(" npm run dev");
1646
- } else if (parsed.framework === "nestjs") {
1647
- console.log(" node --env-file=.env src/main.ts");
1648
- } else {
1649
- console.log(" node --env-file=.env src/index.ts");
1650
- }
1651
- } else if (template === "nextjs") {
1652
- console.log(" npm run dev");
1653
- } else {
1654
- console.log(" node --env-file=.env src/index.ts");
1655
- }
2175
+ console.log(" npm run dev");
1656
2176
  console.log("");
1657
2177
  }
1658
- var __filename = fileURLToPath(import.meta.url);
1659
- if (process.argv[1] === __filename) {
1660
- main(process.argv.slice(2));
1661
- }
1662
2178
  export {
1663
2179
  createProject,
1664
2180
  main,