@stratum-hq/create 0.2.3 → 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/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,13 +1,32 @@
1
1
  {
2
2
  "name": "@stratum-hq/create",
3
- "version": "0.2.3",
3
+ "version": "0.3.1",
4
4
  "description": "Create a new Stratum multi-tenancy project",
5
+ "keywords": [
6
+ "multi-tenancy",
7
+ "multi-tenant",
8
+ "saas",
9
+ "tenant",
10
+ "create",
11
+ "scaffolding",
12
+ "starter",
13
+ "boilerplate",
14
+ "node",
15
+ "typescript",
16
+ "stratum"
17
+ ],
5
18
  "bin": {
6
19
  "create-stratum": "./dist/index.js"
7
20
  },
21
+ "exports": {
22
+ ".": "./dist/index.js",
23
+ "./matrix": "./dist/matrix.js"
24
+ },
8
25
  "type": "module",
9
26
  "scripts": {
10
- "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 .",
11
30
  "test": "vitest run"
12
31
  },
13
32
  "devDependencies": {
@@ -20,6 +39,12 @@
20
39
  "registry": "https://registry.npmjs.org/"
21
40
  },
22
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
+ },
23
48
  "repository": {
24
49
  "type": "git",
25
50
  "url": "https://github.com/stratum-hq/Stratum.git",
@@ -0,0 +1,320 @@
1
+ import type { StackPreset } from "../matrix.js";
2
+
3
+ export interface DbSetupFile {
4
+ filename: string;
5
+ content: string;
6
+ }
7
+
8
+ export function generateDbSetup(preset: StackPreset): DbSetupFile[] {
9
+ switch (preset.orm) {
10
+ case "prisma":
11
+ return generatePrismaSetup(preset);
12
+ case "drizzle":
13
+ return generateDrizzleSetup(preset);
14
+ case "sequelize":
15
+ return generateSequelizeSetup(preset);
16
+ case "knex":
17
+ return generateKnexSetup(preset);
18
+ case "mongoose":
19
+ return generateMongooseSetup(preset);
20
+ case "pg":
21
+ return generatePgSetup(preset);
22
+ }
23
+ }
24
+
25
+ function generatePrismaSetup(preset: StackPreset): DbSetupFile[] {
26
+ const provider = preset.database === "mysql" ? "mysql" : "postgresql";
27
+ const envUrl = preset.database === "mysql"
28
+ ? "mysql://user:password@localhost:3306/dbname"
29
+ : "postgres://user:password@localhost:5432/dbname";
30
+
31
+ return [
32
+ {
33
+ filename: "prisma/schema.prisma",
34
+ content: `// Prisma schema for Stratum multi-tenancy
35
+ generator client {
36
+ provider = "prisma-client-js"
37
+ }
38
+
39
+ datasource db {
40
+ provider = "${provider}"
41
+ url = env("DATABASE_URL")
42
+ }
43
+
44
+ model Tenant {
45
+ id String @id @default(uuid())
46
+ name String
47
+ createdAt DateTime @default(now()) @map("created_at")
48
+
49
+ @@map("tenants")
50
+ }
51
+
52
+ // Add your tenant-scoped models here.
53
+ // Each model that needs tenant isolation should have a tenantId field.
54
+ `,
55
+ },
56
+ {
57
+ filename: "src/stratum-prisma.ts",
58
+ content: `// Prisma client with Stratum tenant-scoped queries
59
+ import { PrismaClient } from "@prisma/client";
60
+ import { Pool } from "pg";
61
+ import { prismaWithTenant } from "@stratum-hq/db-adapters";
62
+
63
+ const prisma = new PrismaClient();
64
+ const pool = new Pool({
65
+ connectionString: process.env.DATABASE_URL,
66
+ });
67
+
68
+ // Create a tenant-scoped Prisma client.
69
+ // All queries through this client are automatically filtered by RLS.
70
+ export function createTenantPrisma(getTenantId: () => string) {
71
+ return prismaWithTenant(prisma, getTenantId, pool);
72
+ }
73
+
74
+ // Usage:
75
+ // const tenantPrisma = createTenantPrisma(() => currentTenantId);
76
+ // const orders = await tenantPrisma.order.findMany();
77
+
78
+ export { prisma, pool };
79
+ `,
80
+ },
81
+ ];
82
+ }
83
+
84
+ function generateDrizzleSetup(preset: StackPreset): DbSetupFile[] {
85
+ const files: DbSetupFile[] = [];
86
+
87
+ if (preset.database === "mysql") {
88
+ files.push({
89
+ filename: "src/stratum-drizzle.ts",
90
+ content: `// Drizzle ORM with Stratum tenant-scoped queries (MySQL)
91
+ import { drizzle } from "drizzle-orm/mysql2";
92
+ import mysql from "mysql2/promise";
93
+
94
+ const connection = await mysql.createConnection({
95
+ uri: process.env.DATABASE_URL,
96
+ });
97
+
98
+ export const db = drizzle(connection);
99
+
100
+ // For tenant scoping, prefix table access with the tenant database name
101
+ // or use the @stratum-hq/mysql adapter for automatic routing.
102
+ `,
103
+ });
104
+ } else {
105
+ files.push({
106
+ filename: "src/stratum-drizzle.ts",
107
+ content: `// Drizzle ORM with Stratum tenant-scoped queries
108
+ import { drizzle } from "drizzle-orm/node-postgres";
109
+ import { Pool } from "pg";
110
+ import { drizzleWithTenant } from "@stratum-hq/db-adapters";
111
+
112
+ const pool = new Pool({
113
+ connectionString: process.env.DATABASE_URL,
114
+ });
115
+
116
+ export const db = drizzle(pool);
117
+
118
+ // Create a tenant-scoped Drizzle instance.
119
+ // Queries are automatically filtered by RLS policy.
120
+ export function createTenantDb(getTenantId: () => string) {
121
+ return drizzleWithTenant(db, getTenantId, pool);
122
+ }
123
+
124
+ // Usage:
125
+ // const tenantDb = createTenantDb(() => currentTenantId);
126
+ // const rows = await tenantDb.select().from(orders);
127
+
128
+ export { pool };
129
+ `,
130
+ });
131
+ }
132
+
133
+ files.push({
134
+ filename: "drizzle.config.ts",
135
+ content: `import type { Config } from "drizzle-kit";
136
+
137
+ export default {
138
+ schema: "./src/schema.ts",
139
+ out: "./drizzle",
140
+ ${preset.database === "mysql" ? 'dialect: "mysql",' : 'dialect: "postgresql",'}
141
+ dbCredentials: {
142
+ url: process.env.DATABASE_URL!,
143
+ },
144
+ } satisfies Config;
145
+ `,
146
+ });
147
+
148
+ return files;
149
+ }
150
+
151
+ function generateSequelizeSetup(preset: StackPreset): DbSetupFile[] {
152
+ const dialect = preset.database === "mysql" ? "mysql" : "postgres";
153
+
154
+ return [
155
+ {
156
+ filename: "src/stratum-sequelize.ts",
157
+ content: `// Sequelize with Stratum tenant-scoped queries
158
+ import { Sequelize, DataTypes, Model } from "sequelize";
159
+
160
+ const sequelize = new Sequelize(process.env.DATABASE_URL!, {
161
+ dialect: "${dialect}",
162
+ logging: false,
163
+ });
164
+
165
+ // Define a tenant-scoped model example
166
+ class Tenant extends Model {}
167
+ Tenant.init(
168
+ {
169
+ id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4 },
170
+ name: { type: DataTypes.STRING, allowNull: false },
171
+ },
172
+ { sequelize, tableName: "tenants", underscored: true },
173
+ );
174
+
175
+ // For tenant isolation, add a tenantId scope to each model:
176
+ // MyModel.addScope("tenant", (tenantId) => ({ where: { tenant_id: tenantId } }));
177
+ // const rows = await MyModel.scope({ method: ["tenant", currentTenantId] }).findAll();
178
+
179
+ export { sequelize, Tenant };
180
+ `,
181
+ },
182
+ ];
183
+ }
184
+
185
+ function generateKnexSetup(preset: StackPreset): DbSetupFile[] {
186
+ const client = preset.database === "mysql" ? "mysql2" : "pg";
187
+
188
+ return [
189
+ {
190
+ filename: "knexfile.ts",
191
+ content: `// Knex configuration for Stratum
192
+ import type { Knex } from "knex";
193
+
194
+ const config: Knex.Config = {
195
+ client: "${client}",
196
+ connection: process.env.DATABASE_URL,
197
+ migrations: {
198
+ directory: "./migrations",
199
+ extension: "ts",
200
+ },
201
+ };
202
+
203
+ export default config;
204
+ `,
205
+ },
206
+ {
207
+ filename: "src/stratum-knex.ts",
208
+ content: `// Knex with Stratum tenant-scoped queries
209
+ import Knex from "knex";
210
+ import config from "../knexfile.js";
211
+
212
+ const knex = Knex(config);
213
+
214
+ // Create a tenant-scoped query builder.
215
+ // For RLS strategy, set the session variable before queries.
216
+ export async function withTenantScope(tenantId: string, fn: (db: typeof knex) => Promise<void>) {
217
+ ${preset.database === "postgres" ? ` await knex.raw("SET app.current_tenant = ?", [tenantId]);` : ` // For ${preset.database}, scope queries by tenant_id column`}
218
+ try {
219
+ await fn(knex);
220
+ } finally {
221
+ ${preset.database === "postgres" ? ` await knex.raw("RESET app.current_tenant");` : ` // Scope cleanup not needed for column-based isolation`}
222
+ }
223
+ }
224
+
225
+ export { knex };
226
+ `,
227
+ },
228
+ ];
229
+ }
230
+
231
+ function generateMongooseSetup(_preset: StackPreset): DbSetupFile[] {
232
+ return [
233
+ {
234
+ filename: "src/stratum-mongoose.ts",
235
+ content: `// Mongoose with Stratum multi-tenant support
236
+ import mongoose from "mongoose";
237
+ import { createTenantConnection } from "@stratum-hq/mongodb";
238
+
239
+ // Main connection (used for tenant metadata)
240
+ const mainConnection = mongoose.createConnection(
241
+ process.env.MONGODB_URI || "mongodb://localhost:27017/main",
242
+ );
243
+
244
+ // Create a tenant-scoped connection.
245
+ // Each tenant gets its own database (database-per-tenant strategy)
246
+ // or collection prefix (collection strategy).
247
+ export function getTenantConnection(tenantId: string) {
248
+ return createTenantConnection(mainConnection, tenantId);
249
+ }
250
+
251
+ // Define schemas that work across tenant connections
252
+ export const TenantSchema = new mongoose.Schema({
253
+ name: { type: String, required: true },
254
+ createdAt: { type: Date, default: Date.now },
255
+ });
256
+
257
+ // Usage:
258
+ // const conn = getTenantConnection("tenant-abc");
259
+ // const Order = conn.model("Order", OrderSchema);
260
+ // const orders = await Order.find();
261
+
262
+ export { mainConnection };
263
+ `,
264
+ },
265
+ ];
266
+ }
267
+
268
+ function generatePgSetup(preset: StackPreset): DbSetupFile[] {
269
+ if (preset.database === "mysql") {
270
+ return [
271
+ {
272
+ filename: "src/stratum-db.ts",
273
+ content: `// Raw MySQL client with Stratum tenant-scoped queries
274
+ import mysql from "mysql2/promise";
275
+
276
+ const pool = mysql.createPool({
277
+ uri: process.env.DATABASE_URL,
278
+ });
279
+
280
+ // Execute a query scoped to a tenant by filtering on tenant_id
281
+ export async function tenantQuery(tenantId: string, sql: string, params: unknown[] = []) {
282
+ const [rows] = await pool.execute(sql, [...params, tenantId]);
283
+ return rows;
284
+ }
285
+
286
+ // Usage:
287
+ // const orders = await tenantQuery("tenant-abc", "SELECT * FROM orders WHERE tenant_id = ?");
288
+
289
+ export { pool };
290
+ `,
291
+ },
292
+ ];
293
+ }
294
+
295
+ return [
296
+ {
297
+ filename: "src/stratum-db.ts",
298
+ content: `// PostgreSQL client with Stratum tenant-scoped queries
299
+ import { Pool } from "pg";
300
+ import { createTenantPool } from "@stratum-hq/db-adapters";
301
+
302
+ const pool = new Pool({
303
+ connectionString: process.env.DATABASE_URL,
304
+ });
305
+
306
+ // Create a tenant-scoped pool.
307
+ // Sets app.current_tenant on each connection for RLS enforcement.
308
+ export function getTenantPool(tenantId: string) {
309
+ return createTenantPool(pool, tenantId);
310
+ }
311
+
312
+ // Usage:
313
+ // const tenantPool = getTenantPool("tenant-abc");
314
+ // const { rows } = await tenantPool.query("SELECT * FROM orders");
315
+
316
+ export { pool };
317
+ `,
318
+ },
319
+ ];
320
+ }
@@ -0,0 +1,98 @@
1
+ import type { StackPreset } from "../matrix.js";
2
+
3
+ export function generatePresetDockerCompose(projectName: string, preset: StackPreset): string {
4
+ const dbName = projectName.replace(/[^a-z0-9]/gi, "_").toLowerCase();
5
+
6
+ switch (preset.database) {
7
+ case "postgres":
8
+ return generatePostgresCompose(projectName, dbName);
9
+ case "mongodb":
10
+ return generateMongoCompose(projectName, dbName);
11
+ case "mysql":
12
+ return generateMysqlCompose(projectName, dbName);
13
+ }
14
+ }
15
+
16
+ function generatePostgresCompose(projectName: string, dbName: string): string {
17
+ return `# Docker Compose for ${projectName}
18
+ # Start with: docker compose up -d
19
+ version: "3.8"
20
+
21
+ services:
22
+ db:
23
+ image: postgres:16-alpine
24
+ environment:
25
+ POSTGRES_DB: ${dbName}
26
+ POSTGRES_USER: ${dbName}
27
+ POSTGRES_PASSWORD: dev_password
28
+ ports:
29
+ - "5432:5432"
30
+ volumes:
31
+ - db_data:/var/lib/postgresql/data
32
+ - ./init.sql:/docker-entrypoint-initdb.d/init.sql
33
+ healthcheck:
34
+ test: ["CMD-SHELL", "pg_isready -U ${dbName}"]
35
+ interval: 5s
36
+ timeout: 5s
37
+ retries: 5
38
+
39
+ volumes:
40
+ db_data:
41
+ `;
42
+ }
43
+
44
+ function generateMongoCompose(projectName: string, dbName: string): string {
45
+ return `# Docker Compose for ${projectName}
46
+ # Start with: docker compose up -d
47
+ version: "3.8"
48
+
49
+ services:
50
+ db:
51
+ image: mongo:7
52
+ environment:
53
+ MONGO_INITDB_ROOT_USERNAME: ${dbName}
54
+ MONGO_INITDB_ROOT_PASSWORD: dev_password
55
+ MONGO_INITDB_DATABASE: ${dbName}
56
+ ports:
57
+ - "27017:27017"
58
+ volumes:
59
+ - db_data:/data/db
60
+ healthcheck:
61
+ test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
62
+ interval: 5s
63
+ timeout: 5s
64
+ retries: 5
65
+
66
+ volumes:
67
+ db_data:
68
+ `;
69
+ }
70
+
71
+ function generateMysqlCompose(projectName: string, dbName: string): string {
72
+ return `# Docker Compose for ${projectName}
73
+ # Start with: docker compose up -d
74
+ version: "3.8"
75
+
76
+ services:
77
+ db:
78
+ image: mysql:8
79
+ environment:
80
+ MYSQL_DATABASE: ${dbName}
81
+ MYSQL_USER: ${dbName}
82
+ MYSQL_PASSWORD: dev_password
83
+ MYSQL_ROOT_PASSWORD: dev_root_password
84
+ ports:
85
+ - "3306:3306"
86
+ volumes:
87
+ - db_data:/var/lib/mysql
88
+ - ./init.sql:/docker-entrypoint-initdb.d/init.sql
89
+ healthcheck:
90
+ test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
91
+ interval: 5s
92
+ timeout: 5s
93
+ retries: 5
94
+
95
+ volumes:
96
+ db_data:
97
+ `;
98
+ }
@@ -0,0 +1,57 @@
1
+ import type { StackPreset } from "../matrix.js";
2
+
3
+ export function generatePresetInitSql(projectName: string, preset: StackPreset): string | null {
4
+ const dbName = projectName.replace(/[^a-z0-9]/gi, "_").toLowerCase();
5
+
6
+ switch (preset.database) {
7
+ case "postgres":
8
+ return generatePostgresInit(projectName, dbName, preset.strategy);
9
+ case "mongodb":
10
+ // MongoDB does not use SQL initialization
11
+ return null;
12
+ case "mysql":
13
+ return generateMysqlInit(projectName, dbName);
14
+ }
15
+ }
16
+
17
+ function generatePostgresInit(projectName: string, dbName: string, strategy: string): string {
18
+ let rlsBlock = "";
19
+ if (strategy === "rls") {
20
+ rlsBlock = `
21
+ -- Enable Row-Level Security for tenant isolation
22
+ -- Add RLS policies to each tenant-scoped table:
23
+ --
24
+ -- ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;
25
+ -- CREATE POLICY tenant_isolation ON your_table
26
+ -- USING (tenant_id = current_setting('app.current_tenant')::uuid);
27
+ --
28
+ -- The Stratum db-adapters package sets app.current_tenant automatically.
29
+ `;
30
+ }
31
+
32
+ return `-- Initialize ${projectName} database
33
+ -- Enable required extensions for Stratum multi-tenancy
34
+ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
35
+ CREATE EXTENSION IF NOT EXISTS "ltree";
36
+
37
+ -- The ltree extension enables hierarchical tenant trees
38
+ -- uuid-ossp provides uuid_generate_v4() for tenant IDs
39
+ COMMENT ON DATABASE ${dbName} IS 'Multi-tenant database for ${projectName}';
40
+ ${rlsBlock}`;
41
+ }
42
+
43
+ function generateMysqlInit(projectName: string, dbName: string): string {
44
+ return `-- Initialize ${projectName} database
45
+ -- MySQL setup for Stratum multi-tenancy
46
+
47
+ -- Ensure utf8mb4 for the database
48
+ ALTER DATABASE ${dbName} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
49
+
50
+ -- Tenant metadata table
51
+ CREATE TABLE IF NOT EXISTS _stratum_tenants (
52
+ id VARCHAR(36) PRIMARY KEY,
53
+ name VARCHAR(255) NOT NULL,
54
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
55
+ );
56
+ `;
57
+ }