@loomcore/api 0.1.59 → 0.1.62

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.
Files changed (37) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +49 -49
  3. package/dist/__tests__/common-test.utils.js +10 -8
  4. package/dist/__tests__/postgres-test-migrations/postgres-test-schema.js +52 -52
  5. package/dist/__tests__/postgres.test-database.js +8 -8
  6. package/dist/__tests__/test-email-client.d.ts +4 -0
  7. package/dist/__tests__/test-email-client.js +6 -0
  8. package/dist/controllers/auth.controller.d.ts +2 -1
  9. package/dist/controllers/auth.controller.js +2 -3
  10. package/dist/databases/migrations/migration-runner.d.ts +3 -1
  11. package/dist/databases/migrations/migration-runner.js +27 -25
  12. package/dist/databases/mongo-db/migrations/mongo-initial-schema.d.ts +3 -2
  13. package/dist/databases/mongo-db/migrations/mongo-initial-schema.js +127 -125
  14. package/dist/databases/postgres/commands/postgres-batch-update.command.js +7 -7
  15. package/dist/databases/postgres/commands/postgres-create-many.command.js +4 -4
  16. package/dist/databases/postgres/commands/postgres-create.command.js +4 -4
  17. package/dist/databases/postgres/commands/postgres-full-update-by-id.command.js +13 -13
  18. package/dist/databases/postgres/commands/postgres-partial-update-by-id.command.js +7 -7
  19. package/dist/databases/postgres/commands/postgres-update.command.js +7 -7
  20. package/dist/databases/postgres/migrations/__tests__/test-migration-helper.js +2 -1
  21. package/dist/databases/postgres/migrations/postgres-initial-schema.d.ts +2 -1
  22. package/dist/databases/postgres/migrations/postgres-initial-schema.js +284 -281
  23. package/dist/databases/postgres/postgres.database.js +17 -17
  24. package/dist/databases/postgres/utils/build-select-clause.js +6 -6
  25. package/dist/databases/postgres/utils/does-table-exist.util.js +4 -4
  26. package/dist/models/base-api-config.interface.d.ts +7 -10
  27. package/dist/models/email-client.interface.d.ts +3 -0
  28. package/dist/models/email-client.interface.js +1 -0
  29. package/dist/models/email-config.interface.d.ts +4 -0
  30. package/dist/models/email-config.interface.js +1 -0
  31. package/dist/models/multi-tenant-config.interface.d.ts +4 -0
  32. package/dist/models/multi-tenant-config.interface.js +1 -0
  33. package/dist/services/auth.service.d.ts +2 -1
  34. package/dist/services/auth.service.js +2 -2
  35. package/dist/services/email.service.d.ts +5 -4
  36. package/dist/services/email.service.js +12 -20
  37. package/package.json +88 -88
@@ -5,22 +5,22 @@ export const getPostgresTestSchema = (config) => {
5
5
  name: '00000000000100_schema-test-entities',
6
6
  up: async ({ context: pool }) => {
7
7
  const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : '';
8
- await pool.query(`
9
- CREATE TABLE IF NOT EXISTS "testEntities" (
10
- "_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
11
- ${orgColumnDef}
12
- "name" VARCHAR(255) NOT NULL,
13
- "description" TEXT,
14
- "isActive" BOOLEAN,
15
- "tags" TEXT[],
16
- "count" INTEGER,
17
- "_created" TIMESTAMPTZ NOT NULL,
18
- "_createdBy" INTEGER NOT NULL,
19
- "_updated" TIMESTAMPTZ NOT NULL,
20
- "_updatedBy" INTEGER NOT NULL,
21
- "_deleted" TIMESTAMPTZ,
22
- "_deletedBy" INTEGER
23
- )
8
+ await pool.query(`
9
+ CREATE TABLE IF NOT EXISTS "testEntities" (
10
+ "_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
11
+ ${orgColumnDef}
12
+ "name" VARCHAR(255) NOT NULL,
13
+ "description" TEXT,
14
+ "isActive" BOOLEAN,
15
+ "tags" TEXT[],
16
+ "count" INTEGER,
17
+ "_created" TIMESTAMPTZ NOT NULL,
18
+ "_createdBy" INTEGER NOT NULL,
19
+ "_updated" TIMESTAMPTZ NOT NULL,
20
+ "_updatedBy" INTEGER NOT NULL,
21
+ "_deleted" TIMESTAMPTZ,
22
+ "_deletedBy" INTEGER
23
+ )
24
24
  `);
25
25
  },
26
26
  down: async ({ context: pool }) => {
@@ -31,12 +31,12 @@ export const getPostgresTestSchema = (config) => {
31
31
  name: '00000000000101_schema-categories',
32
32
  up: async ({ context: pool }) => {
33
33
  const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : '';
34
- await pool.query(`
35
- CREATE TABLE IF NOT EXISTS "categories" (
36
- "_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
37
- ${orgColumnDef}
38
- "name" VARCHAR(255) NOT NULL
39
- )
34
+ await pool.query(`
35
+ CREATE TABLE IF NOT EXISTS "categories" (
36
+ "_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
37
+ ${orgColumnDef}
38
+ "name" VARCHAR(255) NOT NULL
39
+ )
40
40
  `);
41
41
  },
42
42
  down: async ({ context: pool }) => {
@@ -47,22 +47,22 @@ export const getPostgresTestSchema = (config) => {
47
47
  name: '00000000000102_schema-products',
48
48
  up: async ({ context: pool }) => {
49
49
  const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : '';
50
- await pool.query(`
51
- CREATE TABLE IF NOT EXISTS "products" (
52
- "_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
53
- ${orgColumnDef}
54
- "name" VARCHAR(255) NOT NULL,
55
- "description" TEXT,
56
- "internalNumber" VARCHAR(255),
57
- "categoryId" INTEGER NOT NULL,
58
- "_created" TIMESTAMPTZ NOT NULL,
59
- "_createdBy" INTEGER NOT NULL,
60
- "_updated" TIMESTAMPTZ NOT NULL,
61
- "_updatedBy" INTEGER NOT NULL,
62
- "_deleted" TIMESTAMPTZ,
63
- "_deletedBy" INTEGER,
64
- CONSTRAINT "fk_products_category" FOREIGN KEY ("categoryId") REFERENCES "categories"("_id") ON DELETE CASCADE
65
- )
50
+ await pool.query(`
51
+ CREATE TABLE IF NOT EXISTS "products" (
52
+ "_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
53
+ ${orgColumnDef}
54
+ "name" VARCHAR(255) NOT NULL,
55
+ "description" TEXT,
56
+ "internalNumber" VARCHAR(255),
57
+ "categoryId" INTEGER NOT NULL,
58
+ "_created" TIMESTAMPTZ NOT NULL,
59
+ "_createdBy" INTEGER NOT NULL,
60
+ "_updated" TIMESTAMPTZ NOT NULL,
61
+ "_updatedBy" INTEGER NOT NULL,
62
+ "_deleted" TIMESTAMPTZ,
63
+ "_deletedBy" INTEGER,
64
+ CONSTRAINT "fk_products_category" FOREIGN KEY ("categoryId") REFERENCES "categories"("_id") ON DELETE CASCADE
65
+ )
66
66
  `);
67
67
  },
68
68
  down: async ({ context: pool }) => {
@@ -73,20 +73,20 @@ export const getPostgresTestSchema = (config) => {
73
73
  name: '00000000000103_schema-test-items',
74
74
  up: async ({ context: pool }) => {
75
75
  const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : '';
76
- await pool.query(`
77
- CREATE TABLE IF NOT EXISTS "testItems" (
78
- "_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
79
- ${orgColumnDef}
80
- "name" VARCHAR(255) NOT NULL,
81
- "value" INTEGER,
82
- "eventDate" TIMESTAMPTZ,
83
- "_created" TIMESTAMPTZ NOT NULL,
84
- "_createdBy" INTEGER NOT NULL,
85
- "_updated" TIMESTAMPTZ NOT NULL,
86
- "_updatedBy" INTEGER NOT NULL,
87
- "_deleted" TIMESTAMPTZ,
88
- "_deletedBy" INTEGER
89
- )
76
+ await pool.query(`
77
+ CREATE TABLE IF NOT EXISTS "testItems" (
78
+ "_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
79
+ ${orgColumnDef}
80
+ "name" VARCHAR(255) NOT NULL,
81
+ "value" INTEGER,
82
+ "eventDate" TIMESTAMPTZ,
83
+ "_created" TIMESTAMPTZ NOT NULL,
84
+ "_createdBy" INTEGER NOT NULL,
85
+ "_updated" TIMESTAMPTZ NOT NULL,
86
+ "_updatedBy" INTEGER NOT NULL,
87
+ "_deleted" TIMESTAMPTZ,
88
+ "_deletedBy" INTEGER
89
+ )
90
90
  `);
91
91
  },
92
92
  down: async ({ context: pool }) => {
@@ -40,9 +40,9 @@ export class TestPostgresDatabase {
40
40
  }
41
41
  async createIndexes(client) {
42
42
  try {
43
- await client.query(`
44
- CREATE INDEX IF NOT EXISTS email_index ON users (LOWER(email));
45
- CREATE UNIQUE INDEX IF NOT EXISTS email_unique_index ON users (LOWER(email));
43
+ await client.query(`
44
+ CREATE INDEX IF NOT EXISTS email_index ON users (LOWER(email));
45
+ CREATE UNIQUE INDEX IF NOT EXISTS email_unique_index ON users (LOWER(email));
46
46
  `);
47
47
  }
48
48
  catch (error) {
@@ -53,11 +53,11 @@ export class TestPostgresDatabase {
53
53
  if (!this.postgresClient) {
54
54
  throw new Error('Database not initialized');
55
55
  }
56
- const result = await this.postgresClient.query(`
57
- SELECT "table_name"
58
- FROM information_schema.tables
59
- WHERE "table_schema" = 'public'
60
- AND "table_type" = 'BASE TABLE'
56
+ const result = await this.postgresClient.query(`
57
+ SELECT "table_name"
58
+ FROM information_schema.tables
59
+ WHERE "table_schema" = 'public'
60
+ AND "table_type" = 'BASE TABLE'
61
61
  `);
62
62
  result.rows.forEach(async (row) => {
63
63
  await this.postgresClient?.query(`TRUNCATE TABLE "${row.table_name}" RESTART IDENTITY CASCADE`);
@@ -0,0 +1,4 @@
1
+ import { IEmailClient } from "../models/email-client.interface.js";
2
+ export declare class TestEmailClient implements IEmailClient {
3
+ sendHtmlEmail(toEmailAddress: string, subject: string, body: string): Promise<void>;
4
+ }
@@ -0,0 +1,6 @@
1
+ export class TestEmailClient {
2
+ sendHtmlEmail(toEmailAddress, subject, body) {
3
+ console.log(`Sending email to ${toEmailAddress} with subject ${subject} and body ${body}`);
4
+ return Promise.resolve();
5
+ }
6
+ }
@@ -1,9 +1,10 @@
1
1
  import { Application, Request, Response, NextFunction } from 'express';
2
2
  import { AuthService } from '../services/index.js';
3
3
  import { IDatabase } from '../databases/models/index.js';
4
+ import { IEmailClient } from '../models/email-client.interface.js';
4
5
  export declare class AuthController {
5
6
  authService: AuthService;
6
- constructor(app: Application, database: IDatabase);
7
+ constructor(app: Application, database: IDatabase, emailClient: IEmailClient);
7
8
  mapRoutes(app: Application): void;
8
9
  login(req: Request, res: Response, next: NextFunction): Promise<void>;
9
10
  registerUser(req: Request, res: Response): Promise<void>;
@@ -6,9 +6,8 @@ import { AuthService } from '../services/index.js';
6
6
  import { isAuthorized } from '../middleware/index.js';
7
7
  export class AuthController {
8
8
  authService;
9
- constructor(app, database) {
10
- const authService = new AuthService(database);
11
- this.authService = authService;
9
+ constructor(app, database, emailClient) {
10
+ this.authService = new AuthService(database, emailClient);
12
11
  this.mapRoutes(app);
13
12
  }
14
13
  mapRoutes(app) {
@@ -1,4 +1,5 @@
1
1
  import { IBaseApiConfig } from '../../models/base-api-config.interface.js';
2
+ import { IEmailClient } from '../../models/email-client.interface.js';
2
3
  export declare class MigrationRunner {
3
4
  private config;
4
5
  private dbType;
@@ -6,7 +7,8 @@ export declare class MigrationRunner {
6
7
  private migrationsDir;
7
8
  private primaryTimezone;
8
9
  private dbConnection;
9
- constructor(config: IBaseApiConfig);
10
+ private emailClient?;
11
+ constructor(config: IBaseApiConfig, emailClient?: IEmailClient);
10
12
  private getTimestamp;
11
13
  private parseSql;
12
14
  private getMigrator;
@@ -15,13 +15,15 @@ export class MigrationRunner {
15
15
  migrationsDir;
16
16
  primaryTimezone;
17
17
  dbConnection;
18
- constructor(config) {
18
+ emailClient;
19
+ constructor(config, emailClient) {
19
20
  setBaseApiConfig(config);
20
21
  this.config = config;
21
- this.dbType = config.app.dbType || 'mongodb';
22
+ this.dbType = config.app.dbType;
22
23
  this.dbUrl = this.dbType === 'postgres' ? buildPostgresUrl(config) : buildMongoUrl(config);
23
24
  this.migrationsDir = path.join(process.cwd(), 'database', 'migrations');
24
25
  this.primaryTimezone = config.app.primaryTimezone || 'UTC';
26
+ this.emailClient = emailClient;
25
27
  }
26
28
  getTimestamp() {
27
29
  const now = new Date();
@@ -58,7 +60,7 @@ export class MigrationRunner {
58
60
  this.dbConnection = pool;
59
61
  return new Umzug({
60
62
  migrations: async () => {
61
- const initialSchema = getPostgresInitialSchema(this.config).map(m => ({
63
+ const initialSchema = getPostgresInitialSchema(this.config, this.emailClient).map(m => ({
62
64
  name: m.name,
63
65
  up: async () => {
64
66
  console.log(` Running [LIBRARY] ${m.name}...`);
@@ -97,7 +99,7 @@ export class MigrationRunner {
97
99
  console.log(`🔎 Looking for migrations in: ${globPattern}`);
98
100
  return new Umzug({
99
101
  migrations: async () => {
100
- const initialSchema = getMongoInitialSchema(this.config).map(m => ({
102
+ const initialSchema = getMongoInitialSchema(this.config, this.emailClient).map(m => ({
101
103
  name: m.name,
102
104
  up: async () => {
103
105
  console.log(` Running [LIBRARY] ${m.name}...`);
@@ -239,31 +241,31 @@ export class MigrationRunner {
239
241
  let content = '';
240
242
  if (this.dbType === 'postgres') {
241
243
  extension = 'sql';
242
- content = `-- Migration: ${safeName}
243
- -- Created: ${new Date().toISOString()}
244
-
245
- -- up
246
- -- Write your CREATE/ALTER statements here...
247
-
248
-
249
- -- down
250
- -- Write your DROP/UNDO statements here...
244
+ content = `-- Migration: ${safeName}
245
+ -- Created: ${new Date().toISOString()}
246
+
247
+ -- up
248
+ -- Write your CREATE/ALTER statements here...
249
+
250
+
251
+ -- down
252
+ -- Write your DROP/UNDO statements here...
251
253
  `;
252
254
  }
253
255
  else {
254
256
  extension = 'ts';
255
- content = `import { Db } from 'mongodb';
256
-
257
- // Migration: ${safeName}
258
- // Created: ${new Date().toISOString()}
259
-
260
- export const up = async ({ context: db }: { context: Db }) => {
261
- // await db.collection('...')....
262
- };
263
-
264
- export const down = async ({ context: db }: { context: Db }) => {
265
- // await db.collection('...')....
266
- };
257
+ content = `import { Db } from 'mongodb';
258
+
259
+ // Migration: ${safeName}
260
+ // Created: ${new Date().toISOString()}
261
+
262
+ export const up = async ({ context: db }: { context: Db }) => {
263
+ // await db.collection('...')....
264
+ };
265
+
266
+ export const down = async ({ context: db }: { context: Db }) => {
267
+ // await db.collection('...')....
268
+ };
267
269
  `;
268
270
  }
269
271
  const fullPath = path.join(this.migrationsDir, `${filename}.${extension}`);
@@ -1,6 +1,7 @@
1
1
  import { Db } from 'mongodb';
2
2
  import { IBaseApiConfig } from '../../../models/base-api-config.interface.js';
3
- export interface SyntheticMigration {
3
+ import { IEmailClient } from '../../../models/email-client.interface.js';
4
+ export interface ISyntheticMigration {
4
5
  name: string;
5
6
  up: (context: {
6
7
  context: Db;
@@ -9,4 +10,4 @@ export interface SyntheticMigration {
9
10
  context: Db;
10
11
  }) => Promise<void>;
11
12
  }
12
- export declare const getMongoInitialSchema: (config: IBaseApiConfig) => SyntheticMigration[];
13
+ export declare const getMongoInitialSchema: (config: IBaseApiConfig, emailClient?: IEmailClient) => ISyntheticMigration[];
@@ -2,18 +2,20 @@ import { randomUUID } from 'crypto';
2
2
  import { initializeSystemUserContext, EmptyUserContext, getSystemUserContext, isSystemUserContextInitialized } from '@loomcore/common/models';
3
3
  import { MongoDBDatabase } from '../mongo-db.database.js';
4
4
  import { AuthService, OrganizationService } from '../../../services/index.js';
5
- export const getMongoInitialSchema = (config) => {
5
+ export const getMongoInitialSchema = (config, emailClient) => {
6
6
  const migrations = [];
7
- const isMultiTenant = config.app.isMultiTenant === true;
8
- console.log('📋 Migration Config Diagnostic:');
9
- console.log(' isMultiTenant:', isMultiTenant);
10
- console.log(' config.app.metaOrgName:', config.app.metaOrgName ?? '(undefined)');
11
- console.log(' config.app.metaOrgCode:', config.app.metaOrgCode ?? '(undefined)');
12
- console.log(' config.auth?.adminUser?.email:', config.auth?.adminUser?.email ?? '(undefined)');
13
- console.log(' config.auth?.adminUser?.password:', config.auth?.adminUser?.password ? '(set)' : '(undefined)');
14
- console.log(' Will add meta-org migration:', isMultiTenant && !!config.app.metaOrgName && !!config.app.metaOrgCode);
15
- console.log(' Will add admin-user migration:', !!config.auth?.adminUser?.email && !!config.auth?.adminUser?.password);
16
- if (isMultiTenant) {
7
+ const isMultiTenant = config.app.isMultiTenant;
8
+ if (isMultiTenant && !config.multiTenant) {
9
+ throw new Error('Multi-tenant configuration is enabled but multi-tenant configuration is not provided');
10
+ }
11
+ const isAuthEnabled = config.app.isAuthEnabled;
12
+ if (isAuthEnabled && !config.auth) {
13
+ throw new Error('Auth enabled without auth configuration');
14
+ }
15
+ if (isAuthEnabled && (!emailClient || !config.email)) {
16
+ throw new Error('Auth enabled without email client or email configuration');
17
+ }
18
+ if (isMultiTenant)
17
19
  migrations.push({
18
20
  name: '00000000000001_schema-organizations',
19
21
  up: async ({ context: db }) => {
@@ -26,115 +28,120 @@ export const getMongoInitialSchema = (config) => {
26
28
  await db.collection('organizations').drop();
27
29
  }
28
30
  });
29
- }
30
- migrations.push({
31
- name: '00000000000002_schema-users',
32
- up: async ({ context: db }) => {
33
- await db.createCollection('users');
34
- if (isMultiTenant) {
35
- await db.collection('users').createIndex({ _orgId: 1, email: 1 }, { unique: true });
36
- await db.collection('users').createIndex({ _orgId: 1 });
37
- }
38
- else {
39
- await db.collection('users').createIndex({ email: 1 }, { unique: true });
40
- }
41
- },
42
- down: async ({ context: db }) => {
43
- await db.collection('users').drop();
44
- }
45
- });
46
- migrations.push({
47
- name: '00000000000003_schema-refresh-tokens',
48
- up: async ({ context: db }) => {
49
- await db.createCollection('refreshTokens');
50
- await db.collection('refreshTokens').createIndex({ token: 1 }, { unique: true });
51
- await db.collection('refreshTokens').createIndex({ userId: 1 });
52
- await db.collection('refreshTokens').createIndex({ deviceId: 1 });
53
- if (isMultiTenant) {
54
- await db.collection('refreshTokens').createIndex({ _orgId: 1 });
55
- }
56
- },
57
- down: async ({ context: db }) => {
58
- await db.collection('refreshTokens').drop();
59
- }
60
- });
61
- migrations.push({
62
- name: '00000000000004_schema-roles',
63
- up: async ({ context: db }) => {
64
- await db.createCollection('roles');
65
- if (isMultiTenant) {
66
- await db.collection('roles').createIndex({ _orgId: 1, name: 1 }, { unique: true });
67
- await db.collection('roles').createIndex({ _orgId: 1 });
68
- }
69
- else {
70
- await db.collection('roles').createIndex({ name: 1 }, { unique: true });
71
- }
72
- },
73
- down: async ({ context: db }) => {
74
- await db.collection('roles').drop();
75
- }
76
- });
77
- migrations.push({
78
- name: '00000000000005_schema-user-roles',
79
- up: async ({ context: db }) => {
80
- await db.createCollection('user_roles');
81
- if (isMultiTenant) {
82
- await db.collection('user_roles').createIndex({ _orgId: 1, userId: 1, roleId: 1 }, { unique: true });
83
- await db.collection('user_roles').createIndex({ _orgId: 1 });
31
+ if (isAuthEnabled)
32
+ migrations.push({
33
+ name: '00000000000002_schema-users',
34
+ up: async ({ context: db }) => {
35
+ await db.createCollection('users');
36
+ if (config.app.isMultiTenant) {
37
+ await db.collection('users').createIndex({ _orgId: 1, email: 1 }, { unique: true });
38
+ await db.collection('users').createIndex({ _orgId: 1 });
39
+ }
40
+ else {
41
+ await db.collection('users').createIndex({ email: 1 }, { unique: true });
42
+ }
43
+ },
44
+ down: async ({ context: db }) => {
45
+ await db.collection('users').drop();
84
46
  }
85
- else {
86
- await db.collection('user_roles').createIndex({ userId: 1, roleId: 1 }, { unique: true });
47
+ });
48
+ if (isAuthEnabled)
49
+ migrations.push({
50
+ name: '00000000000003_schema-refresh-tokens',
51
+ up: async ({ context: db }) => {
52
+ await db.createCollection('refreshTokens');
53
+ await db.collection('refreshTokens').createIndex({ token: 1 }, { unique: true });
54
+ await db.collection('refreshTokens').createIndex({ userId: 1 });
55
+ await db.collection('refreshTokens').createIndex({ deviceId: 1 });
56
+ if (isMultiTenant) {
57
+ await db.collection('refreshTokens').createIndex({ _orgId: 1 });
58
+ }
59
+ },
60
+ down: async ({ context: db }) => {
61
+ await db.collection('refreshTokens').drop();
87
62
  }
88
- await db.collection('user_roles').createIndex({ userId: 1 });
89
- await db.collection('user_roles').createIndex({ roleId: 1 });
90
- },
91
- down: async ({ context: db }) => {
92
- await db.collection('user_roles').drop();
93
- }
94
- });
95
- migrations.push({
96
- name: '00000000000006_schema-features',
97
- up: async ({ context: db }) => {
98
- await db.createCollection('features');
99
- if (isMultiTenant) {
100
- await db.collection('features').createIndex({ _orgId: 1, name: 1 }, { unique: true });
101
- await db.collection('features').createIndex({ _orgId: 1 });
63
+ });
64
+ if (isAuthEnabled)
65
+ migrations.push({
66
+ name: '00000000000004_schema-roles',
67
+ up: async ({ context: db }) => {
68
+ await db.createCollection('roles');
69
+ if (isMultiTenant) {
70
+ await db.collection('roles').createIndex({ _orgId: 1, name: 1 }, { unique: true });
71
+ await db.collection('roles').createIndex({ _orgId: 1 });
72
+ }
73
+ else {
74
+ await db.collection('roles').createIndex({ name: 1 }, { unique: true });
75
+ }
76
+ },
77
+ down: async ({ context: db }) => {
78
+ await db.collection('roles').drop();
102
79
  }
103
- else {
104
- await db.collection('features').createIndex({ name: 1 }, { unique: true });
80
+ });
81
+ if (isAuthEnabled)
82
+ migrations.push({
83
+ name: '00000000000005_schema-user-roles',
84
+ up: async ({ context: db }) => {
85
+ await db.createCollection('user_roles');
86
+ if (isMultiTenant) {
87
+ await db.collection('user_roles').createIndex({ _orgId: 1, userId: 1, roleId: 1 }, { unique: true });
88
+ await db.collection('user_roles').createIndex({ _orgId: 1 });
89
+ }
90
+ else {
91
+ await db.collection('user_roles').createIndex({ userId: 1, roleId: 1 }, { unique: true });
92
+ }
93
+ await db.collection('user_roles').createIndex({ userId: 1 });
94
+ await db.collection('user_roles').createIndex({ roleId: 1 });
95
+ },
96
+ down: async ({ context: db }) => {
97
+ await db.collection('user_roles').drop();
105
98
  }
106
- },
107
- down: async ({ context: db }) => {
108
- await db.collection('features').drop();
109
- }
110
- });
111
- migrations.push({
112
- name: '00000000000007_schema-authorizations',
113
- up: async ({ context: db }) => {
114
- await db.createCollection('authorizations');
115
- if (isMultiTenant) {
116
- await db.collection('authorizations').createIndex({ _orgId: 1, roleId: 1, featureId: 1 }, { unique: true });
117
- await db.collection('authorizations').createIndex({ _orgId: 1 });
99
+ });
100
+ if (isAuthEnabled)
101
+ migrations.push({
102
+ name: '00000000000006_schema-features',
103
+ up: async ({ context: db }) => {
104
+ await db.createCollection('features');
105
+ if (isMultiTenant) {
106
+ await db.collection('features').createIndex({ _orgId: 1, name: 1 }, { unique: true });
107
+ await db.collection('features').createIndex({ _orgId: 1 });
108
+ }
109
+ else {
110
+ await db.collection('features').createIndex({ name: 1 }, { unique: true });
111
+ }
112
+ },
113
+ down: async ({ context: db }) => {
114
+ await db.collection('features').drop();
118
115
  }
119
- else {
120
- await db.collection('authorizations').createIndex({ roleId: 1, featureId: 1 }, { unique: true });
116
+ });
117
+ if (isAuthEnabled)
118
+ migrations.push({
119
+ name: '00000000000007_schema-authorizations',
120
+ up: async ({ context: db }) => {
121
+ await db.createCollection('authorizations');
122
+ if (isMultiTenant) {
123
+ await db.collection('authorizations').createIndex({ _orgId: 1, roleId: 1, featureId: 1 }, { unique: true });
124
+ await db.collection('authorizations').createIndex({ _orgId: 1 });
125
+ }
126
+ else {
127
+ await db.collection('authorizations').createIndex({ roleId: 1, featureId: 1 }, { unique: true });
128
+ }
129
+ await db.collection('authorizations').createIndex({ roleId: 1 });
130
+ await db.collection('authorizations').createIndex({ featureId: 1 });
131
+ },
132
+ down: async ({ context: db }) => {
133
+ await db.collection('authorizations').drop();
121
134
  }
122
- await db.collection('authorizations').createIndex({ roleId: 1 });
123
- await db.collection('authorizations').createIndex({ featureId: 1 });
124
- },
125
- down: async ({ context: db }) => {
126
- await db.collection('authorizations').drop();
127
- }
128
- });
129
- if (isMultiTenant && config.app.metaOrgName && config.app.metaOrgCode) {
135
+ });
136
+ if (isMultiTenant) {
130
137
  migrations.push({
131
138
  name: '00000000000008_data-meta-org',
132
139
  up: async ({ context: db }) => {
133
140
  const _id = randomUUID().toString();
134
141
  const metaOrg = {
135
142
  _id,
136
- name: config.app.metaOrgName,
137
- code: config.app.metaOrgCode,
143
+ name: config.multiTenant.metaOrgName,
144
+ code: config.multiTenant.metaOrgCode,
138
145
  description: undefined,
139
146
  status: 1,
140
147
  isMetaOrg: true,
@@ -154,22 +161,20 @@ export const getMongoInitialSchema = (config) => {
154
161
  }
155
162
  });
156
163
  }
157
- if (config.auth && config.auth.adminUser) {
164
+ if (isAuthEnabled) {
158
165
  migrations.push({
159
166
  name: '00000000000009_data-admin-user',
160
167
  up: async ({ context: db }) => {
161
- if (!config.auth?.adminUser?.email || !config.auth?.adminUser?.password) {
162
- throw new Error('Admin user email and password must be provided in config');
163
- }
164
168
  const database = new MongoDBDatabase(db);
165
- const authService = new AuthService(database);
166
- if (isMultiTenant && !isSystemUserContextInitialized()) {
167
- throw new Error('SystemUserContext has not been initialized. The meta-org migration (00000000000008_data-meta-org) should have run before this migration. ' +
168
- 'This migration only runs if config.app.metaOrgName and config.app.metaOrgCode are provided. ' +
169
- 'Please ensure both values are set in your config.');
170
- }
171
- if (!isMultiTenant && !isSystemUserContextInitialized()) {
172
- initializeSystemUserContext(config.email?.systemEmailAddress || 'system@example.com', undefined);
169
+ const authService = new AuthService(database, emailClient);
170
+ if (!isSystemUserContextInitialized()) {
171
+ const errorMessage = isMultiTenant
172
+ ? 'SystemUserContext has not been initialized. The meta-org migration (00000000000008_data-meta-org) should have run before this migration. ' +
173
+ 'This migration only runs if config.app.metaOrgName and config.app.metaOrgCode are provided. ' +
174
+ 'Please ensure both values are set in your config.'
175
+ : 'BUG: SystemUserContext has not been initialized. For non-multi-tenant setups, SystemUserContext should be initialized before migrations run.';
176
+ console.error('❌ Migration Error:', errorMessage);
177
+ throw new Error(errorMessage);
173
178
  }
174
179
  const systemUserContext = getSystemUserContext();
175
180
  const _id = randomUUID().toString();
@@ -194,17 +199,14 @@ export const getMongoInitialSchema = (config) => {
194
199
  migrations.push({
195
200
  name: '00000000000010_data-admin-authorizations',
196
201
  up: async ({ context: db }) => {
197
- if (!config.auth?.adminUser?.email) {
198
- throw new Error('Admin user email not found in config');
199
- }
200
202
  const database = new MongoDBDatabase(db);
201
203
  const organizationService = new OrganizationService(database);
202
- const authService = new AuthService(database);
204
+ const authService = new AuthService(database, emailClient);
203
205
  const metaOrg = await organizationService.getMetaOrg(EmptyUserContext);
204
206
  if (!metaOrg) {
205
207
  throw new Error('Meta organization not found. Ensure meta-org migration ran successfully.');
206
208
  }
207
- const adminUser = await authService.getUserByEmail(config.auth?.adminUser?.email);
209
+ const adminUser = await authService.getUserByEmail(config.auth.adminUser.email);
208
210
  if (!adminUser) {
209
211
  throw new Error('Admin user not found. Ensure admin-user migration ran successfully.');
210
212
  }