@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 +29 -0
- package/dist/index.js +4 -4
- package/dist/matrix.js +131 -0
- package/package.json +11 -6
- package/src/generators/db-setup.ts +4 -4
- package/src/__tests__/create.test.ts +0 -217
- package/src/__tests__/matrix.test.ts +0 -283
- package/src/__tests__/preset.test.ts +0 -266
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 {
|
|
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
|
|
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 {
|
|
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
|
|
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.
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
});
|