@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/dist/index.js
CHANGED
|
@@ -174,12 +174,22 @@ function postgresAppRole(dbName) {
|
|
|
174
174
|
return `${dbName}_app`;
|
|
175
175
|
}
|
|
176
176
|
var POSTGRES_APP_PASSWORD = "dev_app_password";
|
|
177
|
+
function postgresStratumRole(dbName) {
|
|
178
|
+
return `${dbName}_stratum`;
|
|
179
|
+
}
|
|
180
|
+
var POSTGRES_STRATUM_PASSWORD = "dev_stratum_password";
|
|
177
181
|
function postgresAppRoleSql(dbName, strategy) {
|
|
178
182
|
const role = postgresAppRole(dbName);
|
|
183
|
+
const stratum = postgresStratumRole(dbName);
|
|
179
184
|
let strategyGrant = "";
|
|
180
185
|
if (strategy === "schema") {
|
|
181
186
|
strategyGrant = `
|
|
182
|
-
-- schema-per-tenant: the app creates one schema per tenant
|
|
187
|
+
-- schema-per-tenant: the app creates one schema per tenant. A schema named
|
|
188
|
+
-- like a login comes first on that login's default search path ("$user",
|
|
189
|
+
-- public), so a schema the app creates could come before public for the
|
|
190
|
+
-- Stratum login or the bootstrap superuser. Both search only public.
|
|
191
|
+
ALTER ROLE ${stratum} IN DATABASE ${dbName} SET search_path = public;
|
|
192
|
+
ALTER ROLE CURRENT_USER IN DATABASE ${dbName} SET search_path = public;
|
|
183
193
|
GRANT CREATE ON DATABASE ${dbName} TO ${role};
|
|
184
194
|
`;
|
|
185
195
|
} else if (strategy === "database") {
|
|
@@ -189,15 +199,40 @@ ALTER ROLE ${role} CREATEDB;
|
|
|
189
199
|
`;
|
|
190
200
|
}
|
|
191
201
|
return `
|
|
202
|
+
-- Stratum's control role (migration 032 of @stratum-hq/lib). Every Stratum
|
|
203
|
+
-- table gets a policy for it, and only its members reach rows across
|
|
204
|
+
-- tenants.
|
|
205
|
+
CREATE ROLE stratum_control NOLOGIN NOSUPERUSER NOBYPASSRLS;
|
|
206
|
+
GRANT USAGE, CREATE ON SCHEMA public TO stratum_control;
|
|
207
|
+
|
|
208
|
+
-- Stratum's own login: pass it to the library as adminPool, which runs the
|
|
209
|
+
-- Stratum migrations, so it owns the Stratum tables. Not a superuser and no
|
|
210
|
+
-- BYPASSRLS: it reaches them as a member of stratum_control. Do not run the
|
|
211
|
+
-- Stratum migrations as the bootstrap superuser: the default privileges
|
|
212
|
+
-- below would give the app role write access to the Stratum tables. If that
|
|
213
|
+
-- happened, "stratum db roles --apply --app-role ${role}" removes it.
|
|
214
|
+
CREATE ROLE ${stratum} WITH LOGIN PASSWORD '${POSTGRES_STRATUM_PASSWORD}' NOSUPERUSER NOBYPASSRLS;
|
|
215
|
+
GRANT stratum_control TO ${stratum} WITH INHERIT TRUE, SET TRUE;
|
|
216
|
+
GRANT CONNECT ON DATABASE ${dbName} TO ${stratum};
|
|
217
|
+
GRANT USAGE, CREATE ON SCHEMA public TO ${stratum};
|
|
218
|
+
|
|
192
219
|
-- Application role. The app connects as ${role} (DATABASE_URL), never as the
|
|
193
|
-
-- bootstrap superuser (
|
|
194
|
-
-- ignores every row-level security policy, FORCE included.
|
|
195
|
-
--
|
|
220
|
+
-- bootstrap superuser (DATABASE_SUPERUSER_URL): a superuser or BYPASSRLS role
|
|
221
|
+
-- ignores every row-level security policy, FORCE included. It creates no
|
|
222
|
+
-- objects in public: the superuser creates the application's tables (its
|
|
223
|
+
-- migrations), and the app role reads and writes the ones the superuser
|
|
224
|
+
-- creates. It gets nothing on the tables ${stratum} creates; to let it read
|
|
225
|
+
-- Stratum's read list once Stratum has migrated, run:
|
|
226
|
+
-- stratum db roles --apply --admin-role ${stratum} --app-role ${role}
|
|
196
227
|
CREATE ROLE ${role} WITH LOGIN PASSWORD '${POSTGRES_APP_PASSWORD}' NOSUPERUSER NOBYPASSRLS NOCREATEROLE;
|
|
197
228
|
GRANT CONNECT ON DATABASE ${dbName} TO ${role};
|
|
198
|
-
GRANT USAGE
|
|
199
|
-
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${role};
|
|
200
|
-
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO ${role};
|
|
229
|
+
GRANT USAGE ON SCHEMA public TO ${role};
|
|
230
|
+
ALTER DEFAULT PRIVILEGES FOR ROLE ${dbName} IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${role};
|
|
231
|
+
ALTER DEFAULT PRIVILEGES FOR ROLE ${dbName} IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO ${role};
|
|
232
|
+
|
|
233
|
+
-- Only roles granted CREATE by name create objects in public. PostgreSQL 15
|
|
234
|
+
-- and later already start this way; older versions grant it to PUBLIC.
|
|
235
|
+
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
|
|
201
236
|
${strategyGrant}`;
|
|
202
237
|
}
|
|
203
238
|
function generatePresetInitSql(projectName, preset) {
|
|
@@ -376,6 +411,10 @@ export { pool };
|
|
|
376
411
|
`
|
|
377
412
|
});
|
|
378
413
|
}
|
|
414
|
+
files.push({
|
|
415
|
+
filename: "src/schema.ts",
|
|
416
|
+
content: drizzleSchema(preset)
|
|
417
|
+
});
|
|
379
418
|
files.push({
|
|
380
419
|
filename: "drizzle.config.ts",
|
|
381
420
|
content: `import type { Config } from "drizzle-kit";
|
|
@@ -385,13 +424,44 @@ export default {
|
|
|
385
424
|
out: "./drizzle",
|
|
386
425
|
${preset.database === "mysql" ? 'dialect: "mysql",' : 'dialect: "postgresql",'}
|
|
387
426
|
dbCredentials: {
|
|
388
|
-
url: process.env.DATABASE_URL
|
|
427
|
+
url: ${preset.database === "mysql" ? "process.env.DATABASE_URL!" : "(process.env.DATABASE_SUPERUSER_URL ?? process.env.DATABASE_URL)!"},
|
|
389
428
|
},
|
|
390
429
|
} satisfies Config;
|
|
391
430
|
`
|
|
392
431
|
});
|
|
393
432
|
return files;
|
|
394
433
|
}
|
|
434
|
+
function drizzleSchema(preset) {
|
|
435
|
+
if (preset.database === "mysql") {
|
|
436
|
+
return `// Drizzle table definitions. drizzle.config.ts reads this file.
|
|
437
|
+
import { mysqlTable, varchar, text, timestamp } from "drizzle-orm/mysql-core";
|
|
438
|
+
|
|
439
|
+
// An example tenant-scoped table: every row carries the tenant it belongs to.
|
|
440
|
+
export const notes = mysqlTable("notes", {
|
|
441
|
+
id: varchar("id", { length: 36 }).primaryKey(),
|
|
442
|
+
tenantId: varchar("tenant_id", { length: 36 }).notNull(),
|
|
443
|
+
body: text("body").notNull(),
|
|
444
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
445
|
+
});
|
|
446
|
+
`;
|
|
447
|
+
}
|
|
448
|
+
return `// Drizzle table definitions. drizzle.config.ts reads this file.
|
|
449
|
+
import { pgTable, uuid, text, timestamp, index } from "drizzle-orm/pg-core";
|
|
450
|
+
|
|
451
|
+
// An example tenant-scoped table: every row carries the tenant it belongs to.
|
|
452
|
+
// Stratum's own tables, such as tenants, are created by Stratum, not here.
|
|
453
|
+
export const notes = pgTable(
|
|
454
|
+
"notes",
|
|
455
|
+
{
|
|
456
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
457
|
+
tenantId: uuid("tenant_id").notNull(),
|
|
458
|
+
body: text("body").notNull(),
|
|
459
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
460
|
+
},
|
|
461
|
+
(table) => [index("notes_tenant_id_idx").on(table.tenantId)],
|
|
462
|
+
);
|
|
463
|
+
`;
|
|
464
|
+
}
|
|
395
465
|
function generateSequelizeSetup(preset) {
|
|
396
466
|
const dialect = preset.database === "mysql" ? "mysql" : "postgres";
|
|
397
467
|
return [
|
|
@@ -642,8 +712,10 @@ async function verifiedTenant(
|
|
|
642
712
|
var INVALID_TOKEN = `{ error: "Bearer token is invalid or has no tenant_id claim" }`;
|
|
643
713
|
var TENANT_REQUIRED = `{ error: "A bearer token with a tenant_id claim is required" }`;
|
|
644
714
|
function nextjsTenantMiddleware() {
|
|
645
|
-
return `// middleware.ts
|
|
646
|
-
//
|
|
715
|
+
return `// src/middleware.ts: Next.js middleware for Stratum tenant resolution
|
|
716
|
+
//
|
|
717
|
+
// Next.js runs middleware only from the directory that holds the app
|
|
718
|
+
// directory: src/middleware.ts for src/app, middleware.ts for app.
|
|
647
719
|
//
|
|
648
720
|
// The tenant ID comes only from the tenant_id claim of a bearer token that
|
|
649
721
|
// verifies with JWT_SECRET, and is forwarded as x-tenant-id. Any copy of the
|
|
@@ -718,6 +790,19 @@ export const config = {
|
|
|
718
790
|
};
|
|
719
791
|
`;
|
|
720
792
|
}
|
|
793
|
+
function nextjsRootLayout(projectName) {
|
|
794
|
+
return `// app/layout.tsx: ${projectName} root layout
|
|
795
|
+
import type { ReactNode } from "react";
|
|
796
|
+
|
|
797
|
+
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
798
|
+
return (
|
|
799
|
+
<html lang="en">
|
|
800
|
+
<body>{children}</body>
|
|
801
|
+
</html>
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
`;
|
|
805
|
+
}
|
|
721
806
|
function generateMiddleware(projectName, preset) {
|
|
722
807
|
switch (preset.framework) {
|
|
723
808
|
case "express":
|
|
@@ -735,10 +820,10 @@ function generateMiddleware(projectName, preset) {
|
|
|
735
820
|
}
|
|
736
821
|
}
|
|
737
822
|
function generateExpressMiddleware(projectName) {
|
|
738
|
-
return [
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
823
|
+
return [{ filename: "src/index.ts", content: expressServer(projectName) }];
|
|
824
|
+
}
|
|
825
|
+
function expressServer(projectName) {
|
|
826
|
+
return `import express from "express";
|
|
742
827
|
${VERIFIED_TENANT}
|
|
743
828
|
|
|
744
829
|
const app = express();
|
|
@@ -774,15 +859,13 @@ app.get("/tenants", async (req, res) => {
|
|
|
774
859
|
app.listen(port, () => {
|
|
775
860
|
console.log(\`${projectName} running on http://localhost:\${port}\`);
|
|
776
861
|
});
|
|
777
|
-
|
|
778
|
-
}
|
|
779
|
-
];
|
|
862
|
+
`;
|
|
780
863
|
}
|
|
781
864
|
function generateFastifyMiddleware(projectName) {
|
|
782
|
-
return [
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
865
|
+
return [{ filename: "src/index.ts", content: fastifyServer(projectName) }];
|
|
866
|
+
}
|
|
867
|
+
function fastifyServer(projectName) {
|
|
868
|
+
return `import Fastify from "fastify";
|
|
786
869
|
${VERIFIED_TENANT}
|
|
787
870
|
|
|
788
871
|
const fastify = Fastify({ logger: true });
|
|
@@ -817,16 +900,18 @@ fastify.listen({ port, host: "0.0.0.0" }, (err) => {
|
|
|
817
900
|
process.exit(1);
|
|
818
901
|
}
|
|
819
902
|
});
|
|
820
|
-
|
|
821
|
-
}
|
|
822
|
-
];
|
|
903
|
+
`;
|
|
823
904
|
}
|
|
824
905
|
function generateNextjsMiddleware(projectName) {
|
|
825
906
|
return [
|
|
826
907
|
{
|
|
827
|
-
filename: "middleware.ts",
|
|
908
|
+
filename: "src/middleware.ts",
|
|
828
909
|
content: nextjsTenantMiddleware()
|
|
829
910
|
},
|
|
911
|
+
{
|
|
912
|
+
filename: "src/app/layout.tsx",
|
|
913
|
+
content: nextjsRootLayout(projectName)
|
|
914
|
+
},
|
|
830
915
|
{
|
|
831
916
|
filename: "src/app/page.tsx",
|
|
832
917
|
content: `// app/page.tsx - ${projectName} root page
|
|
@@ -837,7 +922,7 @@ export default function Home() {
|
|
|
837
922
|
<p>Multi-tenant app powered by Stratum.</p>
|
|
838
923
|
<ul>
|
|
839
924
|
<li>Configure tenants via the Stratum control plane</li>
|
|
840
|
-
<li>The tenant comes from a verified JWT in <code>middleware.ts</code></li>
|
|
925
|
+
<li>The tenant comes from a verified JWT in <code>src/middleware.ts</code></li>
|
|
841
926
|
<li>Use <code>@stratum-hq/lib</code> for tenant resolution</li>
|
|
842
927
|
</ul>
|
|
843
928
|
</main>
|
|
@@ -990,18 +1075,19 @@ function generateTsconfig(framework, extraSources = []) {
|
|
|
990
1075
|
{
|
|
991
1076
|
compilerOptions: {
|
|
992
1077
|
target: "ESNext",
|
|
993
|
-
|
|
994
|
-
|
|
1078
|
+
// Next.js resolves imports like a bundler: "next/server" has no file
|
|
1079
|
+
// extension, which NodeNext refuses in an ES module package.
|
|
1080
|
+
...framework === "nextjs" ? { module: "ESNext", moduleResolution: "Bundler" } : { module: "NodeNext", moduleResolution: "NodeNext" },
|
|
995
1081
|
strict: true,
|
|
996
|
-
|
|
997
|
-
rootDir
|
|
998
|
-
declaration: true,
|
|
1082
|
+
// next build compiles a Next.js app itself and type-checks the files it
|
|
1083
|
+
// generates under .next/types, which a rootDir of src would reject.
|
|
1084
|
+
...framework === "nextjs" ? { noEmit: true } : { outDir: "dist", rootDir, declaration: true },
|
|
999
1085
|
skipLibCheck: true,
|
|
1000
1086
|
esModuleInterop: true,
|
|
1001
1087
|
...framework === "nestjs" ? { experimentalDecorators: true, emitDecoratorMetadata: true } : {},
|
|
1002
1088
|
...framework === "nextjs" ? { jsx: "preserve", plugins: [{ name: "next" }] } : {}
|
|
1003
1089
|
},
|
|
1004
|
-
include: ["src", ...extraSources],
|
|
1090
|
+
include: framework === "nextjs" ? ["next-env.d.ts", "src", ...extraSources, ".next/types/**/*.ts"] : ["src", ...extraSources],
|
|
1005
1091
|
exclude: ["node_modules", "dist"]
|
|
1006
1092
|
},
|
|
1007
1093
|
null,
|
|
@@ -1012,14 +1098,17 @@ function generateTsconfig(framework, extraSources = []) {
|
|
|
1012
1098
|
// ../hono/package.json
|
|
1013
1099
|
var package_default = {
|
|
1014
1100
|
name: "@stratum-hq/hono",
|
|
1015
|
-
version: "1.2
|
|
1016
|
-
description: "
|
|
1101
|
+
version: "1.3.2",
|
|
1102
|
+
description: "Hono multi-tenancy middleware: tenant resolution with AsyncLocalStorage context",
|
|
1017
1103
|
keywords: [
|
|
1018
1104
|
"multi-tenancy",
|
|
1105
|
+
"multitenancy",
|
|
1019
1106
|
"multi-tenant",
|
|
1020
1107
|
"saas",
|
|
1021
1108
|
"tenant",
|
|
1109
|
+
"tenant-isolation",
|
|
1022
1110
|
"hono",
|
|
1111
|
+
"asynclocalstorage",
|
|
1023
1112
|
"middleware",
|
|
1024
1113
|
"typescript",
|
|
1025
1114
|
"stratum"
|
|
@@ -1063,7 +1152,7 @@ var package_default = {
|
|
|
1063
1152
|
},
|
|
1064
1153
|
license: "MIT",
|
|
1065
1154
|
author: "Christian Crank",
|
|
1066
|
-
homepage: "https://
|
|
1155
|
+
homepage: "https://docs.stratum-hq.org/packages/hono/",
|
|
1067
1156
|
bugs: "https://github.com/stratum-hq/Stratum/issues",
|
|
1068
1157
|
engines: {
|
|
1069
1158
|
node: ">=20.0.0"
|
|
@@ -1078,18 +1167,27 @@ var package_default = {
|
|
|
1078
1167
|
// ../db-adapters/package.json
|
|
1079
1168
|
var package_default2 = {
|
|
1080
1169
|
name: "@stratum-hq/db-adapters",
|
|
1081
|
-
version: "1.
|
|
1082
|
-
description: "
|
|
1170
|
+
version: "1.5.0",
|
|
1171
|
+
description: "Multi-tenant PostgreSQL row-level security for pg, Prisma, Drizzle, and Sequelize, plus schema-per-tenant and database-per-tenant isolation",
|
|
1083
1172
|
keywords: [
|
|
1084
1173
|
"multi-tenancy",
|
|
1174
|
+
"multitenancy",
|
|
1085
1175
|
"multi-tenant",
|
|
1086
1176
|
"saas",
|
|
1087
1177
|
"tenant",
|
|
1088
1178
|
"tenant-isolation",
|
|
1089
|
-
"postgresql",
|
|
1090
|
-
"postgres",
|
|
1091
1179
|
"row-level-security",
|
|
1092
1180
|
"rls",
|
|
1181
|
+
"postgresql",
|
|
1182
|
+
"prisma",
|
|
1183
|
+
"drizzle",
|
|
1184
|
+
"drizzle-orm",
|
|
1185
|
+
"sequelize",
|
|
1186
|
+
"pg",
|
|
1187
|
+
"node-postgres",
|
|
1188
|
+
"schema-per-tenant",
|
|
1189
|
+
"database-per-tenant",
|
|
1190
|
+
"postgres",
|
|
1093
1191
|
"schema-isolation",
|
|
1094
1192
|
"database",
|
|
1095
1193
|
"stratum"
|
|
@@ -1100,6 +1198,10 @@ var package_default2 = {
|
|
|
1100
1198
|
".": {
|
|
1101
1199
|
types: "./dist/index.d.ts",
|
|
1102
1200
|
default: "./dist/index.js"
|
|
1201
|
+
},
|
|
1202
|
+
"./pglite": {
|
|
1203
|
+
types: "./dist/pglite/index.d.ts",
|
|
1204
|
+
default: "./dist/pglite/index.js"
|
|
1103
1205
|
}
|
|
1104
1206
|
},
|
|
1105
1207
|
files: [
|
|
@@ -1119,18 +1221,23 @@ var package_default2 = {
|
|
|
1119
1221
|
registry: "https://registry.npmjs.org/"
|
|
1120
1222
|
},
|
|
1121
1223
|
dependencies: {
|
|
1122
|
-
"@stratum-hq/core": "^1.
|
|
1224
|
+
"@stratum-hq/core": "^1.6.0",
|
|
1123
1225
|
pg: "^8.11.0"
|
|
1124
1226
|
},
|
|
1125
1227
|
peerDependencies: {
|
|
1228
|
+
"@electric-sql/pglite": "^0.4.2",
|
|
1126
1229
|
"drizzle-orm": ">=0.29.0"
|
|
1127
1230
|
},
|
|
1128
1231
|
peerDependenciesMeta: {
|
|
1232
|
+
"@electric-sql/pglite": {
|
|
1233
|
+
optional: true
|
|
1234
|
+
},
|
|
1129
1235
|
"drizzle-orm": {
|
|
1130
1236
|
optional: true
|
|
1131
1237
|
}
|
|
1132
1238
|
},
|
|
1133
1239
|
devDependencies: {
|
|
1240
|
+
"@electric-sql/pglite": "^0.4.6",
|
|
1134
1241
|
"@prisma/client": "^5.10.0",
|
|
1135
1242
|
"@types/pg": "^8.11.0",
|
|
1136
1243
|
"drizzle-orm": "^0.45.3",
|
|
@@ -1139,7 +1246,7 @@ var package_default2 = {
|
|
|
1139
1246
|
},
|
|
1140
1247
|
license: "MIT",
|
|
1141
1248
|
author: "Christian Crank",
|
|
1142
|
-
homepage: "https://
|
|
1249
|
+
homepage: "https://docs.stratum-hq.org/packages/db-adapters/",
|
|
1143
1250
|
bugs: "https://github.com/stratum-hq/Stratum/issues",
|
|
1144
1251
|
engines: {
|
|
1145
1252
|
node: ">=20.0.0"
|
|
@@ -1154,21 +1261,28 @@ var package_default2 = {
|
|
|
1154
1261
|
// ../lib/package.json
|
|
1155
1262
|
var package_default3 = {
|
|
1156
1263
|
name: "@stratum-hq/lib",
|
|
1157
|
-
version: "1.
|
|
1158
|
-
description: "
|
|
1264
|
+
version: "1.8.0",
|
|
1265
|
+
description: "Multi-tenancy for Node.js and PostgreSQL: tenant hierarchy, config inheritance, row-level security, ABAC, audit log, and GDPR tooling",
|
|
1159
1266
|
keywords: [
|
|
1160
1267
|
"multi-tenancy",
|
|
1268
|
+
"multitenancy",
|
|
1161
1269
|
"multi-tenant",
|
|
1162
1270
|
"saas",
|
|
1163
|
-
"b2b",
|
|
1164
1271
|
"tenant",
|
|
1165
|
-
"tenant-management",
|
|
1166
1272
|
"tenant-isolation",
|
|
1167
|
-
"node",
|
|
1168
|
-
"typescript",
|
|
1169
|
-
"postgresql",
|
|
1170
1273
|
"row-level-security",
|
|
1171
1274
|
"rls",
|
|
1275
|
+
"postgresql",
|
|
1276
|
+
"tenant-hierarchy",
|
|
1277
|
+
"ltree",
|
|
1278
|
+
"abac",
|
|
1279
|
+
"audit-log",
|
|
1280
|
+
"encryption",
|
|
1281
|
+
"webhooks",
|
|
1282
|
+
"b2b",
|
|
1283
|
+
"tenant-management",
|
|
1284
|
+
"node",
|
|
1285
|
+
"typescript",
|
|
1172
1286
|
"schema-isolation",
|
|
1173
1287
|
"config-inheritance",
|
|
1174
1288
|
"gdpr",
|
|
@@ -1190,7 +1304,7 @@ var package_default3 = {
|
|
|
1190
1304
|
scripts: {
|
|
1191
1305
|
build: "tsc && node ../../scripts/copy-dir.mjs src/migrations dist/migrations",
|
|
1192
1306
|
test: "vitest run",
|
|
1193
|
-
typecheck: "tsc --noEmit",
|
|
1307
|
+
typecheck: "tsc --noEmit && tsc -p tsconfig.types.json",
|
|
1194
1308
|
lint: "eslint .",
|
|
1195
1309
|
clean: "rm -rf dist"
|
|
1196
1310
|
},
|
|
@@ -1199,10 +1313,10 @@ var package_default3 = {
|
|
|
1199
1313
|
registry: "https://registry.npmjs.org/"
|
|
1200
1314
|
},
|
|
1201
1315
|
dependencies: {
|
|
1202
|
-
"@opentelemetry/api": "^1.
|
|
1203
|
-
"@stratum-hq/core": "^1.
|
|
1204
|
-
"@stratum-hq/db-adapters": "^1.
|
|
1205
|
-
"@stratum-hq/sdk": "^1.
|
|
1316
|
+
"@opentelemetry/api": "^1.0.0",
|
|
1317
|
+
"@stratum-hq/core": "^1.6.0",
|
|
1318
|
+
"@stratum-hq/db-adapters": "^1.5.0",
|
|
1319
|
+
"@stratum-hq/sdk": "^1.4.0",
|
|
1206
1320
|
pg: "^8.11.0"
|
|
1207
1321
|
},
|
|
1208
1322
|
peerDependencies: {
|
|
@@ -1214,13 +1328,14 @@ var package_default3 = {
|
|
|
1214
1328
|
}
|
|
1215
1329
|
},
|
|
1216
1330
|
devDependencies: {
|
|
1331
|
+
"@electric-sql/pglite": "^0.4.6",
|
|
1217
1332
|
"@types/pg": "^8.11.0",
|
|
1218
1333
|
typescript: "^5.4.0",
|
|
1219
1334
|
vitest: "^4.1.11"
|
|
1220
1335
|
},
|
|
1221
1336
|
license: "MIT",
|
|
1222
1337
|
author: "Christian Crank",
|
|
1223
|
-
homepage: "https://
|
|
1338
|
+
homepage: "https://docs.stratum-hq.org/packages/lib/",
|
|
1224
1339
|
bugs: "https://github.com/stratum-hq/Stratum/issues",
|
|
1225
1340
|
engines: {
|
|
1226
1341
|
node: ">=20.0.0"
|
|
@@ -1235,10 +1350,11 @@ var package_default3 = {
|
|
|
1235
1350
|
// ../mongodb/package.json
|
|
1236
1351
|
var package_default4 = {
|
|
1237
1352
|
name: "@stratum-hq/mongodb",
|
|
1238
|
-
version: "0.
|
|
1239
|
-
description: "MongoDB tenant isolation for Stratum
|
|
1353
|
+
version: "0.7.0",
|
|
1354
|
+
description: "MongoDB tenant isolation for Stratum: shared collection, collection-per-tenant, and database-per-tenant strategies",
|
|
1240
1355
|
keywords: [
|
|
1241
1356
|
"multi-tenancy",
|
|
1357
|
+
"multitenancy",
|
|
1242
1358
|
"multi-tenant",
|
|
1243
1359
|
"saas",
|
|
1244
1360
|
"tenant",
|
|
@@ -1265,17 +1381,18 @@ var package_default4 = {
|
|
|
1265
1381
|
build: "tsc",
|
|
1266
1382
|
test: "vitest run",
|
|
1267
1383
|
"test:watch": "vitest",
|
|
1268
|
-
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
1269
|
-
typecheck: "tsc --noEmit",
|
|
1384
|
+
"test:integration": "vitest run --config vitest.integration.config.ts && vitest run --config vitest.integration.mongoose9.config.ts",
|
|
1385
|
+
typecheck: "tsc --noEmit && tsc -p tsconfig.types.json",
|
|
1270
1386
|
lint: "eslint .",
|
|
1271
1387
|
clean: "rm -rf dist"
|
|
1272
1388
|
},
|
|
1273
1389
|
dependencies: {
|
|
1274
|
-
"@stratum-hq/core": "^1.
|
|
1275
|
-
"@stratum-hq/sdk": "^1.
|
|
1390
|
+
"@stratum-hq/core": "^1.6.0",
|
|
1391
|
+
"@stratum-hq/sdk": "^1.4.0"
|
|
1276
1392
|
},
|
|
1277
1393
|
peerDependencies: {
|
|
1278
|
-
mongodb: "^6.0.0 || ^7.0.0"
|
|
1394
|
+
mongodb: "^6.0.0 || ^7.0.0",
|
|
1395
|
+
mongoose: "^8.0.0 || ^9.0.0"
|
|
1279
1396
|
},
|
|
1280
1397
|
peerDependenciesMeta: {
|
|
1281
1398
|
mongoose: {
|
|
@@ -1283,9 +1400,14 @@ var package_default4 = {
|
|
|
1283
1400
|
}
|
|
1284
1401
|
},
|
|
1285
1402
|
devDependencies: {
|
|
1403
|
+
"@stratum-hq/lib": "*",
|
|
1286
1404
|
"@stratum-hq/test-utils": "*",
|
|
1405
|
+
"@types/express": "^4.17.0",
|
|
1406
|
+
"@types/jsonwebtoken": "^9.0.0",
|
|
1407
|
+
"@types/pg": "^8.11.0",
|
|
1287
1408
|
mongodb: "^7.6.0",
|
|
1288
1409
|
mongoose: "^8.0.0",
|
|
1410
|
+
mongoose9: "npm:mongoose@^9.10.3",
|
|
1289
1411
|
typescript: "^5.9.0",
|
|
1290
1412
|
vitest: "^4.1.11"
|
|
1291
1413
|
},
|
|
@@ -1294,7 +1416,7 @@ var package_default4 = {
|
|
|
1294
1416
|
},
|
|
1295
1417
|
license: "MIT",
|
|
1296
1418
|
author: "Christian Crank",
|
|
1297
|
-
homepage: "https://
|
|
1419
|
+
homepage: "https://docs.stratum-hq.org/packages/mongodb/",
|
|
1298
1420
|
bugs: "https://github.com/stratum-hq/Stratum/issues",
|
|
1299
1421
|
engines: {
|
|
1300
1422
|
node: ">=20.0.0"
|
|
@@ -1309,10 +1431,11 @@ var package_default4 = {
|
|
|
1309
1431
|
// ../mysql/package.json
|
|
1310
1432
|
var package_default5 = {
|
|
1311
1433
|
name: "@stratum-hq/mysql",
|
|
1312
|
-
version: "0.
|
|
1313
|
-
description: "MySQL tenant isolation for Stratum
|
|
1434
|
+
version: "0.7.0",
|
|
1435
|
+
description: "MySQL tenant isolation for Stratum: shared table, table-per-tenant, and database-per-tenant strategies",
|
|
1314
1436
|
keywords: [
|
|
1315
1437
|
"multi-tenancy",
|
|
1438
|
+
"multitenancy",
|
|
1316
1439
|
"multi-tenant",
|
|
1317
1440
|
"saas",
|
|
1318
1441
|
"tenant",
|
|
@@ -1342,13 +1465,13 @@ var package_default5 = {
|
|
|
1342
1465
|
test: "vitest run",
|
|
1343
1466
|
"test:watch": "vitest",
|
|
1344
1467
|
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
1345
|
-
typecheck: "tsc --noEmit",
|
|
1468
|
+
typecheck: "tsc --noEmit && tsc -p tsconfig.types.json",
|
|
1346
1469
|
lint: "eslint .",
|
|
1347
1470
|
clean: "rm -rf dist"
|
|
1348
1471
|
},
|
|
1349
1472
|
dependencies: {
|
|
1350
|
-
"@stratum-hq/core": "^1.
|
|
1351
|
-
"@stratum-hq/sdk": "^1.
|
|
1473
|
+
"@stratum-hq/core": "^1.6.0",
|
|
1474
|
+
"@stratum-hq/sdk": "^1.4.0"
|
|
1352
1475
|
},
|
|
1353
1476
|
peerDependencies: {
|
|
1354
1477
|
mysql2: "^3.0.0",
|
|
@@ -1368,8 +1491,11 @@ var package_default5 = {
|
|
|
1368
1491
|
}
|
|
1369
1492
|
},
|
|
1370
1493
|
devDependencies: {
|
|
1494
|
+
"@stratum-hq/lib": "*",
|
|
1495
|
+
"@types/pg": "^8.11.0",
|
|
1371
1496
|
knex: "^3.3.0",
|
|
1372
1497
|
mysql2: "^3.24.4",
|
|
1498
|
+
sequelize: "^6.37.8",
|
|
1373
1499
|
typeorm: "^1.1.1",
|
|
1374
1500
|
typescript: "^5.9.0",
|
|
1375
1501
|
vitest: "^4.1.11"
|
|
@@ -1379,7 +1505,7 @@ var package_default5 = {
|
|
|
1379
1505
|
},
|
|
1380
1506
|
license: "MIT",
|
|
1381
1507
|
author: "Christian Crank",
|
|
1382
|
-
homepage: "https://
|
|
1508
|
+
homepage: "https://docs.stratum-hq.org/packages/mysql/",
|
|
1383
1509
|
bugs: "https://github.com/stratum-hq/Stratum/issues",
|
|
1384
1510
|
engines: {
|
|
1385
1511
|
node: ">=20.0.0"
|
|
@@ -1394,13 +1520,15 @@ var package_default5 = {
|
|
|
1394
1520
|
// ../nestjs/package.json
|
|
1395
1521
|
var package_default6 = {
|
|
1396
1522
|
name: "@stratum-hq/nestjs",
|
|
1397
|
-
version: "1.3.
|
|
1398
|
-
description: "
|
|
1523
|
+
version: "1.3.2",
|
|
1524
|
+
description: "NestJS multi-tenancy: tenant guard, @Tenant() decorator, and module for Stratum",
|
|
1399
1525
|
keywords: [
|
|
1400
1526
|
"multi-tenancy",
|
|
1527
|
+
"multitenancy",
|
|
1401
1528
|
"multi-tenant",
|
|
1402
1529
|
"saas",
|
|
1403
1530
|
"tenant",
|
|
1531
|
+
"tenant-isolation",
|
|
1404
1532
|
"nestjs",
|
|
1405
1533
|
"nest",
|
|
1406
1534
|
"guard",
|
|
@@ -1444,6 +1572,7 @@ var package_default6 = {
|
|
|
1444
1572
|
devDependencies: {
|
|
1445
1573
|
"@nestjs/common": "^12.1.2",
|
|
1446
1574
|
"@nestjs/core": "^12.1.2",
|
|
1575
|
+
"@nestjs/platform-express": "^12.1.2",
|
|
1447
1576
|
"@nestjs/testing": "^12.1.2",
|
|
1448
1577
|
"@stratum-hq/core": "*",
|
|
1449
1578
|
"@stratum-hq/sdk": "*",
|
|
@@ -1453,7 +1582,7 @@ var package_default6 = {
|
|
|
1453
1582
|
},
|
|
1454
1583
|
license: "MIT",
|
|
1455
1584
|
author: "Christian Crank",
|
|
1456
|
-
homepage: "https://
|
|
1585
|
+
homepage: "https://docs.stratum-hq.org/packages/nestjs/",
|
|
1457
1586
|
bugs: "https://github.com/stratum-hq/Stratum/issues",
|
|
1458
1587
|
engines: {
|
|
1459
1588
|
node: ">=20.0.0"
|
|
@@ -1476,6 +1605,9 @@ var STRATUM_RANGES = {
|
|
|
1476
1605
|
};
|
|
1477
1606
|
|
|
1478
1607
|
// src/generators/package-json.ts
|
|
1608
|
+
var DRIZZLE_KIT_OVERRIDES = {
|
|
1609
|
+
"@esbuild-kit/core-utils": { esbuild: "^0.25.4" }
|
|
1610
|
+
};
|
|
1479
1611
|
function generatePresetPackageJson(projectName, preset) {
|
|
1480
1612
|
const deps = {
|
|
1481
1613
|
"@stratum-hq/lib": STRATUM_RANGES["@stratum-hq/lib"]
|
|
@@ -1487,7 +1619,7 @@ function generatePresetPackageJson(projectName, preset) {
|
|
|
1487
1619
|
if (preset.framework === "nestjs") {
|
|
1488
1620
|
devDeps["tsc-watch"] = "^7.2.0";
|
|
1489
1621
|
} else if (preset.framework !== "nextjs") {
|
|
1490
|
-
devDeps["tsx"] = "^4.
|
|
1622
|
+
devDeps["tsx"] = "^4.19.3";
|
|
1491
1623
|
}
|
|
1492
1624
|
addDatabaseDeps(deps, devDeps, preset);
|
|
1493
1625
|
addOrmDeps(deps, devDeps, preset);
|
|
@@ -1503,6 +1635,7 @@ function generatePresetPackageJson(projectName, preset) {
|
|
|
1503
1635
|
scripts,
|
|
1504
1636
|
dependencies: sortKeys(deps),
|
|
1505
1637
|
devDependencies: sortKeys(devDeps),
|
|
1638
|
+
...preset.orm === "drizzle" ? { overrides: DRIZZLE_KIT_OVERRIDES } : {},
|
|
1506
1639
|
engines: {
|
|
1507
1640
|
node: ">=20.0.0"
|
|
1508
1641
|
}
|
|
@@ -1534,7 +1667,7 @@ function addDatabaseDeps(deps, devDeps, preset) {
|
|
|
1534
1667
|
break;
|
|
1535
1668
|
case "mysql":
|
|
1536
1669
|
if (preset.orm === "drizzle" || preset.orm === "pg") {
|
|
1537
|
-
deps["mysql2"] = "^3.
|
|
1670
|
+
deps["mysql2"] = "^3.23.1";
|
|
1538
1671
|
}
|
|
1539
1672
|
break;
|
|
1540
1673
|
}
|
|
@@ -1549,8 +1682,8 @@ function addOrmDeps(deps, devDeps, preset) {
|
|
|
1549
1682
|
}
|
|
1550
1683
|
break;
|
|
1551
1684
|
case "drizzle":
|
|
1552
|
-
deps["drizzle-orm"] = "^0.
|
|
1553
|
-
devDeps["drizzle-kit"] = "^0.
|
|
1685
|
+
deps["drizzle-orm"] = "^0.45.3";
|
|
1686
|
+
devDeps["drizzle-kit"] = "^0.31.11";
|
|
1554
1687
|
if (preset.database === "postgres") {
|
|
1555
1688
|
deps["pg"] = "^8.11.0";
|
|
1556
1689
|
}
|
|
@@ -1558,7 +1691,7 @@ function addOrmDeps(deps, devDeps, preset) {
|
|
|
1558
1691
|
case "sequelize":
|
|
1559
1692
|
deps["sequelize"] = "^6.37.0";
|
|
1560
1693
|
if (preset.database === "mysql") {
|
|
1561
|
-
deps["mysql2"] = "^3.
|
|
1694
|
+
deps["mysql2"] = "^3.23.1";
|
|
1562
1695
|
} else {
|
|
1563
1696
|
deps["pg"] = "^8.11.0";
|
|
1564
1697
|
}
|
|
@@ -1567,7 +1700,7 @@ function addOrmDeps(deps, devDeps, preset) {
|
|
|
1567
1700
|
deps["knex"] = "^3.1.0";
|
|
1568
1701
|
break;
|
|
1569
1702
|
case "mongoose":
|
|
1570
|
-
deps["mongoose"] = "^8.
|
|
1703
|
+
deps["mongoose"] = "^8.24.1";
|
|
1571
1704
|
break;
|
|
1572
1705
|
case "pg":
|
|
1573
1706
|
break;
|
|
@@ -1579,11 +1712,11 @@ function addFrameworkDeps(deps, devDeps, preset) {
|
|
|
1579
1712
|
}
|
|
1580
1713
|
switch (preset.framework) {
|
|
1581
1714
|
case "express":
|
|
1582
|
-
deps["express"] = "^4.
|
|
1715
|
+
deps["express"] = "^4.22.3";
|
|
1583
1716
|
devDeps["@types/express"] = "^4.17.21";
|
|
1584
1717
|
break;
|
|
1585
1718
|
case "fastify":
|
|
1586
|
-
deps["fastify"] = "^
|
|
1719
|
+
deps["fastify"] = "^5.12.5";
|
|
1587
1720
|
break;
|
|
1588
1721
|
case "nextjs":
|
|
1589
1722
|
deps["next"] = "^15.5.16";
|
|
@@ -1593,13 +1726,13 @@ function addFrameworkDeps(deps, devDeps, preset) {
|
|
|
1593
1726
|
devDeps["@types/react-dom"] = "^19.0.0";
|
|
1594
1727
|
break;
|
|
1595
1728
|
case "hono":
|
|
1596
|
-
deps["hono"] = "^4.
|
|
1597
|
-
deps["@hono/node-server"] = "^1.
|
|
1729
|
+
deps["hono"] = "^4.13.7";
|
|
1730
|
+
deps["@hono/node-server"] = "^1.19.15";
|
|
1598
1731
|
break;
|
|
1599
1732
|
case "nestjs":
|
|
1600
|
-
deps["@nestjs/core"] = "^11.
|
|
1601
|
-
deps["@nestjs/common"] = "^11.
|
|
1602
|
-
deps["@nestjs/platform-express"] = "^11.
|
|
1733
|
+
deps["@nestjs/core"] = "^11.1.18";
|
|
1734
|
+
deps["@nestjs/common"] = "^11.1.18";
|
|
1735
|
+
deps["@nestjs/platform-express"] = "^11.1.18";
|
|
1603
1736
|
deps["reflect-metadata"] = "^0.2.0";
|
|
1604
1737
|
deps["rxjs"] = "^7.8.0";
|
|
1605
1738
|
break;
|
|
@@ -1712,35 +1845,38 @@ npx prisma db push
|
|
|
1712
1845
|
}
|
|
1713
1846
|
if (preset.orm === "drizzle") {
|
|
1714
1847
|
return `
|
|
1715
|
-
### 3b.
|
|
1848
|
+
### 3b. Create the tables
|
|
1849
|
+
|
|
1850
|
+
The tables are defined in \`src/schema.ts\`, which \`drizzle.config.ts\` points at.
|
|
1851
|
+
drizzle-kit does not read \`.env\`, so load it first:
|
|
1716
1852
|
|
|
1717
1853
|
\`\`\`bash
|
|
1718
|
-
|
|
1854
|
+
node --env-file=.env node_modules/drizzle-kit/bin.cjs push
|
|
1719
1855
|
\`\`\`
|
|
1720
|
-
`;
|
|
1856
|
+
${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" : ""}`;
|
|
1721
1857
|
}
|
|
1722
1858
|
return "";
|
|
1723
1859
|
}
|
|
1724
1860
|
function getStrategyDescription(strategy) {
|
|
1725
1861
|
switch (strategy) {
|
|
1726
1862
|
case "rls":
|
|
1727
|
-
return `- **Row-Level Security
|
|
1863
|
+
return `- **Row-Level Security**: PostgreSQL RLS policies filter rows by tenant automatically
|
|
1728
1864
|
- Each query sets \`app.current_tenant_id\` and RLS enforces isolation
|
|
1729
1865
|
- All tenants share one database and schema`;
|
|
1730
1866
|
case "schema":
|
|
1731
|
-
return `- **Schema-per-tenant
|
|
1867
|
+
return `- **Schema-per-tenant**: each tenant gets a dedicated PostgreSQL schema
|
|
1732
1868
|
- Queries are routed to the correct schema via search_path
|
|
1733
1869
|
- Shared database, isolated schemas`;
|
|
1734
1870
|
case "database":
|
|
1735
|
-
return `- **Database-per-tenant
|
|
1871
|
+
return `- **Database-per-tenant**: each tenant gets a fully isolated database
|
|
1736
1872
|
- Connection routing directs queries to the correct database
|
|
1737
1873
|
- Maximum isolation at the cost of more resource usage`;
|
|
1738
1874
|
case "collection":
|
|
1739
|
-
return `- **Collection-per-tenant
|
|
1875
|
+
return `- **Collection-per-tenant**: each tenant gets dedicated MongoDB collections
|
|
1740
1876
|
- Collection names are prefixed or namespaced by tenant ID
|
|
1741
1877
|
- Shared database, isolated collections`;
|
|
1742
1878
|
case "table-prefix":
|
|
1743
|
-
return `- **Table-prefix
|
|
1879
|
+
return `- **Table-prefix**: tenant-specific tables with a naming prefix
|
|
1744
1880
|
- Tables are prefixed with the tenant identifier
|
|
1745
1881
|
- Shared database, prefixed table names`;
|
|
1746
1882
|
default:
|
|
@@ -1767,7 +1903,10 @@ function generatePresetEnv(projectName, preset) {
|
|
|
1767
1903
|
dbUrl = `postgres://${postgresAppRole(dbName)}:${POSTGRES_APP_PASSWORD}@localhost:5432/${dbName}`;
|
|
1768
1904
|
adminUrlLine = `
|
|
1769
1905
|
# Superuser: bootstrap and migrations only. It bypasses row-level security.
|
|
1770
|
-
|
|
1906
|
+
DATABASE_SUPERUSER_URL=postgres://${dbName}:dev_password@localhost:5432/${dbName}
|
|
1907
|
+
|
|
1908
|
+
# Stratum's own login: the library's adminPool, which runs the Stratum migrations (see init.sql).
|
|
1909
|
+
STRATUM_ADMIN_DATABASE_URL=postgres://${postgresStratumRole(dbName)}:${POSTGRES_STRATUM_PASSWORD}@localhost:5432/${dbName}
|
|
1771
1910
|
`;
|
|
1772
1911
|
break;
|
|
1773
1912
|
case "mongodb":
|
|
@@ -1917,25 +2056,25 @@ function writeFile2(filePath, content) {
|
|
|
1917
2056
|
function generatePackageJson(projectName, template) {
|
|
1918
2057
|
const frameworkDeps = {
|
|
1919
2058
|
express: {
|
|
1920
|
-
express: "^4.
|
|
2059
|
+
express: "^4.22.3",
|
|
1921
2060
|
"@types/express": "^4.17.21"
|
|
1922
2061
|
},
|
|
1923
2062
|
fastify: {
|
|
1924
|
-
fastify: "^
|
|
2063
|
+
fastify: "^5.12.5"
|
|
1925
2064
|
},
|
|
1926
2065
|
nextjs: {
|
|
1927
2066
|
next: "^15.5.16",
|
|
1928
2067
|
react: "^19.0.0",
|
|
1929
2068
|
"react-dom": "^19.0.0",
|
|
1930
2069
|
"@types/react": "^19.0.0",
|
|
1931
|
-
"@types/react-dom": "^19.0.0"
|
|
1932
|
-
// The middleware verifies the tenant JWT with jose.
|
|
1933
|
-
jose: "^6.2.12"
|
|
2070
|
+
"@types/react-dom": "^19.0.0"
|
|
1934
2071
|
}
|
|
1935
2072
|
};
|
|
1936
2073
|
const deps = {
|
|
1937
2074
|
"@stratum-hq/lib": STRATUM_RANGES["@stratum-hq/lib"],
|
|
1938
2075
|
pg: "^8.11.0",
|
|
2076
|
+
// Every template verifies the tenant JWT with jose.
|
|
2077
|
+
jose: "^6.2.12",
|
|
1939
2078
|
...frameworkDeps[template]
|
|
1940
2079
|
};
|
|
1941
2080
|
return JSON.stringify(
|
|
@@ -1952,7 +2091,7 @@ function generatePackageJson(projectName, template) {
|
|
|
1952
2091
|
devDependencies: {
|
|
1953
2092
|
typescript: "^5.3.0",
|
|
1954
2093
|
"@types/node": "^20.11.0",
|
|
1955
|
-
...template === "nextjs" ? {} : { tsx: "^4.
|
|
2094
|
+
...template === "nextjs" ? {} : { tsx: "^4.19.3", "@types/pg": "^8.11.0" }
|
|
1956
2095
|
},
|
|
1957
2096
|
engines: {
|
|
1958
2097
|
node: ">=20.0.0"
|
|
@@ -2013,7 +2152,10 @@ function generateEnv(projectName) {
|
|
|
2013
2152
|
DATABASE_URL=postgres://${postgresAppRole(dbName)}:${POSTGRES_APP_PASSWORD}@localhost:5432/${dbName}
|
|
2014
2153
|
|
|
2015
2154
|
# Superuser: bootstrap and migrations only. It bypasses row-level security.
|
|
2016
|
-
|
|
2155
|
+
DATABASE_SUPERUSER_URL=postgres://${dbName}:dev_password@localhost:5432/${dbName}
|
|
2156
|
+
|
|
2157
|
+
# Stratum's own login: the library's adminPool, which runs the Stratum migrations (see init.sql).
|
|
2158
|
+
STRATUM_ADMIN_DATABASE_URL=postgres://${postgresStratumRole(dbName)}:${POSTGRES_STRATUM_PASSWORD}@localhost:5432/${dbName}
|
|
2017
2159
|
|
|
2018
2160
|
# Authentication
|
|
2019
2161
|
JWT_SECRET=${jwtSecret}
|
|
@@ -2025,75 +2167,8 @@ STRATUM_API_KEY=sk_test_your_key_here
|
|
|
2025
2167
|
NODE_ENV=development
|
|
2026
2168
|
`;
|
|
2027
2169
|
}
|
|
2028
|
-
function generateExpressServer(projectName) {
|
|
2029
|
-
return `import express from "express";
|
|
2030
|
-
import { Pool } from "pg";
|
|
2031
|
-
|
|
2032
|
-
const app = express();
|
|
2033
|
-
const port = process.env.PORT || 3000;
|
|
2034
|
-
|
|
2035
|
-
const pool = new Pool({
|
|
2036
|
-
connectionString: process.env.DATABASE_URL,
|
|
2037
|
-
});
|
|
2038
|
-
|
|
2039
|
-
app.use(express.json());
|
|
2040
|
-
|
|
2041
|
-
app.get("/health", (_req, res) => {
|
|
2042
|
-
res.json({ status: "ok", project: "${projectName}" });
|
|
2043
|
-
});
|
|
2044
|
-
|
|
2045
|
-
app.get("/tenants", async (_req, res) => {
|
|
2046
|
-
try {
|
|
2047
|
-
// Example: query your tenant table
|
|
2048
|
-
// const { rows } = await pool.query("SELECT * FROM tenants LIMIT 20");
|
|
2049
|
-
// res.json(rows);
|
|
2050
|
-
res.json({ message: "Replace this with your tenant queries", pool: !!pool });
|
|
2051
|
-
} catch (err) {
|
|
2052
|
-
res.status(500).json({ error: String(err) });
|
|
2053
|
-
}
|
|
2054
|
-
});
|
|
2055
|
-
|
|
2056
|
-
app.listen(port, () => {
|
|
2057
|
-
console.log(\`${projectName} server running on http://localhost:\${port}\`);
|
|
2058
|
-
});
|
|
2059
|
-
`;
|
|
2060
|
-
}
|
|
2061
|
-
function generateFastifyServer(projectName) {
|
|
2062
|
-
return `import Fastify from "fastify";
|
|
2063
|
-
import { Pool } from "pg";
|
|
2064
|
-
|
|
2065
|
-
const fastify = Fastify({ logger: true });
|
|
2066
|
-
const port = Number(process.env.PORT) || 3000;
|
|
2067
|
-
|
|
2068
|
-
const pool = new Pool({
|
|
2069
|
-
connectionString: process.env.DATABASE_URL,
|
|
2070
|
-
});
|
|
2071
|
-
|
|
2072
|
-
fastify.get("/health", async () => {
|
|
2073
|
-
return { status: "ok", project: "${projectName}" };
|
|
2074
|
-
});
|
|
2075
|
-
|
|
2076
|
-
fastify.get("/tenants", async (_request, reply) => {
|
|
2077
|
-
try {
|
|
2078
|
-
// Example: query your tenant table
|
|
2079
|
-
// const { rows } = await pool.query("SELECT * FROM tenants LIMIT 20");
|
|
2080
|
-
// return rows;
|
|
2081
|
-
return { message: "Replace this with your tenant queries", pool: !!pool };
|
|
2082
|
-
} catch (err) {
|
|
2083
|
-
return reply.status(500).send({ error: String(err) });
|
|
2084
|
-
}
|
|
2085
|
-
});
|
|
2086
|
-
|
|
2087
|
-
fastify.listen({ port, host: "0.0.0.0" }, (err) => {
|
|
2088
|
-
if (err) {
|
|
2089
|
-
fastify.log.error(err);
|
|
2090
|
-
process.exit(1);
|
|
2091
|
-
}
|
|
2092
|
-
});
|
|
2093
|
-
`;
|
|
2094
|
-
}
|
|
2095
2170
|
function generateNextjsPage(projectName) {
|
|
2096
|
-
return `// app/page.tsx
|
|
2171
|
+
return `// app/page.tsx: ${projectName} root page
|
|
2097
2172
|
export default function Home() {
|
|
2098
2173
|
return (
|
|
2099
2174
|
<main style={{ padding: "2rem", fontFamily: "sans-serif" }}>
|
|
@@ -2101,7 +2176,7 @@ export default function Home() {
|
|
|
2101
2176
|
<p>Multi-tenant app powered by Stratum.</p>
|
|
2102
2177
|
<ul>
|
|
2103
2178
|
<li>Configure tenants via the Stratum control plane</li>
|
|
2104
|
-
<li>The tenant comes from a verified JWT in <code>middleware.ts</code></li>
|
|
2179
|
+
<li>The tenant comes from a verified JWT in <code>src/middleware.ts</code></li>
|
|
2105
2180
|
<li>Use <code>@stratum-hq/lib</code> for tenant resolution</li>
|
|
2106
2181
|
</ul>
|
|
2107
2182
|
</main>
|
|
@@ -2126,7 +2201,7 @@ docker compose up -d
|
|
|
2126
2201
|
|
|
2127
2202
|
\`\`\`bash
|
|
2128
2203
|
cp .env.example .env
|
|
2129
|
-
# Edit .env
|
|
2204
|
+
# Edit .env: update DATABASE_URL, JWT_SECRET, and STRATUM_API_KEY
|
|
2130
2205
|
\`\`\`
|
|
2131
2206
|
|
|
2132
2207
|
### 3. Install dependencies
|
|
@@ -2156,9 +2231,9 @@ ${template === "nextjs" ? "" : "\u251C\u2500\u2500 tsconfig.json\n"}\u2514\u2500
|
|
|
2156
2231
|
|
|
2157
2232
|
This project uses Stratum for hierarchical multi-tenancy:
|
|
2158
2233
|
|
|
2159
|
-
- **Tenant resolution
|
|
2160
|
-
- **Config inheritance
|
|
2161
|
-
- **Permission ABAC
|
|
2234
|
+
${template === "nextjs" ? "- **Tenant resolution**: from the `tenant_id` claim of a bearer token verified with `JWT_SECRET` (see `src/middleware.ts`); the subdomain is only a display slug" : "- **Tenant resolution**: the tenant middleware in `src/index.ts` takes the tenant from the `tenant_id` claim of a bearer token verified with `JWT_SECRET` (HS256, using `jose`). A token that does not verify is rejected with 401, and `GET /tenants` answers 401 without a tenant. The hostname and headers such as `x-tenant-id` are never used"}
|
|
2235
|
+
- **Config inheritance**: settings flow down the tenant tree with override support
|
|
2236
|
+
- **Permission ABAC**: role-based permissions with tenant-scoped enforcement
|
|
2162
2237
|
|
|
2163
2238
|
See the [Stratum docs](https://github.com/stratum-hq/Stratum) for full reference.
|
|
2164
2239
|
`;
|
|
@@ -2175,14 +2250,15 @@ Creating ${projectName} with ${template} template...
|
|
|
2175
2250
|
writeFile2(path2.join(targetDir, "init.sql"), generateInitSql(projectName));
|
|
2176
2251
|
writeFile2(path2.join(targetDir, ".env.example"), generateEnv(projectName));
|
|
2177
2252
|
if (template === "express") {
|
|
2178
|
-
writeFile2(path2.join(targetDir, "src", "index.ts"),
|
|
2253
|
+
writeFile2(path2.join(targetDir, "src", "index.ts"), expressServer(projectName));
|
|
2179
2254
|
writeFile2(path2.join(targetDir, "tsconfig.json"), generateTsconfig(template));
|
|
2180
2255
|
} else if (template === "fastify") {
|
|
2181
|
-
writeFile2(path2.join(targetDir, "src", "index.ts"),
|
|
2256
|
+
writeFile2(path2.join(targetDir, "src", "index.ts"), fastifyServer(projectName));
|
|
2182
2257
|
writeFile2(path2.join(targetDir, "tsconfig.json"), generateTsconfig(template));
|
|
2183
2258
|
} else if (template === "nextjs") {
|
|
2259
|
+
writeFile2(path2.join(targetDir, "src", "app", "layout.tsx"), nextjsRootLayout(projectName));
|
|
2184
2260
|
writeFile2(path2.join(targetDir, "src", "app", "page.tsx"), generateNextjsPage(projectName));
|
|
2185
|
-
writeFile2(path2.join(targetDir, "middleware.ts"), nextjsTenantMiddleware());
|
|
2261
|
+
writeFile2(path2.join(targetDir, "src", "middleware.ts"), nextjsTenantMiddleware());
|
|
2186
2262
|
}
|
|
2187
2263
|
writeFile2(path2.join(targetDir, "README.md"), generateReadme(projectName, template));
|
|
2188
2264
|
if (!skipInstall) {
|
|
@@ -2219,31 +2295,34 @@ function main(argv) {
|
|
|
2219
2295
|
console.error(`Error: ${validationError}`);
|
|
2220
2296
|
process.exit(1);
|
|
2221
2297
|
}
|
|
2222
|
-
|
|
2223
|
-
if (fs2.existsSync(targetDir) && !force) {
|
|
2224
|
-
console.error(`Error: Directory "${projectName}" already exists.`);
|
|
2225
|
-
console.error(`Use --force to overwrite.`);
|
|
2226
|
-
process.exit(1);
|
|
2227
|
-
}
|
|
2228
|
-
if (fs2.existsSync(targetDir) && force) {
|
|
2229
|
-
fs2.rmSync(targetDir, { recursive: true, force: true });
|
|
2230
|
-
}
|
|
2231
|
-
fs2.mkdirSync(targetDir, { recursive: true });
|
|
2298
|
+
let parsedPreset = null;
|
|
2232
2299
|
if (preset) {
|
|
2233
|
-
|
|
2234
|
-
if (!
|
|
2300
|
+
parsedPreset = parsePresetString(preset);
|
|
2301
|
+
if (!parsedPreset) {
|
|
2235
2302
|
console.error(`Error: Invalid preset string "${preset}".`);
|
|
2236
2303
|
console.error("Format: {database}-{strategy}-{orm}-{framework}");
|
|
2237
2304
|
console.error("Example: postgres-rls-prisma-express, mongodb-database-mongoose-hono");
|
|
2238
2305
|
process.exit(1);
|
|
2239
2306
|
}
|
|
2240
|
-
if (!isValidPreset(
|
|
2307
|
+
if (!isValidPreset(parsedPreset)) {
|
|
2241
2308
|
console.error(`Error: Invalid preset combination "${preset}".`);
|
|
2242
|
-
console.error(`${
|
|
2309
|
+
console.error(`${parsedPreset.database} does not support ${parsedPreset.strategy}/${parsedPreset.orm} together.`);
|
|
2243
2310
|
console.error("Run with --help to see valid combinations.");
|
|
2244
2311
|
process.exit(1);
|
|
2245
2312
|
}
|
|
2246
|
-
|
|
2313
|
+
}
|
|
2314
|
+
const targetDir = path2.resolve(process.cwd(), projectName);
|
|
2315
|
+
if (fs2.existsSync(targetDir) && !force) {
|
|
2316
|
+
console.error(`Error: Directory "${projectName}" already exists.`);
|
|
2317
|
+
console.error(`Use --force to overwrite.`);
|
|
2318
|
+
process.exit(1);
|
|
2319
|
+
}
|
|
2320
|
+
if (fs2.existsSync(targetDir) && force) {
|
|
2321
|
+
fs2.rmSync(targetDir, { recursive: true, force: true });
|
|
2322
|
+
}
|
|
2323
|
+
fs2.mkdirSync(targetDir, { recursive: true });
|
|
2324
|
+
if (parsedPreset) {
|
|
2325
|
+
createPresetProject(projectName, parsedPreset, targetDir, skipInstall);
|
|
2247
2326
|
} else {
|
|
2248
2327
|
createProject(projectName, template, targetDir, skipInstall);
|
|
2249
2328
|
}
|