@stratum-hq/create 0.3.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # @stratum-hq/create
2
2
 
3
+ ## 0.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - b55ae70: Correct and complete package metadata for the npm registry listing.
8
+
9
+ Every published package now declares `license` (MIT), `author`, `homepage`, and
10
+ `bugs`. Runtime packages declare `engines` (Node >=20) to match the project's
11
+ support policy; this fixes `@stratum-hq/cli`, which previously declared Node >=18.
12
+ `@stratum-hq/mysql` and `@stratum-hq/mongodb` gain the `keywords` array they were
13
+ missing. No runtime code changes.
14
+
15
+ - c17b1a5: Point `@stratum-hq/create`'s `exports["./matrix"]` at built output instead of raw source (#219, from the #133 v1.0 surface review).
16
+
17
+ The `./matrix` subpath previously resolved (and shipped) `./src/matrix.ts` for both the `import` and `types` conditions, blessing a raw-source subpath unlike every other package. The build now emits `dist/matrix.js` and `./matrix` resolves there, matching the package's `.` entry. The stack-combination matrix API is unchanged.
18
+
19
+ - c17b1a5: Give the `@stratum-hq/db-adapters` barrel one consistent naming scheme for the tenant-context helpers (#219, from the #133 v1.0 surface review).
20
+
21
+ The barrel previously exposed the same "run in tenant context" concept under colliding names patched over with `as` aliases. The 1.0 names use an `<orm>` prefix so no export is an alias workaround:
22
+ - `withTenant` (Prisma) -> `prismaWithTenant`
23
+ - `withDrizzleTenant` -> `drizzleWithTenant`
24
+ - `withTenantScope` (Sequelize) -> `sequelizeWithTenantScope`
25
+ - `withDrizzleTenantScope` -> `drizzleWithTenantScope`
26
+ - `enableRLSMigration` (migration helper) -> `enableRLSForMigration` (distinct from the runtime `enableRLS`)
27
+
28
+ Behavior is identical; only the exported names change. The `@stratum-hq/cli` and `@stratum-hq/create` scaffolding templates emit the new `prismaWithTenant` name. Update your imports to the new names.
29
+
30
+ - 4adcbb5: Stop shipping test files in published tarballs. tsc-built packages now exclude **tests** directories and .test/.spec files from compilation, so dist and the tarball contain only real package output. The create package, which ships source for its ./matrix export, excludes tests via .npmignore instead. The vitest runner is unaffected and still runs tests from src.
31
+
3
32
  ## 0.3.0
4
33
 
5
34
  ### Minor Changes
package/dist/index.js CHANGED
@@ -283,7 +283,7 @@ model Tenant {
283
283
  content: `// Prisma client with Stratum tenant-scoped queries
284
284
  import { PrismaClient } from "@prisma/client";
285
285
  import { Pool } from "pg";
286
- import { withTenant } from "@stratum-hq/db-adapters";
286
+ import { prismaWithTenant } from "@stratum-hq/db-adapters";
287
287
 
288
288
  const prisma = new PrismaClient();
289
289
  const pool = new Pool({
@@ -293,7 +293,7 @@ const pool = new Pool({
293
293
  // Create a tenant-scoped Prisma client.
294
294
  // All queries through this client are automatically filtered by RLS.
295
295
  export function createTenantPrisma(getTenantId: () => string) {
296
- return withTenant(prisma, getTenantId, pool);
296
+ return prismaWithTenant(prisma, getTenantId, pool);
297
297
  }
298
298
 
299
299
  // Usage:
@@ -330,7 +330,7 @@ export const db = drizzle(connection);
330
330
  content: `// Drizzle ORM with Stratum tenant-scoped queries
331
331
  import { drizzle } from "drizzle-orm/node-postgres";
332
332
  import { Pool } from "pg";
333
- import { withTenantDrizzle } from "@stratum-hq/db-adapters";
333
+ import { drizzleWithTenant } from "@stratum-hq/db-adapters";
334
334
 
335
335
  const pool = new Pool({
336
336
  connectionString: process.env.DATABASE_URL,
@@ -341,7 +341,7 @@ export const db = drizzle(pool);
341
341
  // Create a tenant-scoped Drizzle instance.
342
342
  // Queries are automatically filtered by RLS policy.
343
343
  export function createTenantDb(getTenantId: () => string) {
344
- return withTenantDrizzle(db, getTenantId, pool);
344
+ return drizzleWithTenant(db, getTenantId, pool);
345
345
  }
346
346
 
347
347
  // Usage:
package/dist/matrix.js ADDED
@@ -0,0 +1,131 @@
1
+ // src/matrix.ts
2
+ var POSTGRES_STRATEGIES = ["rls", "schema", "database"];
3
+ var POSTGRES_ORMS = ["prisma", "drizzle", "sequelize", "knex", "pg"];
4
+ var MONGODB_STRATEGIES = ["database", "collection"];
5
+ var MONGODB_ORMS = ["mongoose"];
6
+ var MYSQL_STRATEGIES = ["database", "table-prefix"];
7
+ var MYSQL_ORMS = ["sequelize", "knex", "pg"];
8
+ var ALL_FRAMEWORKS = ["express", "fastify", "nextjs", "hono", "nestjs", "none"];
9
+ var VALID_COMBINATIONS = {
10
+ postgres: {
11
+ strategies: POSTGRES_STRATEGIES,
12
+ orms: POSTGRES_ORMS,
13
+ frameworks: ALL_FRAMEWORKS
14
+ },
15
+ mongodb: {
16
+ strategies: MONGODB_STRATEGIES,
17
+ orms: MONGODB_ORMS,
18
+ frameworks: ALL_FRAMEWORKS
19
+ },
20
+ mysql: {
21
+ strategies: MYSQL_STRATEGIES,
22
+ orms: MYSQL_ORMS,
23
+ frameworks: ALL_FRAMEWORKS
24
+ }
25
+ };
26
+ var ALL_DATABASES = ["postgres", "mongodb", "mysql"];
27
+ var ALL_STRATEGIES = ["rls", "schema", "database", "collection", "table-prefix"];
28
+ var ALL_ORMS = ["prisma", "drizzle", "sequelize", "knex", "mongoose", "pg"];
29
+ function isValidPreset(preset) {
30
+ const config = VALID_COMBINATIONS[preset.database];
31
+ if (!config)
32
+ return false;
33
+ if (!config.strategies.includes(preset.strategy))
34
+ return false;
35
+ if (!config.orms.includes(preset.orm))
36
+ return false;
37
+ if (!config.frameworks.includes(preset.framework))
38
+ return false;
39
+ return true;
40
+ }
41
+ function parsePresetString(s) {
42
+ if (!s || typeof s !== "string")
43
+ return null;
44
+ const parts = s.toLowerCase().split("-");
45
+ let database;
46
+ let strategy;
47
+ let orm;
48
+ let framework;
49
+ if (parts.length === 4) {
50
+ [database, strategy, orm, framework] = parts;
51
+ } else if (parts.length === 5) {
52
+ database = parts[0];
53
+ strategy = `${parts[1]}-${parts[2]}`;
54
+ orm = parts[3];
55
+ framework = parts[4];
56
+ } else {
57
+ return null;
58
+ }
59
+ if (!ALL_DATABASES.includes(database))
60
+ return null;
61
+ if (!ALL_STRATEGIES.includes(strategy))
62
+ return null;
63
+ if (!ALL_ORMS.includes(orm))
64
+ return null;
65
+ if (!ALL_FRAMEWORKS.includes(framework))
66
+ return null;
67
+ return {
68
+ database,
69
+ strategy,
70
+ orm,
71
+ framework
72
+ };
73
+ }
74
+ function formatPresetString(preset) {
75
+ return `${preset.database}-${preset.strategy}-${preset.orm}-${preset.framework}`;
76
+ }
77
+ function getValidOptions(partial) {
78
+ let databases = [...ALL_DATABASES];
79
+ let strategies = [];
80
+ let orms = [];
81
+ let frameworks = [];
82
+ if (partial.database) {
83
+ databases = [partial.database];
84
+ }
85
+ const strategySet = /* @__PURE__ */ new Set();
86
+ const ormSet = /* @__PURE__ */ new Set();
87
+ const frameworkSet = /* @__PURE__ */ new Set();
88
+ for (const db of databases) {
89
+ const config = VALID_COMBINATIONS[db];
90
+ if (partial.strategy && !config.strategies.includes(partial.strategy))
91
+ continue;
92
+ if (partial.orm && !config.orms.includes(partial.orm))
93
+ continue;
94
+ for (const s of config.strategies)
95
+ strategySet.add(s);
96
+ for (const o of config.orms)
97
+ ormSet.add(o);
98
+ for (const f of config.frameworks)
99
+ frameworkSet.add(f);
100
+ }
101
+ strategies = [...strategySet];
102
+ orms = [...ormSet];
103
+ frameworks = [...frameworkSet];
104
+ if (partial.strategy) {
105
+ strategies = strategies.filter((s) => s === partial.strategy);
106
+ }
107
+ if (partial.orm) {
108
+ orms = orms.filter((o) => o === partial.orm);
109
+ }
110
+ if (partial.framework) {
111
+ frameworks = frameworks.filter((f) => f === partial.framework);
112
+ }
113
+ if (partial.strategy || partial.orm) {
114
+ databases = databases.filter((db) => {
115
+ const config = VALID_COMBINATIONS[db];
116
+ if (partial.strategy && !config.strategies.includes(partial.strategy))
117
+ return false;
118
+ if (partial.orm && !config.orms.includes(partial.orm))
119
+ return false;
120
+ return true;
121
+ });
122
+ }
123
+ return { databases, strategies, orms, frameworks };
124
+ }
125
+ export {
126
+ VALID_COMBINATIONS,
127
+ formatPresetString,
128
+ getValidOptions,
129
+ isValidPreset,
130
+ parsePresetString
131
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stratum-hq/create",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Create a new Stratum multi-tenancy project",
5
5
  "keywords": [
6
6
  "multi-tenancy",
@@ -20,14 +20,13 @@
20
20
  },
21
21
  "exports": {
22
22
  ".": "./dist/index.js",
23
- "./matrix": {
24
- "import": "./src/matrix.ts",
25
- "types": "./src/matrix.ts"
26
- }
23
+ "./matrix": "./dist/matrix.js"
27
24
  },
28
25
  "type": "module",
29
26
  "scripts": {
30
- "build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@stratum-hq/*",
27
+ "build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@stratum-hq/* && esbuild src/matrix.ts --bundle --platform=node --format=esm --outfile=dist/matrix.js --external:@stratum-hq/*",
28
+ "typecheck": "tsc --noEmit",
29
+ "lint": "eslint .",
31
30
  "test": "vitest run"
32
31
  },
33
32
  "devDependencies": {
@@ -40,6 +39,12 @@
40
39
  "registry": "https://registry.npmjs.org/"
41
40
  },
42
41
  "license": "MIT",
42
+ "author": "Christian Crank",
43
+ "homepage": "https://github.com/stratum-hq/Stratum/tree/main/packages/create#readme",
44
+ "bugs": "https://github.com/stratum-hq/Stratum/issues",
45
+ "engines": {
46
+ "node": ">=20.0.0"
47
+ },
43
48
  "repository": {
44
49
  "type": "git",
45
50
  "url": "https://github.com/stratum-hq/Stratum.git",
@@ -58,7 +58,7 @@ model Tenant {
58
58
  content: `// Prisma client with Stratum tenant-scoped queries
59
59
  import { PrismaClient } from "@prisma/client";
60
60
  import { Pool } from "pg";
61
- import { withTenant } from "@stratum-hq/db-adapters";
61
+ import { prismaWithTenant } from "@stratum-hq/db-adapters";
62
62
 
63
63
  const prisma = new PrismaClient();
64
64
  const pool = new Pool({
@@ -68,7 +68,7 @@ const pool = new Pool({
68
68
  // Create a tenant-scoped Prisma client.
69
69
  // All queries through this client are automatically filtered by RLS.
70
70
  export function createTenantPrisma(getTenantId: () => string) {
71
- return withTenant(prisma, getTenantId, pool);
71
+ return prismaWithTenant(prisma, getTenantId, pool);
72
72
  }
73
73
 
74
74
  // Usage:
@@ -107,7 +107,7 @@ export const db = drizzle(connection);
107
107
  content: `// Drizzle ORM with Stratum tenant-scoped queries
108
108
  import { drizzle } from "drizzle-orm/node-postgres";
109
109
  import { Pool } from "pg";
110
- import { withTenantDrizzle } from "@stratum-hq/db-adapters";
110
+ import { drizzleWithTenant } from "@stratum-hq/db-adapters";
111
111
 
112
112
  const pool = new Pool({
113
113
  connectionString: process.env.DATABASE_URL,
@@ -118,7 +118,7 @@ export const db = drizzle(pool);
118
118
  // Create a tenant-scoped Drizzle instance.
119
119
  // Queries are automatically filtered by RLS policy.
120
120
  export function createTenantDb(getTenantId: () => string) {
121
- return withTenantDrizzle(db, getTenantId, pool);
121
+ return drizzleWithTenant(db, getTenantId, pool);
122
122
  }
123
123
 
124
124
  // Usage:
@@ -1,217 +0,0 @@
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, validateProjectName } from "../index.js";
6
-
7
- // ─── parseArgs tests ──────────────────────────────────────────────────────────
8
-
9
- describe("parseArgs", () => {
10
- it("extracts project name from positional arg", () => {
11
- const result = parseArgs(["my-app"]);
12
- expect(result.projectName).toBe("my-app");
13
- });
14
-
15
- it("defaults template to express", () => {
16
- const result = parseArgs(["my-app"]);
17
- expect(result.template).toBe("express");
18
- });
19
-
20
- it("parses --template flag", () => {
21
- const result = parseArgs(["my-app", "--template", "fastify"]);
22
- expect(result.template).toBe("fastify");
23
- });
24
-
25
- it("parses --template=value syntax", () => {
26
- const result = parseArgs(["my-app", "--template=nextjs"]);
27
- expect(result.template).toBe("nextjs");
28
- });
29
-
30
- it("parses --skip-install flag", () => {
31
- const result = parseArgs(["my-app", "--skip-install"]);
32
- expect(result.skipInstall).toBe(true);
33
- });
34
-
35
- it("parses --force flag", () => {
36
- const result = parseArgs(["my-app", "--force"]);
37
- expect(result.force).toBe(true);
38
- });
39
-
40
- it("returns null projectName when no positional arg given", () => {
41
- const result = parseArgs(["--template", "express"]);
42
- expect(result.projectName).toBeNull();
43
- });
44
-
45
- it("skipInstall defaults to false", () => {
46
- const result = parseArgs(["my-app"]);
47
- expect(result.skipInstall).toBe(false);
48
- });
49
-
50
- it("force defaults to false", () => {
51
- const result = parseArgs(["my-app"]);
52
- expect(result.force).toBe(false);
53
- });
54
- });
55
-
56
- // ─── Error cases ──────────────────────────────────────────────────────────────
57
-
58
- describe("main arg validation", () => {
59
- it("errors when no project name given", () => {
60
- // parseArgs returns null projectName, which main() would exit on
61
- const result = parseArgs([]);
62
- expect(result.projectName).toBeNull();
63
- });
64
- });
65
-
66
- // ─── File generation tests ────────────────────────────────────────────────────
67
-
68
- describe("createProject", () => {
69
- let tmpDir: string;
70
- let projectDir: string;
71
-
72
- beforeEach(() => {
73
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "stratum-create-test-"));
74
- projectDir = path.join(tmpDir, "test-project");
75
- fs.mkdirSync(projectDir, { recursive: true });
76
- });
77
-
78
- afterEach(() => {
79
- fs.rmSync(tmpDir, { recursive: true, force: true });
80
- });
81
-
82
- it("generates correct files for express template", () => {
83
- createProject("test-project", "express", projectDir, true);
84
-
85
- expect(fs.existsSync(path.join(projectDir, "package.json"))).toBe(true);
86
- expect(fs.existsSync(path.join(projectDir, "docker-compose.yml"))).toBe(true);
87
- expect(fs.existsSync(path.join(projectDir, ".env.example"))).toBe(true);
88
- expect(fs.existsSync(path.join(projectDir, "src", "index.ts"))).toBe(true);
89
- expect(fs.existsSync(path.join(projectDir, "README.md"))).toBe(true);
90
- expect(fs.existsSync(path.join(projectDir, "init.sql"))).toBe(true);
91
- });
92
-
93
- it("generates correct files for fastify template", () => {
94
- createProject("test-project", "fastify", projectDir, true);
95
-
96
- expect(fs.existsSync(path.join(projectDir, "src", "index.ts"))).toBe(true);
97
- const serverContent = fs.readFileSync(
98
- path.join(projectDir, "src", "index.ts"),
99
- "utf8",
100
- );
101
- expect(serverContent).toContain("Fastify");
102
- });
103
-
104
- it("generates correct files for nextjs template", () => {
105
- createProject("test-project", "nextjs", projectDir, true);
106
-
107
- expect(fs.existsSync(path.join(projectDir, "src", "app", "page.tsx"))).toBe(true);
108
- expect(fs.existsSync(path.join(projectDir, "middleware.ts"))).toBe(true);
109
- });
110
-
111
- it("package.json contains @stratum-hq/lib dependency", () => {
112
- createProject("test-project", "express", projectDir, true);
113
-
114
- const pkgContent = fs.readFileSync(path.join(projectDir, "package.json"), "utf8");
115
- const pkg = JSON.parse(pkgContent);
116
- expect(pkg.dependencies["@stratum-hq/lib"]).toBeDefined();
117
- });
118
-
119
- it("package.json contains pg dependency", () => {
120
- createProject("test-project", "express", projectDir, true);
121
-
122
- const pkgContent = fs.readFileSync(path.join(projectDir, "package.json"), "utf8");
123
- const pkg = JSON.parse(pkgContent);
124
- expect(pkg.dependencies["pg"]).toBeDefined();
125
- });
126
-
127
- it("package.json contains express dependency for express template", () => {
128
- createProject("test-project", "express", projectDir, true);
129
-
130
- const pkgContent = fs.readFileSync(path.join(projectDir, "package.json"), "utf8");
131
- const pkg = JSON.parse(pkgContent);
132
- expect(pkg.dependencies["express"]).toBeDefined();
133
- });
134
-
135
- it("docker-compose.yml contains PostgreSQL 16", () => {
136
- createProject("test-project", "express", projectDir, true);
137
-
138
- const dockerContent = fs.readFileSync(
139
- path.join(projectDir, "docker-compose.yml"),
140
- "utf8",
141
- );
142
- expect(dockerContent).toContain("postgres:16-alpine");
143
- });
144
-
145
- it("init.sql enables uuid-ossp and ltree extensions", () => {
146
- createProject("test-project", "express", projectDir, true);
147
-
148
- const sqlContent = fs.readFileSync(path.join(projectDir, "init.sql"), "utf8");
149
- expect(sqlContent).toContain("uuid-ossp");
150
- expect(sqlContent).toContain("ltree");
151
- });
152
-
153
- it(".env.example contains DATABASE_URL", () => {
154
- createProject("test-project", "express", projectDir, true);
155
-
156
- const envContent = fs.readFileSync(path.join(projectDir, ".env.example"), "utf8");
157
- expect(envContent).toContain("DATABASE_URL");
158
- });
159
-
160
- it("default template is express", () => {
161
- const result = parseArgs(["my-app"]);
162
- expect(result.template).toBe("express");
163
- });
164
- });
165
-
166
- // ─── Project name validation ──────────────────────────────────────────────────
167
-
168
- describe("validateProjectName", () => {
169
- it("accepts a valid name", () => {
170
- expect(validateProjectName("valid-name")).toBeNull();
171
- });
172
-
173
- it("accepts names with dots and underscores", () => {
174
- expect(validateProjectName("my.app_1")).toBeNull();
175
- });
176
-
177
- it("rejects empty string", () => {
178
- expect(validateProjectName("")).not.toBeNull();
179
- });
180
-
181
- it("rejects name with spaces", () => {
182
- expect(validateProjectName("name with spaces")).not.toBeNull();
183
- });
184
-
185
- it("rejects path traversal with ../", () => {
186
- expect(validateProjectName("../escape")).not.toBeNull();
187
- });
188
-
189
- it("rejects absolute path", () => {
190
- expect(validateProjectName("/absolute/path")).not.toBeNull();
191
- });
192
-
193
- it("rejects name starting with a dot", () => {
194
- expect(validateProjectName(".hidden")).not.toBeNull();
195
- });
196
-
197
- it("rejects name with path separator embedded", () => {
198
- expect(validateProjectName("foo/bar")).not.toBeNull();
199
- });
200
- });
201
-
202
- // ─── Directory existence error ────────────────────────────────────────────────
203
-
204
- describe("directory existence check", () => {
205
- it("errors when directory exists without --force", () => {
206
- // parseArgs doesn't control this — main() does the fs.existsSync check.
207
- // We verify that parseArgs returns force=false by default, which triggers the error.
208
- const result = parseArgs(["existing-dir"]);
209
- expect(result.force).toBe(false);
210
- // When force is false and dir exists, main() calls process.exit(1)
211
- });
212
-
213
- it("--force flag allows overwriting existing directory", () => {
214
- const result = parseArgs(["existing-dir", "--force"]);
215
- expect(result.force).toBe(true);
216
- });
217
- });
@@ -1,283 +0,0 @@
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
- });
@@ -1,266 +0,0 @@
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
- });