@stratum-hq/create 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +31 -7
- package/dist/index.js +270 -191
- package/package.json +17 -3
- package/src/generators/db-setup.ts +43 -1
- package/src/generators/init-sql.ts +56 -11
- package/src/generators/middleware.ts +50 -18
- package/src/generators/package-json.ts +24 -13
- package/src/generators/readme.ts +11 -8
- package/src/generators/tsconfig.ts +12 -6
- package/src/index.ts +58 -102
- package/src/matrix.ts +1 -1
- package/src/preset-project.ts +10 -2
package/package.json
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stratum-hq/create",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Scaffold a multi-tenant SaaS starter for Node.js: PostgreSQL, MySQL, or MongoDB with Express, Fastify, Next.js, Hono, or NestJS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"multi-tenancy",
|
|
7
|
+
"multitenancy",
|
|
7
8
|
"multi-tenant",
|
|
8
9
|
"saas",
|
|
9
10
|
"tenant",
|
|
11
|
+
"tenant-isolation",
|
|
12
|
+
"row-level-security",
|
|
13
|
+
"rls",
|
|
14
|
+
"postgresql",
|
|
15
|
+
"mysql",
|
|
16
|
+
"mongodb",
|
|
17
|
+
"express",
|
|
18
|
+
"fastify",
|
|
19
|
+
"nextjs",
|
|
20
|
+
"hono",
|
|
21
|
+
"nestjs",
|
|
22
|
+
"prisma",
|
|
23
|
+
"drizzle",
|
|
10
24
|
"create",
|
|
11
25
|
"scaffolding",
|
|
12
26
|
"starter",
|
|
@@ -40,7 +54,7 @@
|
|
|
40
54
|
},
|
|
41
55
|
"license": "MIT",
|
|
42
56
|
"author": "Christian Crank",
|
|
43
|
-
"homepage": "https://
|
|
57
|
+
"homepage": "https://docs.stratum-hq.org/packages/create/",
|
|
44
58
|
"bugs": "https://github.com/stratum-hq/Stratum/issues",
|
|
45
59
|
"engines": {
|
|
46
60
|
"node": ">=20.0.0"
|
|
@@ -128,6 +128,15 @@ export { pool };
|
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
files.push({
|
|
132
|
+
filename: "src/schema.ts",
|
|
133
|
+
content: drizzleSchema(preset),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// drizzle-kit creates tables, so on PostgreSQL it connects as the superuser
|
|
137
|
+
// in DATABASE_SUPERUSER_URL, kept for migrations. A table the app role owned
|
|
138
|
+
// would not be subject to its own RLS policies; init.sql grants the app role
|
|
139
|
+
// access to the tables the superuser creates.
|
|
131
140
|
files.push({
|
|
132
141
|
filename: "drizzle.config.ts",
|
|
133
142
|
content: `import type { Config } from "drizzle-kit";
|
|
@@ -137,7 +146,7 @@ export default {
|
|
|
137
146
|
out: "./drizzle",
|
|
138
147
|
${preset.database === "mysql" ? 'dialect: "mysql",' : 'dialect: "postgresql",'}
|
|
139
148
|
dbCredentials: {
|
|
140
|
-
url: process.env.DATABASE_URL
|
|
149
|
+
url: ${preset.database === "mysql" ? "process.env.DATABASE_URL!" : "(process.env.DATABASE_SUPERUSER_URL ?? process.env.DATABASE_URL)!"},
|
|
141
150
|
},
|
|
142
151
|
} satisfies Config;
|
|
143
152
|
`,
|
|
@@ -146,6 +155,39 @@ export default {
|
|
|
146
155
|
return files;
|
|
147
156
|
}
|
|
148
157
|
|
|
158
|
+
/** src/schema.ts, the Drizzle table definitions that drizzle.config.ts points at. */
|
|
159
|
+
function drizzleSchema(preset: StackPreset): string {
|
|
160
|
+
if (preset.database === "mysql") {
|
|
161
|
+
return `// Drizzle table definitions. drizzle.config.ts reads this file.
|
|
162
|
+
import { mysqlTable, varchar, text, timestamp } from "drizzle-orm/mysql-core";
|
|
163
|
+
|
|
164
|
+
// An example tenant-scoped table: every row carries the tenant it belongs to.
|
|
165
|
+
export const notes = mysqlTable("notes", {
|
|
166
|
+
id: varchar("id", { length: 36 }).primaryKey(),
|
|
167
|
+
tenantId: varchar("tenant_id", { length: 36 }).notNull(),
|
|
168
|
+
body: text("body").notNull(),
|
|
169
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
170
|
+
});
|
|
171
|
+
`;
|
|
172
|
+
}
|
|
173
|
+
return `// Drizzle table definitions. drizzle.config.ts reads this file.
|
|
174
|
+
import { pgTable, uuid, text, timestamp, index } from "drizzle-orm/pg-core";
|
|
175
|
+
|
|
176
|
+
// An example tenant-scoped table: every row carries the tenant it belongs to.
|
|
177
|
+
// Stratum's own tables, such as tenants, are created by Stratum, not here.
|
|
178
|
+
export const notes = pgTable(
|
|
179
|
+
"notes",
|
|
180
|
+
{
|
|
181
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
182
|
+
tenantId: uuid("tenant_id").notNull(),
|
|
183
|
+
body: text("body").notNull(),
|
|
184
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
185
|
+
},
|
|
186
|
+
(table) => [index("notes_tenant_id_idx").on(table.tenantId)],
|
|
187
|
+
);
|
|
188
|
+
`;
|
|
189
|
+
}
|
|
190
|
+
|
|
149
191
|
function generateSequelizeSetup(preset: StackPreset): DbSetupFile[] {
|
|
150
192
|
const dialect = preset.database === "mysql" ? "mysql" : "postgres";
|
|
151
193
|
|
|
@@ -9,17 +9,37 @@ export function postgresAppRole(dbName: string): string {
|
|
|
9
9
|
export const POSTGRES_APP_PASSWORD = "dev_app_password";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
|
|
12
|
+
* Name of the login Stratum itself connects as: the library's adminPool,
|
|
13
|
+
* which runs the Stratum migrations. It is a member of the control role of
|
|
14
|
+
* @stratum-hq/lib migration 032, not a superuser.
|
|
15
|
+
*/
|
|
16
|
+
export function postgresStratumRole(dbName: string): string {
|
|
17
|
+
return `${dbName}_stratum`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Local development password for the generated Stratum login. */
|
|
21
|
+
export const POSTGRES_STRATUM_PASSWORD = "dev_stratum_password";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* SQL that creates the roles of the hardened role model of @stratum-hq/lib
|
|
25
|
+
* (the "Hardening: separate admin and app roles" guide), as docker/init-db.sql
|
|
26
|
+
* does. POSTGRES_USER is a superuser, and a superuser (or any BYPASSRLS
|
|
27
|
+
* role) ignores every row-level security policy, FORCE included. So the app
|
|
28
|
+
* gets its own role, Stratum gets its own login for its tables, and the
|
|
29
|
+
* superuser is kept for bootstrap and the application's own migrations.
|
|
16
30
|
*/
|
|
17
31
|
export function postgresAppRoleSql(dbName: string, strategy?: string): string {
|
|
18
32
|
const role = postgresAppRole(dbName);
|
|
33
|
+
const stratum = postgresStratumRole(dbName);
|
|
19
34
|
let strategyGrant = "";
|
|
20
35
|
if (strategy === "schema") {
|
|
21
36
|
strategyGrant = `
|
|
22
|
-
-- schema-per-tenant: the app creates one schema per tenant
|
|
37
|
+
-- schema-per-tenant: the app creates one schema per tenant. A schema named
|
|
38
|
+
-- like a login comes first on that login's default search path ("$user",
|
|
39
|
+
-- public), so a schema the app creates could come before public for the
|
|
40
|
+
-- Stratum login or the bootstrap superuser. Both search only public.
|
|
41
|
+
ALTER ROLE ${stratum} IN DATABASE ${dbName} SET search_path = public;
|
|
42
|
+
ALTER ROLE CURRENT_USER IN DATABASE ${dbName} SET search_path = public;
|
|
23
43
|
GRANT CREATE ON DATABASE ${dbName} TO ${role};
|
|
24
44
|
`;
|
|
25
45
|
} else if (strategy === "database") {
|
|
@@ -29,15 +49,40 @@ ALTER ROLE ${role} CREATEDB;
|
|
|
29
49
|
`;
|
|
30
50
|
}
|
|
31
51
|
return `
|
|
52
|
+
-- Stratum's control role (migration 032 of @stratum-hq/lib). Every Stratum
|
|
53
|
+
-- table gets a policy for it, and only its members reach rows across
|
|
54
|
+
-- tenants.
|
|
55
|
+
CREATE ROLE stratum_control NOLOGIN NOSUPERUSER NOBYPASSRLS;
|
|
56
|
+
GRANT USAGE, CREATE ON SCHEMA public TO stratum_control;
|
|
57
|
+
|
|
58
|
+
-- Stratum's own login: pass it to the library as adminPool, which runs the
|
|
59
|
+
-- Stratum migrations, so it owns the Stratum tables. Not a superuser and no
|
|
60
|
+
-- BYPASSRLS: it reaches them as a member of stratum_control. Do not run the
|
|
61
|
+
-- Stratum migrations as the bootstrap superuser: the default privileges
|
|
62
|
+
-- below would give the app role write access to the Stratum tables. If that
|
|
63
|
+
-- happened, "stratum db roles --apply --app-role ${role}" removes it.
|
|
64
|
+
CREATE ROLE ${stratum} WITH LOGIN PASSWORD '${POSTGRES_STRATUM_PASSWORD}' NOSUPERUSER NOBYPASSRLS;
|
|
65
|
+
GRANT stratum_control TO ${stratum} WITH INHERIT TRUE, SET TRUE;
|
|
66
|
+
GRANT CONNECT ON DATABASE ${dbName} TO ${stratum};
|
|
67
|
+
GRANT USAGE, CREATE ON SCHEMA public TO ${stratum};
|
|
68
|
+
|
|
32
69
|
-- Application role. The app connects as ${role} (DATABASE_URL), never as the
|
|
33
|
-
-- bootstrap superuser (
|
|
34
|
-
-- ignores every row-level security policy, FORCE included.
|
|
35
|
-
--
|
|
70
|
+
-- bootstrap superuser (DATABASE_SUPERUSER_URL): a superuser or BYPASSRLS role
|
|
71
|
+
-- ignores every row-level security policy, FORCE included. It creates no
|
|
72
|
+
-- objects in public: the superuser creates the application's tables (its
|
|
73
|
+
-- migrations), and the app role reads and writes the ones the superuser
|
|
74
|
+
-- creates. It gets nothing on the tables ${stratum} creates; to let it read
|
|
75
|
+
-- Stratum's read list once Stratum has migrated, run:
|
|
76
|
+
-- stratum db roles --apply --admin-role ${stratum} --app-role ${role}
|
|
36
77
|
CREATE ROLE ${role} WITH LOGIN PASSWORD '${POSTGRES_APP_PASSWORD}' NOSUPERUSER NOBYPASSRLS NOCREATEROLE;
|
|
37
78
|
GRANT CONNECT ON DATABASE ${dbName} TO ${role};
|
|
38
|
-
GRANT USAGE
|
|
39
|
-
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${role};
|
|
40
|
-
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO ${role};
|
|
79
|
+
GRANT USAGE ON SCHEMA public TO ${role};
|
|
80
|
+
ALTER DEFAULT PRIVILEGES FOR ROLE ${dbName} IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${role};
|
|
81
|
+
ALTER DEFAULT PRIVILEGES FOR ROLE ${dbName} IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO ${role};
|
|
82
|
+
|
|
83
|
+
-- Only roles granted CREATE by name create objects in public. PostgreSQL 15
|
|
84
|
+
-- and later already start this way; older versions grant it to PUBLIC.
|
|
85
|
+
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
|
|
41
86
|
${strategyGrant}`;
|
|
42
87
|
}
|
|
43
88
|
|
|
@@ -50,8 +50,10 @@ const TENANT_REQUIRED = `{ error: "A bearer token with a tenant_id claim is requ
|
|
|
50
50
|
* x-tenant-id. The subdomain is forwarded as x-tenant-slug, never as the ID.
|
|
51
51
|
*/
|
|
52
52
|
export function nextjsTenantMiddleware(): string {
|
|
53
|
-
return `// middleware.ts
|
|
54
|
-
//
|
|
53
|
+
return `// src/middleware.ts: Next.js middleware for Stratum tenant resolution
|
|
54
|
+
//
|
|
55
|
+
// Next.js runs middleware only from the directory that holds the app
|
|
56
|
+
// directory: src/middleware.ts for src/app, middleware.ts for app.
|
|
55
57
|
//
|
|
56
58
|
// The tenant ID comes only from the tenant_id claim of a bearer token that
|
|
57
59
|
// verifies with JWT_SECRET, and is forwarded as x-tenant-id. Any copy of the
|
|
@@ -127,6 +129,24 @@ export const config = {
|
|
|
127
129
|
`;
|
|
128
130
|
}
|
|
129
131
|
|
|
132
|
+
/**
|
|
133
|
+
* The root layout of a generated Next.js app, src/app/layout.tsx. \`next build\`
|
|
134
|
+
* refuses an app directory without one.
|
|
135
|
+
*/
|
|
136
|
+
export function nextjsRootLayout(projectName: string): string {
|
|
137
|
+
return `// app/layout.tsx: ${projectName} root layout
|
|
138
|
+
import type { ReactNode } from "react";
|
|
139
|
+
|
|
140
|
+
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
141
|
+
return (
|
|
142
|
+
<html lang="en">
|
|
143
|
+
<body>{children}</body>
|
|
144
|
+
</html>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
`;
|
|
148
|
+
}
|
|
149
|
+
|
|
130
150
|
export function generateMiddleware(projectName: string, preset: StackPreset): MiddlewareFile[] {
|
|
131
151
|
switch (preset.framework) {
|
|
132
152
|
case "express":
|
|
@@ -145,10 +165,16 @@ export function generateMiddleware(projectName: string, preset: StackPreset): Mi
|
|
|
145
165
|
}
|
|
146
166
|
|
|
147
167
|
function generateExpressMiddleware(projectName: string): MiddlewareFile[] {
|
|
148
|
-
return [
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
168
|
+
return [{ filename: "src/index.ts", content: expressServer(projectName) }];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* src/index.ts of a generated Express server, for the express template and
|
|
173
|
+
* every express preset: tenant middleware that takes the tenant from a
|
|
174
|
+
* verified JWT, and a /tenants route that requires a tenant.
|
|
175
|
+
*/
|
|
176
|
+
export function expressServer(projectName: string): string {
|
|
177
|
+
return `import express from "express";
|
|
152
178
|
${VERIFIED_TENANT}
|
|
153
179
|
|
|
154
180
|
const app = express();
|
|
@@ -184,16 +210,20 @@ app.get("/tenants", async (req, res) => {
|
|
|
184
210
|
app.listen(port, () => {
|
|
185
211
|
console.log(\`${projectName} running on http://localhost:\${port}\`);
|
|
186
212
|
});
|
|
187
|
-
|
|
188
|
-
},
|
|
189
|
-
];
|
|
213
|
+
`;
|
|
190
214
|
}
|
|
191
215
|
|
|
192
216
|
function generateFastifyMiddleware(projectName: string): MiddlewareFile[] {
|
|
193
|
-
return [
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
217
|
+
return [{ filename: "src/index.ts", content: fastifyServer(projectName) }];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* src/index.ts of a generated Fastify server, for the fastify template and
|
|
222
|
+
* every fastify preset: an onRequest hook that takes the tenant from a
|
|
223
|
+
* verified JWT, and a /tenants route that requires a tenant.
|
|
224
|
+
*/
|
|
225
|
+
export function fastifyServer(projectName: string): string {
|
|
226
|
+
return `import Fastify from "fastify";
|
|
197
227
|
${VERIFIED_TENANT}
|
|
198
228
|
|
|
199
229
|
const fastify = Fastify({ logger: true });
|
|
@@ -228,17 +258,19 @@ fastify.listen({ port, host: "0.0.0.0" }, (err) => {
|
|
|
228
258
|
process.exit(1);
|
|
229
259
|
}
|
|
230
260
|
});
|
|
231
|
-
|
|
232
|
-
},
|
|
233
|
-
];
|
|
261
|
+
`;
|
|
234
262
|
}
|
|
235
263
|
|
|
236
264
|
function generateNextjsMiddleware(projectName: string): MiddlewareFile[] {
|
|
237
265
|
return [
|
|
238
266
|
{
|
|
239
|
-
filename: "middleware.ts",
|
|
267
|
+
filename: "src/middleware.ts",
|
|
240
268
|
content: nextjsTenantMiddleware(),
|
|
241
269
|
},
|
|
270
|
+
{
|
|
271
|
+
filename: "src/app/layout.tsx",
|
|
272
|
+
content: nextjsRootLayout(projectName),
|
|
273
|
+
},
|
|
242
274
|
{
|
|
243
275
|
filename: "src/app/page.tsx",
|
|
244
276
|
content: `// app/page.tsx - ${projectName} root page
|
|
@@ -249,7 +281,7 @@ export default function Home() {
|
|
|
249
281
|
<p>Multi-tenant app powered by Stratum.</p>
|
|
250
282
|
<ul>
|
|
251
283
|
<li>Configure tenants via the Stratum control plane</li>
|
|
252
|
-
<li>The tenant comes from a verified JWT in <code>middleware.ts</code></li>
|
|
284
|
+
<li>The tenant comes from a verified JWT in <code>src/middleware.ts</code></li>
|
|
253
285
|
<li>Use <code>@stratum-hq/lib</code> for tenant resolution</li>
|
|
254
286
|
</ul>
|
|
255
287
|
</main>
|
|
@@ -2,6 +2,16 @@ import type { StackPreset } from "../matrix.js";
|
|
|
2
2
|
import { rootSources } from "./tsconfig.js";
|
|
3
3
|
import { STRATUM_RANGES } from "../stratum-versions.js";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* drizzle-kit 0.31 still depends on the deprecated @esbuild-kit/esm-loader,
|
|
7
|
+
* whose @esbuild-kit/core-utils pins esbuild ~0.18, which has a published
|
|
8
|
+
* advisory (GHSA-67mh-4wv8-2f99). drizzle-kit itself uses esbuild ^0.25, so
|
|
9
|
+
* the override moves the nested copy to that line too.
|
|
10
|
+
*/
|
|
11
|
+
const DRIZZLE_KIT_OVERRIDES = {
|
|
12
|
+
"@esbuild-kit/core-utils": { esbuild: "^0.25.4" },
|
|
13
|
+
};
|
|
14
|
+
|
|
5
15
|
export function generatePresetPackageJson(projectName: string, preset: StackPreset): string {
|
|
6
16
|
const deps: Record<string, string> = {
|
|
7
17
|
"@stratum-hq/lib": STRATUM_RANGES["@stratum-hq/lib"],
|
|
@@ -14,7 +24,7 @@ export function generatePresetPackageJson(projectName: string, preset: StackPres
|
|
|
14
24
|
if (preset.framework === "nestjs") {
|
|
15
25
|
devDeps["tsc-watch"] = "^7.2.0";
|
|
16
26
|
} else if (preset.framework !== "nextjs") {
|
|
17
|
-
devDeps["tsx"] = "^4.
|
|
27
|
+
devDeps["tsx"] = "^4.19.3";
|
|
18
28
|
}
|
|
19
29
|
|
|
20
30
|
// Database driver deps
|
|
@@ -40,6 +50,7 @@ export function generatePresetPackageJson(projectName: string, preset: StackPres
|
|
|
40
50
|
scripts,
|
|
41
51
|
dependencies: sortKeys(deps),
|
|
42
52
|
devDependencies: sortKeys(devDeps),
|
|
53
|
+
...(preset.orm === "drizzle" ? { overrides: DRIZZLE_KIT_OVERRIDES } : {}),
|
|
43
54
|
engines: {
|
|
44
55
|
node: ">=20.0.0",
|
|
45
56
|
},
|
|
@@ -76,7 +87,7 @@ function addDatabaseDeps(deps: Record<string, string>, devDeps: Record<string, s
|
|
|
76
87
|
break;
|
|
77
88
|
case "mysql":
|
|
78
89
|
if (preset.orm === "drizzle" || preset.orm === "pg") {
|
|
79
|
-
deps["mysql2"] = "^3.
|
|
90
|
+
deps["mysql2"] = "^3.23.1";
|
|
80
91
|
}
|
|
81
92
|
break;
|
|
82
93
|
}
|
|
@@ -92,8 +103,8 @@ function addOrmDeps(deps: Record<string, string>, devDeps: Record<string, string
|
|
|
92
103
|
}
|
|
93
104
|
break;
|
|
94
105
|
case "drizzle":
|
|
95
|
-
deps["drizzle-orm"] = "^0.
|
|
96
|
-
devDeps["drizzle-kit"] = "^0.
|
|
106
|
+
deps["drizzle-orm"] = "^0.45.3";
|
|
107
|
+
devDeps["drizzle-kit"] = "^0.31.11";
|
|
97
108
|
if (preset.database === "postgres") {
|
|
98
109
|
deps["pg"] = "^8.11.0";
|
|
99
110
|
}
|
|
@@ -101,7 +112,7 @@ function addOrmDeps(deps: Record<string, string>, devDeps: Record<string, string
|
|
|
101
112
|
case "sequelize":
|
|
102
113
|
deps["sequelize"] = "^6.37.0";
|
|
103
114
|
if (preset.database === "mysql") {
|
|
104
|
-
deps["mysql2"] = "^3.
|
|
115
|
+
deps["mysql2"] = "^3.23.1";
|
|
105
116
|
} else {
|
|
106
117
|
deps["pg"] = "^8.11.0";
|
|
107
118
|
}
|
|
@@ -110,7 +121,7 @@ function addOrmDeps(deps: Record<string, string>, devDeps: Record<string, string
|
|
|
110
121
|
deps["knex"] = "^3.1.0";
|
|
111
122
|
break;
|
|
112
123
|
case "mongoose":
|
|
113
|
-
deps["mongoose"] = "^8.
|
|
124
|
+
deps["mongoose"] = "^8.24.1";
|
|
114
125
|
break;
|
|
115
126
|
case "pg":
|
|
116
127
|
// pg already handled in addDatabaseDeps
|
|
@@ -125,11 +136,11 @@ function addFrameworkDeps(deps: Record<string, string>, devDeps: Record<string,
|
|
|
125
136
|
}
|
|
126
137
|
switch (preset.framework) {
|
|
127
138
|
case "express":
|
|
128
|
-
deps["express"] = "^4.
|
|
139
|
+
deps["express"] = "^4.22.3";
|
|
129
140
|
devDeps["@types/express"] = "^4.17.21";
|
|
130
141
|
break;
|
|
131
142
|
case "fastify":
|
|
132
|
-
deps["fastify"] = "^
|
|
143
|
+
deps["fastify"] = "^5.12.5";
|
|
133
144
|
break;
|
|
134
145
|
case "nextjs":
|
|
135
146
|
deps["next"] = "^15.5.16";
|
|
@@ -139,13 +150,13 @@ function addFrameworkDeps(deps: Record<string, string>, devDeps: Record<string,
|
|
|
139
150
|
devDeps["@types/react-dom"] = "^19.0.0";
|
|
140
151
|
break;
|
|
141
152
|
case "hono":
|
|
142
|
-
deps["hono"] = "^4.
|
|
143
|
-
deps["@hono/node-server"] = "^1.
|
|
153
|
+
deps["hono"] = "^4.13.7";
|
|
154
|
+
deps["@hono/node-server"] = "^1.19.15";
|
|
144
155
|
break;
|
|
145
156
|
case "nestjs":
|
|
146
|
-
deps["@nestjs/core"] = "^11.
|
|
147
|
-
deps["@nestjs/common"] = "^11.
|
|
148
|
-
deps["@nestjs/platform-express"] = "^11.
|
|
157
|
+
deps["@nestjs/core"] = "^11.1.18";
|
|
158
|
+
deps["@nestjs/common"] = "^11.1.18";
|
|
159
|
+
deps["@nestjs/platform-express"] = "^11.1.18";
|
|
149
160
|
deps["reflect-metadata"] = "^0.2.0";
|
|
150
161
|
deps["rxjs"] = "^7.8.0";
|
|
151
162
|
break;
|
package/src/generators/readme.ts
CHANGED
|
@@ -73,12 +73,15 @@ npx prisma db push
|
|
|
73
73
|
}
|
|
74
74
|
if (preset.orm === "drizzle") {
|
|
75
75
|
return `
|
|
76
|
-
### 3b.
|
|
76
|
+
### 3b. Create the tables
|
|
77
|
+
|
|
78
|
+
The tables are defined in \`src/schema.ts\`, which \`drizzle.config.ts\` points at.
|
|
79
|
+
drizzle-kit does not read \`.env\`, so load it first:
|
|
77
80
|
|
|
78
81
|
\`\`\`bash
|
|
79
|
-
|
|
82
|
+
node --env-file=.env node_modules/drizzle-kit/bin.cjs push
|
|
80
83
|
\`\`\`
|
|
81
|
-
`;
|
|
84
|
+
${preset.database === "postgres" ? "\nOn PostgreSQL drizzle-kit connects with `DATABASE_SUPERUSER_URL`, the superuser kept for migrations. The app role still reads and writes the new tables, and because it does not own them, their row-level security policies apply to it.\n" : ""}`;
|
|
82
85
|
}
|
|
83
86
|
return "";
|
|
84
87
|
}
|
|
@@ -86,23 +89,23 @@ npx drizzle-kit push
|
|
|
86
89
|
function getStrategyDescription(strategy: string): string {
|
|
87
90
|
switch (strategy) {
|
|
88
91
|
case "rls":
|
|
89
|
-
return `- **Row-Level Security
|
|
92
|
+
return `- **Row-Level Security**: PostgreSQL RLS policies filter rows by tenant automatically
|
|
90
93
|
- Each query sets \`app.current_tenant_id\` and RLS enforces isolation
|
|
91
94
|
- All tenants share one database and schema`;
|
|
92
95
|
case "schema":
|
|
93
|
-
return `- **Schema-per-tenant
|
|
96
|
+
return `- **Schema-per-tenant**: each tenant gets a dedicated PostgreSQL schema
|
|
94
97
|
- Queries are routed to the correct schema via search_path
|
|
95
98
|
- Shared database, isolated schemas`;
|
|
96
99
|
case "database":
|
|
97
|
-
return `- **Database-per-tenant
|
|
100
|
+
return `- **Database-per-tenant**: each tenant gets a fully isolated database
|
|
98
101
|
- Connection routing directs queries to the correct database
|
|
99
102
|
- Maximum isolation at the cost of more resource usage`;
|
|
100
103
|
case "collection":
|
|
101
|
-
return `- **Collection-per-tenant
|
|
104
|
+
return `- **Collection-per-tenant**: each tenant gets dedicated MongoDB collections
|
|
102
105
|
- Collection names are prefixed or namespaced by tenant ID
|
|
103
106
|
- Shared database, isolated collections`;
|
|
104
107
|
case "table-prefix":
|
|
105
|
-
return `- **Table-prefix
|
|
108
|
+
return `- **Table-prefix**: tenant-specific tables with a naming prefix
|
|
106
109
|
- Tables are prefixed with the tenant identifier
|
|
107
110
|
- Shared database, prefixed table names`;
|
|
108
111
|
default:
|
|
@@ -24,12 +24,15 @@ export function generateTsconfig(framework: Framework, extraSources: string[] =
|
|
|
24
24
|
{
|
|
25
25
|
compilerOptions: {
|
|
26
26
|
target: "ESNext",
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
// Next.js resolves imports like a bundler: "next/server" has no file
|
|
28
|
+
// extension, which NodeNext refuses in an ES module package.
|
|
29
|
+
...(framework === "nextjs"
|
|
30
|
+
? { module: "ESNext", moduleResolution: "Bundler" }
|
|
31
|
+
: { module: "NodeNext", moduleResolution: "NodeNext" }),
|
|
29
32
|
strict: true,
|
|
30
|
-
|
|
31
|
-
rootDir
|
|
32
|
-
declaration: true,
|
|
33
|
+
// next build compiles a Next.js app itself and type-checks the files it
|
|
34
|
+
// generates under .next/types, which a rootDir of src would reject.
|
|
35
|
+
...(framework === "nextjs" ? { noEmit: true } : { outDir: "dist", rootDir, declaration: true }),
|
|
33
36
|
skipLibCheck: true,
|
|
34
37
|
esModuleInterop: true,
|
|
35
38
|
...(framework === "nestjs"
|
|
@@ -39,7 +42,10 @@ export function generateTsconfig(framework: Framework, extraSources: string[] =
|
|
|
39
42
|
? { jsx: "preserve", plugins: [{ name: "next" }] }
|
|
40
43
|
: {}),
|
|
41
44
|
},
|
|
42
|
-
include:
|
|
45
|
+
include:
|
|
46
|
+
framework === "nextjs"
|
|
47
|
+
? ["next-env.d.ts", "src", ...extraSources, ".next/types/**/*.ts"]
|
|
48
|
+
: ["src", ...extraSources],
|
|
43
49
|
exclude: ["node_modules", "dist"],
|
|
44
50
|
},
|
|
45
51
|
null,
|