@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/package.json CHANGED
@@ -1,10 +1,30 @@
1
1
  {
2
2
  "name": "@stratum-hq/create",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Create a new Stratum multi-tenancy project",
5
+ "keywords": [
6
+ "multi-tenancy",
7
+ "multi-tenant",
8
+ "saas",
9
+ "tenant",
10
+ "create",
11
+ "scaffolding",
12
+ "starter",
13
+ "boilerplate",
14
+ "node",
15
+ "typescript",
16
+ "stratum"
17
+ ],
5
18
  "bin": {
6
19
  "create-stratum": "./dist/index.js"
7
20
  },
21
+ "exports": {
22
+ ".": "./dist/index.js",
23
+ "./matrix": {
24
+ "import": "./src/matrix.ts",
25
+ "types": "./src/matrix.ts"
26
+ }
27
+ },
8
28
  "type": "module",
9
29
  "scripts": {
10
30
  "build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@stratum-hq/*",
@@ -0,0 +1,283 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ parsePresetString,
4
+ formatPresetString,
5
+ isValidPreset,
6
+ getValidOptions,
7
+ VALID_COMBINATIONS,
8
+ type StackPreset,
9
+ } from "../matrix.js";
10
+
11
+ // ─── parsePresetString ───────────────────────────────────────────────────────
12
+
13
+ describe("parsePresetString", () => {
14
+ it("parses a valid postgres preset", () => {
15
+ const result = parsePresetString("postgres-rls-prisma-express");
16
+ expect(result).toEqual({
17
+ database: "postgres",
18
+ strategy: "rls",
19
+ orm: "prisma",
20
+ framework: "express",
21
+ });
22
+ });
23
+
24
+ it("parses a valid mongodb preset", () => {
25
+ const result = parsePresetString("mongodb-database-mongoose-hono");
26
+ expect(result).toEqual({
27
+ database: "mongodb",
28
+ strategy: "database",
29
+ orm: "mongoose",
30
+ framework: "hono",
31
+ });
32
+ });
33
+
34
+ it("parses mysql with table-prefix (hyphenated strategy)", () => {
35
+ const result = parsePresetString("mysql-table-prefix-sequelize-nestjs");
36
+ expect(result).toEqual({
37
+ database: "mysql",
38
+ strategy: "table-prefix",
39
+ orm: "sequelize",
40
+ framework: "nestjs",
41
+ });
42
+ });
43
+
44
+ it("parses preset with none framework", () => {
45
+ const result = parsePresetString("postgres-schema-drizzle-none");
46
+ expect(result).toEqual({
47
+ database: "postgres",
48
+ strategy: "schema",
49
+ orm: "drizzle",
50
+ framework: "none",
51
+ });
52
+ });
53
+
54
+ it("handles uppercase input by lowering", () => {
55
+ const result = parsePresetString("POSTGRES-RLS-PRISMA-EXPRESS");
56
+ expect(result).toEqual({
57
+ database: "postgres",
58
+ strategy: "rls",
59
+ orm: "prisma",
60
+ framework: "express",
61
+ });
62
+ });
63
+
64
+ it("returns null for empty string", () => {
65
+ expect(parsePresetString("")).toBeNull();
66
+ });
67
+
68
+ it("returns null for garbage input", () => {
69
+ expect(parsePresetString("invalid-garbage")).toBeNull();
70
+ });
71
+
72
+ it("returns null for too few parts", () => {
73
+ expect(parsePresetString("postgres-rls")).toBeNull();
74
+ });
75
+
76
+ it("returns null for too many parts (non table-prefix)", () => {
77
+ expect(parsePresetString("postgres-rls-prisma-express-extra-stuff")).toBeNull();
78
+ });
79
+
80
+ it("returns null for unknown database", () => {
81
+ expect(parsePresetString("redis-rls-prisma-express")).toBeNull();
82
+ });
83
+
84
+ it("returns null for unknown orm", () => {
85
+ expect(parsePresetString("postgres-rls-typeorm-express")).toBeNull();
86
+ });
87
+
88
+ it("returns null for unknown framework", () => {
89
+ expect(parsePresetString("postgres-rls-prisma-koa")).toBeNull();
90
+ });
91
+
92
+ it("returns null for unknown strategy", () => {
93
+ expect(parsePresetString("postgres-sharding-prisma-express")).toBeNull();
94
+ });
95
+ });
96
+
97
+ // ─── formatPresetString ──────────────────────────────────────────────────────
98
+
99
+ describe("formatPresetString", () => {
100
+ it("formats a simple preset", () => {
101
+ const preset: StackPreset = {
102
+ database: "postgres",
103
+ strategy: "rls",
104
+ orm: "prisma",
105
+ framework: "express",
106
+ };
107
+ expect(formatPresetString(preset)).toBe("postgres-rls-prisma-express");
108
+ });
109
+
110
+ it("formats a table-prefix preset", () => {
111
+ const preset: StackPreset = {
112
+ database: "mysql",
113
+ strategy: "table-prefix",
114
+ orm: "sequelize",
115
+ framework: "nestjs",
116
+ };
117
+ expect(formatPresetString(preset)).toBe("mysql-table-prefix-sequelize-nestjs");
118
+ });
119
+
120
+ it("round-trips with parsePresetString", () => {
121
+ const original = "mongodb-collection-mongoose-fastify";
122
+ const parsed = parsePresetString(original)!;
123
+ expect(parsed).not.toBeNull();
124
+ expect(formatPresetString(parsed)).toBe(original);
125
+ });
126
+
127
+ it("round-trips table-prefix", () => {
128
+ const original = "mysql-table-prefix-knex-hono";
129
+ const parsed = parsePresetString(original)!;
130
+ expect(parsed).not.toBeNull();
131
+ expect(formatPresetString(parsed)).toBe(original);
132
+ });
133
+ });
134
+
135
+ // ─── isValidPreset ───────────────────────────────────────────────────────────
136
+
137
+ describe("isValidPreset", () => {
138
+ // Valid PostgreSQL combos
139
+ it("accepts postgres-rls-prisma-express", () => {
140
+ expect(isValidPreset({ database: "postgres", strategy: "rls", orm: "prisma", framework: "express" })).toBe(true);
141
+ });
142
+
143
+ it("accepts postgres-schema-drizzle-fastify", () => {
144
+ expect(isValidPreset({ database: "postgres", strategy: "schema", orm: "drizzle", framework: "fastify" })).toBe(true);
145
+ });
146
+
147
+ it("accepts postgres-database-pg-none", () => {
148
+ expect(isValidPreset({ database: "postgres", strategy: "database", orm: "pg", framework: "none" })).toBe(true);
149
+ });
150
+
151
+ it("accepts postgres-rls-knex-hono", () => {
152
+ expect(isValidPreset({ database: "postgres", strategy: "rls", orm: "knex", framework: "hono" })).toBe(true);
153
+ });
154
+
155
+ it("accepts postgres-rls-sequelize-nestjs", () => {
156
+ expect(isValidPreset({ database: "postgres", strategy: "rls", orm: "sequelize", framework: "nestjs" })).toBe(true);
157
+ });
158
+
159
+ // Valid MongoDB combos
160
+ it("accepts mongodb-database-mongoose-express", () => {
161
+ expect(isValidPreset({ database: "mongodb", strategy: "database", orm: "mongoose", framework: "express" })).toBe(true);
162
+ });
163
+
164
+ it("accepts mongodb-collection-mongoose-hono", () => {
165
+ expect(isValidPreset({ database: "mongodb", strategy: "collection", orm: "mongoose", framework: "hono" })).toBe(true);
166
+ });
167
+
168
+ // Valid MySQL combos
169
+ it("accepts mysql-database-sequelize-express", () => {
170
+ expect(isValidPreset({ database: "mysql", strategy: "database", orm: "sequelize", framework: "express" })).toBe(true);
171
+ });
172
+
173
+ it("accepts mysql-table-prefix-knex-nextjs", () => {
174
+ expect(isValidPreset({ database: "mysql", strategy: "table-prefix", orm: "knex", framework: "nextjs" })).toBe(true);
175
+ });
176
+
177
+ it("accepts mysql-database-pg-fastify", () => {
178
+ expect(isValidPreset({ database: "mysql", strategy: "database", orm: "pg", framework: "fastify" })).toBe(true);
179
+ });
180
+
181
+ // Invalid combos
182
+ it("rejects mongodb-rls-prisma-express (rls not valid for mongodb)", () => {
183
+ expect(isValidPreset({ database: "mongodb", strategy: "rls", orm: "prisma", framework: "express" })).toBe(false);
184
+ });
185
+
186
+ it("rejects mongodb-database-prisma-express (prisma not valid for mongodb)", () => {
187
+ expect(isValidPreset({ database: "mongodb", strategy: "database", orm: "prisma", framework: "express" })).toBe(false);
188
+ });
189
+
190
+ it("rejects postgres-collection-prisma-express (collection not valid for postgres)", () => {
191
+ expect(isValidPreset({ database: "postgres", strategy: "collection", orm: "prisma", framework: "express" })).toBe(false);
192
+ });
193
+
194
+ it("rejects mysql-rls-sequelize-express (rls not valid for mysql)", () => {
195
+ expect(isValidPreset({ database: "mysql", strategy: "rls", orm: "sequelize", framework: "express" })).toBe(false);
196
+ });
197
+
198
+ it("rejects mysql-database-mongoose-express (mongoose not valid for mysql)", () => {
199
+ expect(isValidPreset({ database: "mysql", strategy: "database", orm: "mongoose", framework: "express" })).toBe(false);
200
+ });
201
+
202
+ it("rejects postgres-rls-mongoose-express (mongoose not valid for postgres)", () => {
203
+ expect(isValidPreset({ database: "postgres", strategy: "rls", orm: "mongoose", framework: "express" })).toBe(false);
204
+ });
205
+ });
206
+
207
+ // ─── getValidOptions ─────────────────────────────────────────────────────────
208
+
209
+ describe("getValidOptions", () => {
210
+ it("returns all options when no selection made", () => {
211
+ const opts = getValidOptions({});
212
+ expect(opts.databases).toContain("postgres");
213
+ expect(opts.databases).toContain("mongodb");
214
+ expect(opts.databases).toContain("mysql");
215
+ expect(opts.orms.length).toBeGreaterThan(0);
216
+ expect(opts.strategies.length).toBeGreaterThan(0);
217
+ expect(opts.frameworks.length).toBeGreaterThan(0);
218
+ });
219
+
220
+ it("narrows orms to mongoose when mongodb selected", () => {
221
+ const opts = getValidOptions({ database: "mongodb" });
222
+ expect(opts.orms).toEqual(["mongoose"]);
223
+ });
224
+
225
+ it("narrows strategies when mongodb selected", () => {
226
+ const opts = getValidOptions({ database: "mongodb" });
227
+ expect(opts.strategies).toEqual(["database", "collection"]);
228
+ });
229
+
230
+ it("narrows databases when mongoose selected", () => {
231
+ const opts = getValidOptions({ orm: "mongoose" });
232
+ expect(opts.databases).toEqual(["mongodb"]);
233
+ });
234
+
235
+ it("narrows databases when rls strategy selected", () => {
236
+ const opts = getValidOptions({ strategy: "rls" });
237
+ expect(opts.databases).toEqual(["postgres"]);
238
+ });
239
+
240
+ it("includes all frameworks for any database", () => {
241
+ for (const db of ["postgres", "mongodb", "mysql"] as const) {
242
+ const opts = getValidOptions({ database: db });
243
+ expect(opts.frameworks).toContain("express");
244
+ expect(opts.frameworks).toContain("none");
245
+ }
246
+ });
247
+
248
+ it("narrows databases when table-prefix selected", () => {
249
+ const opts = getValidOptions({ strategy: "table-prefix" });
250
+ expect(opts.databases).toEqual(["mysql"]);
251
+ });
252
+
253
+ it("includes postgres and mysql for sequelize", () => {
254
+ const opts = getValidOptions({ orm: "sequelize" });
255
+ expect(opts.databases).toContain("postgres");
256
+ expect(opts.databases).toContain("mysql");
257
+ expect(opts.databases).not.toContain("mongodb");
258
+ });
259
+ });
260
+
261
+ // ─── VALID_COMBINATIONS structure ────────────────────────────────────────────
262
+
263
+ describe("VALID_COMBINATIONS", () => {
264
+ it("has all three databases", () => {
265
+ expect(Object.keys(VALID_COMBINATIONS)).toEqual(["postgres", "mongodb", "mysql"]);
266
+ });
267
+
268
+ it("postgres has rls, schema, database strategies", () => {
269
+ expect(VALID_COMBINATIONS.postgres.strategies).toEqual(["rls", "schema", "database"]);
270
+ });
271
+
272
+ it("mongodb has database, collection strategies", () => {
273
+ expect(VALID_COMBINATIONS.mongodb.strategies).toEqual(["database", "collection"]);
274
+ });
275
+
276
+ it("mysql has database, table-prefix strategies", () => {
277
+ expect(VALID_COMBINATIONS.mysql.strategies).toEqual(["database", "table-prefix"]);
278
+ });
279
+
280
+ it("mongodb only supports mongoose", () => {
281
+ expect(VALID_COMBINATIONS.mongodb.orms).toEqual(["mongoose"]);
282
+ });
283
+ });
@@ -0,0 +1,266 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import * as os from "os";
5
+ import { parseArgs, createProject } from "../index.js";
6
+ import { createPresetProject } from "../preset-project.js";
7
+ import type { StackPreset } from "../matrix.js";
8
+
9
+ // ─── parseArgs --preset tests ────────────────────────────────────────────────
10
+
11
+ describe("parseArgs --preset", () => {
12
+ it("parses --preset flag", () => {
13
+ const result = parseArgs(["my-app", "--preset", "postgres-rls-prisma-express"]);
14
+ expect(result.preset).toBe("postgres-rls-prisma-express");
15
+ });
16
+
17
+ it("parses --preset=value syntax", () => {
18
+ const result = parseArgs(["my-app", "--preset=mongodb-database-mongoose-hono"]);
19
+ expect(result.preset).toBe("mongodb-database-mongoose-hono");
20
+ });
21
+
22
+ it("preset defaults to null", () => {
23
+ const result = parseArgs(["my-app"]);
24
+ expect(result.preset).toBeNull();
25
+ });
26
+
27
+ it("existing --template still works when no preset", () => {
28
+ const result = parseArgs(["my-app", "--template", "fastify"]);
29
+ expect(result.template).toBe("fastify");
30
+ expect(result.preset).toBeNull();
31
+ });
32
+ });
33
+
34
+ // ─── Regression: existing template tests ─────────────────────────────────────
35
+
36
+ describe("regression: --template path unchanged", () => {
37
+ let tmpDir: string;
38
+ let projectDir: string;
39
+
40
+ beforeEach(() => {
41
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "stratum-preset-regress-"));
42
+ projectDir = path.join(tmpDir, "test-project");
43
+ fs.mkdirSync(projectDir, { recursive: true });
44
+ });
45
+
46
+ afterEach(() => {
47
+ fs.rmSync(tmpDir, { recursive: true, force: true });
48
+ });
49
+
50
+ it("--template express still generates correct files", () => {
51
+ createProject("test-project", "express", projectDir, true);
52
+ expect(fs.existsSync(path.join(projectDir, "package.json"))).toBe(true);
53
+ expect(fs.existsSync(path.join(projectDir, "docker-compose.yml"))).toBe(true);
54
+ expect(fs.existsSync(path.join(projectDir, "src", "index.ts"))).toBe(true);
55
+
56
+ const pkg = JSON.parse(fs.readFileSync(path.join(projectDir, "package.json"), "utf8"));
57
+ expect(pkg.dependencies["express"]).toBeDefined();
58
+ expect(pkg.dependencies["@stratum-hq/lib"]).toBeDefined();
59
+ });
60
+
61
+ it("--template nextjs still generates correct files", () => {
62
+ createProject("test-project", "nextjs", projectDir, true);
63
+ expect(fs.existsSync(path.join(projectDir, "src", "app", "page.tsx"))).toBe(true);
64
+ expect(fs.existsSync(path.join(projectDir, "middleware.ts"))).toBe(true);
65
+ });
66
+ });
67
+
68
+ // ─── Preset project generation ───────────────────────────────────────────────
69
+
70
+ describe("createPresetProject", () => {
71
+ let tmpDir: string;
72
+ let projectDir: string;
73
+
74
+ beforeEach(() => {
75
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "stratum-preset-test-"));
76
+ projectDir = path.join(tmpDir, "test-project");
77
+ fs.mkdirSync(projectDir, { recursive: true });
78
+ });
79
+
80
+ afterEach(() => {
81
+ fs.rmSync(tmpDir, { recursive: true, force: true });
82
+ });
83
+
84
+ // ── PostgreSQL presets ──
85
+
86
+ it("generates postgres-rls-prisma-express project", () => {
87
+ const preset: StackPreset = { database: "postgres", strategy: "rls", orm: "prisma", framework: "express" };
88
+ createPresetProject("test-project", preset, projectDir, true);
89
+
90
+ expect(fs.existsSync(path.join(projectDir, "package.json"))).toBe(true);
91
+ expect(fs.existsSync(path.join(projectDir, "docker-compose.yml"))).toBe(true);
92
+ expect(fs.existsSync(path.join(projectDir, "init.sql"))).toBe(true);
93
+ expect(fs.existsSync(path.join(projectDir, ".env.example"))).toBe(true);
94
+ expect(fs.existsSync(path.join(projectDir, "src", "index.ts"))).toBe(true);
95
+ expect(fs.existsSync(path.join(projectDir, "prisma", "schema.prisma"))).toBe(true);
96
+ expect(fs.existsSync(path.join(projectDir, "src", "stratum-prisma.ts"))).toBe(true);
97
+ expect(fs.existsSync(path.join(projectDir, "README.md"))).toBe(true);
98
+ expect(fs.existsSync(path.join(projectDir, "tsconfig.json"))).toBe(true);
99
+
100
+ const pkg = JSON.parse(fs.readFileSync(path.join(projectDir, "package.json"), "utf8"));
101
+ expect(pkg.dependencies["@prisma/client"]).toBeDefined();
102
+ expect(pkg.dependencies["express"]).toBeDefined();
103
+ expect(pkg.dependencies["@stratum-hq/lib"]).toBeDefined();
104
+ expect(pkg.dependencies["@stratum-hq/db-adapters"]).toBeDefined();
105
+
106
+ const docker = fs.readFileSync(path.join(projectDir, "docker-compose.yml"), "utf8");
107
+ expect(docker).toContain("postgres:16-alpine");
108
+
109
+ const sql = fs.readFileSync(path.join(projectDir, "init.sql"), "utf8");
110
+ expect(sql).toContain("uuid-ossp");
111
+ expect(sql).toContain("ltree");
112
+ expect(sql).toContain("Row-Level Security");
113
+ });
114
+
115
+ it("generates postgres-schema-drizzle-fastify project", () => {
116
+ const preset: StackPreset = { database: "postgres", strategy: "schema", orm: "drizzle", framework: "fastify" };
117
+ createPresetProject("test-project", preset, projectDir, true);
118
+
119
+ expect(fs.existsSync(path.join(projectDir, "src", "stratum-drizzle.ts"))).toBe(true);
120
+ expect(fs.existsSync(path.join(projectDir, "drizzle.config.ts"))).toBe(true);
121
+ expect(fs.existsSync(path.join(projectDir, "src", "index.ts"))).toBe(true);
122
+
123
+ const pkg = JSON.parse(fs.readFileSync(path.join(projectDir, "package.json"), "utf8"));
124
+ expect(pkg.dependencies["drizzle-orm"]).toBeDefined();
125
+ expect(pkg.dependencies["fastify"]).toBeDefined();
126
+
127
+ const server = fs.readFileSync(path.join(projectDir, "src", "index.ts"), "utf8");
128
+ expect(server).toContain("Fastify");
129
+ });
130
+
131
+ it("generates postgres-database-pg-express project", () => {
132
+ const preset: StackPreset = { database: "postgres", strategy: "database", orm: "pg", framework: "express" };
133
+ createPresetProject("test-project", preset, projectDir, true);
134
+
135
+ expect(fs.existsSync(path.join(projectDir, "src", "stratum-db.ts"))).toBe(true);
136
+
137
+ const pkg = JSON.parse(fs.readFileSync(path.join(projectDir, "package.json"), "utf8"));
138
+ expect(pkg.dependencies["pg"]).toBeDefined();
139
+ expect(pkg.dependencies["@stratum-hq/db-adapters"]).toBeDefined();
140
+ });
141
+
142
+ // ── MongoDB presets ──
143
+
144
+ it("generates mongodb-database-mongoose-hono project", () => {
145
+ const preset: StackPreset = { database: "mongodb", strategy: "database", orm: "mongoose", framework: "hono" };
146
+ createPresetProject("test-project", preset, projectDir, true);
147
+
148
+ expect(fs.existsSync(path.join(projectDir, "docker-compose.yml"))).toBe(true);
149
+ expect(fs.existsSync(path.join(projectDir, "src", "stratum-mongoose.ts"))).toBe(true);
150
+ expect(fs.existsSync(path.join(projectDir, "src", "index.ts"))).toBe(true);
151
+ // MongoDB should NOT have init.sql
152
+ expect(fs.existsSync(path.join(projectDir, "init.sql"))).toBe(false);
153
+
154
+ const docker = fs.readFileSync(path.join(projectDir, "docker-compose.yml"), "utf8");
155
+ expect(docker).toContain("mongo:7");
156
+
157
+ const pkg = JSON.parse(fs.readFileSync(path.join(projectDir, "package.json"), "utf8"));
158
+ expect(pkg.dependencies["mongoose"]).toBeDefined();
159
+ expect(pkg.dependencies["hono"]).toBeDefined();
160
+ expect(pkg.dependencies["@stratum-hq/mongodb"]).toBeDefined();
161
+
162
+ const env = fs.readFileSync(path.join(projectDir, ".env.example"), "utf8");
163
+ expect(env).toContain("MONGODB_URI");
164
+ });
165
+
166
+ // ── MySQL presets ──
167
+
168
+ it("generates mysql-table-prefix-sequelize-nestjs project", () => {
169
+ const preset: StackPreset = { database: "mysql", strategy: "table-prefix", orm: "sequelize", framework: "nestjs" };
170
+ createPresetProject("test-project", preset, projectDir, true);
171
+
172
+ expect(fs.existsSync(path.join(projectDir, "docker-compose.yml"))).toBe(true);
173
+ expect(fs.existsSync(path.join(projectDir, "init.sql"))).toBe(true);
174
+ expect(fs.existsSync(path.join(projectDir, "src", "stratum-sequelize.ts"))).toBe(true);
175
+ expect(fs.existsSync(path.join(projectDir, "src", "main.ts"))).toBe(true);
176
+ expect(fs.existsSync(path.join(projectDir, "src", "app.module.ts"))).toBe(true);
177
+ expect(fs.existsSync(path.join(projectDir, "src", "tenant.guard.ts"))).toBe(true);
178
+
179
+ const docker = fs.readFileSync(path.join(projectDir, "docker-compose.yml"), "utf8");
180
+ expect(docker).toContain("mysql:8");
181
+
182
+ const pkg = JSON.parse(fs.readFileSync(path.join(projectDir, "package.json"), "utf8"));
183
+ expect(pkg.dependencies["sequelize"]).toBeDefined();
184
+ expect(pkg.dependencies["@nestjs/core"]).toBeDefined();
185
+ expect(pkg.dependencies["@stratum-hq/mysql"]).toBeDefined();
186
+ expect(pkg.dependencies["@stratum-hq/nestjs"]).toBeDefined();
187
+ });
188
+
189
+ it("generates mysql-database-knex-express project", () => {
190
+ const preset: StackPreset = { database: "mysql", strategy: "database", orm: "knex", framework: "express" };
191
+ createPresetProject("test-project", preset, projectDir, true);
192
+
193
+ expect(fs.existsSync(path.join(projectDir, "knexfile.ts"))).toBe(true);
194
+ expect(fs.existsSync(path.join(projectDir, "src", "stratum-knex.ts"))).toBe(true);
195
+
196
+ const pkg = JSON.parse(fs.readFileSync(path.join(projectDir, "package.json"), "utf8"));
197
+ expect(pkg.dependencies["knex"]).toBeDefined();
198
+ expect(pkg.dependencies["express"]).toBeDefined();
199
+ });
200
+
201
+ // ── Next.js preset ──
202
+
203
+ it("generates postgres-rls-prisma-nextjs project", () => {
204
+ const preset: StackPreset = { database: "postgres", strategy: "rls", orm: "prisma", framework: "nextjs" };
205
+ createPresetProject("test-project", preset, projectDir, true);
206
+
207
+ expect(fs.existsSync(path.join(projectDir, "middleware.ts"))).toBe(true);
208
+ expect(fs.existsSync(path.join(projectDir, "src", "app", "page.tsx"))).toBe(true);
209
+ expect(fs.existsSync(path.join(projectDir, "prisma", "schema.prisma"))).toBe(true);
210
+
211
+ const pkg = JSON.parse(fs.readFileSync(path.join(projectDir, "package.json"), "utf8"));
212
+ expect(pkg.dependencies["next"]).toBeDefined();
213
+ expect(pkg.dependencies["react"]).toBeDefined();
214
+ expect(pkg.scripts.dev).toBe("next dev");
215
+ });
216
+
217
+ // ── "none" framework ──
218
+
219
+ it("generates postgres-rls-pg-none project", () => {
220
+ const preset: StackPreset = { database: "postgres", strategy: "rls", orm: "pg", framework: "none" };
221
+ createPresetProject("test-project", preset, projectDir, true);
222
+
223
+ expect(fs.existsSync(path.join(projectDir, "src", "index.ts"))).toBe(true);
224
+ expect(fs.existsSync(path.join(projectDir, "src", "stratum-db.ts"))).toBe(true);
225
+
226
+ const pkg = JSON.parse(fs.readFileSync(path.join(projectDir, "package.json"), "utf8"));
227
+ // Should not have any framework dependency
228
+ expect(pkg.dependencies["express"]).toBeUndefined();
229
+ expect(pkg.dependencies["fastify"]).toBeUndefined();
230
+ expect(pkg.dependencies["next"]).toBeUndefined();
231
+ expect(pkg.dependencies["hono"]).toBeUndefined();
232
+ expect(pkg.dependencies["@nestjs/core"]).toBeUndefined();
233
+ });
234
+
235
+ // ── README content ──
236
+
237
+ it("README mentions the preset stack", () => {
238
+ const preset: StackPreset = { database: "postgres", strategy: "rls", orm: "prisma", framework: "express" };
239
+ createPresetProject("test-project", preset, projectDir, true);
240
+
241
+ const readme = fs.readFileSync(path.join(projectDir, "README.md"), "utf8");
242
+ expect(readme).toContain("postgres");
243
+ expect(readme).toContain("rls");
244
+ expect(readme).toContain("prisma");
245
+ expect(readme).toContain("Row-Level Security");
246
+ });
247
+
248
+ // ── tsconfig.json ──
249
+
250
+ it("generates tsconfig with decorators for nestjs", () => {
251
+ const preset: StackPreset = { database: "postgres", strategy: "rls", orm: "prisma", framework: "nestjs" };
252
+ createPresetProject("test-project", preset, projectDir, true);
253
+
254
+ const tsconfig = JSON.parse(fs.readFileSync(path.join(projectDir, "tsconfig.json"), "utf8"));
255
+ expect(tsconfig.compilerOptions.experimentalDecorators).toBe(true);
256
+ expect(tsconfig.compilerOptions.emitDecoratorMetadata).toBe(true);
257
+ });
258
+
259
+ it("generates tsconfig with jsx for nextjs", () => {
260
+ const preset: StackPreset = { database: "postgres", strategy: "rls", orm: "prisma", framework: "nextjs" };
261
+ createPresetProject("test-project", preset, projectDir, true);
262
+
263
+ const tsconfig = JSON.parse(fs.readFileSync(path.join(projectDir, "tsconfig.json"), "utf8"));
264
+ expect(tsconfig.compilerOptions.jsx).toBe("preserve");
265
+ });
266
+ });