@loomcore/api 0.1.60 → 0.1.63
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/LICENSE +201 -201
- package/README.md +49 -49
- package/dist/__tests__/common-test.utils.js +10 -8
- package/dist/__tests__/postgres-test-migrations/postgres-test-schema.js +52 -52
- package/dist/__tests__/postgres.test-database.js +8 -8
- package/dist/__tests__/test-email-client.d.ts +4 -0
- package/dist/__tests__/test-email-client.js +6 -0
- package/dist/controllers/auth.controller.d.ts +2 -1
- package/dist/controllers/auth.controller.js +2 -3
- package/dist/databases/migrations/migration-runner.d.ts +3 -1
- package/dist/databases/migrations/migration-runner.js +27 -25
- package/dist/databases/mongo-db/migrations/mongo-initial-schema.d.ts +3 -2
- package/dist/databases/mongo-db/migrations/mongo-initial-schema.js +119 -110
- package/dist/databases/postgres/commands/postgres-batch-update.command.js +7 -7
- package/dist/databases/postgres/commands/postgres-create-many.command.js +4 -4
- package/dist/databases/postgres/commands/postgres-create.command.js +4 -4
- package/dist/databases/postgres/commands/postgres-full-update-by-id.command.js +13 -13
- package/dist/databases/postgres/commands/postgres-partial-update-by-id.command.js +7 -7
- package/dist/databases/postgres/commands/postgres-update.command.js +7 -7
- package/dist/databases/postgres/migrations/__tests__/test-migration-helper.js +2 -1
- package/dist/databases/postgres/migrations/postgres-initial-schema.d.ts +2 -1
- package/dist/databases/postgres/migrations/postgres-initial-schema.js +276 -266
- package/dist/databases/postgres/postgres.database.js +17 -17
- package/dist/databases/postgres/utils/build-select-clause.js +6 -6
- package/dist/databases/postgres/utils/does-table-exist.util.js +4 -4
- package/dist/models/base-api-config.interface.d.ts +7 -10
- package/dist/models/email-client.interface.d.ts +3 -0
- package/dist/models/email-client.interface.js +1 -0
- package/dist/models/email-config.interface.d.ts +4 -0
- package/dist/models/email-config.interface.js +1 -0
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.js +1 -0
- package/dist/models/multi-tenant-config.interface.d.ts +4 -0
- package/dist/models/multi-tenant-config.interface.js +1 -0
- package/dist/services/auth.service.d.ts +2 -1
- package/dist/services/auth.service.js +2 -2
- package/dist/services/email.service.d.ts +5 -4
- package/dist/services/email.service.js +12 -20
- 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`);
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
+
emailClient;
|
|
19
|
+
constructor(config, emailClient) {
|
|
19
20
|
setBaseApiConfig(config);
|
|
20
21
|
this.config = config;
|
|
21
|
-
this.dbType = config.app.dbType
|
|
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
|
-
|
|
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) =>
|
|
13
|
+
export declare const getMongoInitialSchema: (config: IBaseApiConfig, emailClient?: IEmailClient) => ISyntheticMigration[];
|
|
@@ -2,10 +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
|
|
8
|
-
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)
|
|
9
19
|
migrations.push({
|
|
10
20
|
name: '00000000000001_schema-organizations',
|
|
11
21
|
up: async ({ context: db }) => {
|
|
@@ -18,115 +28,120 @@ export const getMongoInitialSchema = (config) => {
|
|
|
18
28
|
await db.collection('organizations').drop();
|
|
19
29
|
}
|
|
20
30
|
});
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
migrations.push({
|
|
39
|
-
name: '00000000000003_schema-refresh-tokens',
|
|
40
|
-
up: async ({ context: db }) => {
|
|
41
|
-
await db.createCollection('refreshTokens');
|
|
42
|
-
await db.collection('refreshTokens').createIndex({ token: 1 }, { unique: true });
|
|
43
|
-
await db.collection('refreshTokens').createIndex({ userId: 1 });
|
|
44
|
-
await db.collection('refreshTokens').createIndex({ deviceId: 1 });
|
|
45
|
-
if (isMultiTenant) {
|
|
46
|
-
await db.collection('refreshTokens').createIndex({ _orgId: 1 });
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
down: async ({ context: db }) => {
|
|
50
|
-
await db.collection('refreshTokens').drop();
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
migrations.push({
|
|
54
|
-
name: '00000000000004_schema-roles',
|
|
55
|
-
up: async ({ context: db }) => {
|
|
56
|
-
await db.createCollection('roles');
|
|
57
|
-
if (isMultiTenant) {
|
|
58
|
-
await db.collection('roles').createIndex({ _orgId: 1, name: 1 }, { unique: true });
|
|
59
|
-
await db.collection('roles').createIndex({ _orgId: 1 });
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
await db.collection('roles').createIndex({ name: 1 }, { unique: true });
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
down: async ({ context: db }) => {
|
|
66
|
-
await db.collection('roles').drop();
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
migrations.push({
|
|
70
|
-
name: '00000000000005_schema-user-roles',
|
|
71
|
-
up: async ({ context: db }) => {
|
|
72
|
-
await db.createCollection('user_roles');
|
|
73
|
-
if (isMultiTenant) {
|
|
74
|
-
await db.collection('user_roles').createIndex({ _orgId: 1, userId: 1, roleId: 1 }, { unique: true });
|
|
75
|
-
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();
|
|
76
46
|
}
|
|
77
|
-
|
|
78
|
-
|
|
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();
|
|
79
62
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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();
|
|
94
79
|
}
|
|
95
|
-
|
|
96
|
-
|
|
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();
|
|
97
98
|
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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();
|
|
110
115
|
}
|
|
111
|
-
|
|
112
|
-
|
|
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();
|
|
113
134
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
},
|
|
117
|
-
down: async ({ context: db }) => {
|
|
118
|
-
await db.collection('authorizations').drop();
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
if (isMultiTenant && config.app.metaOrgName && config.app.metaOrgCode) {
|
|
135
|
+
});
|
|
136
|
+
if (isMultiTenant) {
|
|
122
137
|
migrations.push({
|
|
123
138
|
name: '00000000000008_data-meta-org',
|
|
124
139
|
up: async ({ context: db }) => {
|
|
125
140
|
const _id = randomUUID().toString();
|
|
126
141
|
const metaOrg = {
|
|
127
142
|
_id,
|
|
128
|
-
name: config.
|
|
129
|
-
code: config.
|
|
143
|
+
name: config.multiTenant.metaOrgName,
|
|
144
|
+
code: config.multiTenant.metaOrgCode,
|
|
130
145
|
description: undefined,
|
|
131
146
|
status: 1,
|
|
132
147
|
isMetaOrg: true,
|
|
@@ -146,15 +161,12 @@ export const getMongoInitialSchema = (config) => {
|
|
|
146
161
|
}
|
|
147
162
|
});
|
|
148
163
|
}
|
|
149
|
-
if (
|
|
164
|
+
if (isAuthEnabled) {
|
|
150
165
|
migrations.push({
|
|
151
166
|
name: '00000000000009_data-admin-user',
|
|
152
167
|
up: async ({ context: db }) => {
|
|
153
|
-
if (!config.auth?.adminUser?.email || !config.auth?.adminUser?.password) {
|
|
154
|
-
throw new Error('Admin user email and password must be provided in config');
|
|
155
|
-
}
|
|
156
168
|
const database = new MongoDBDatabase(db);
|
|
157
|
-
const authService = new AuthService(database);
|
|
169
|
+
const authService = new AuthService(database, emailClient);
|
|
158
170
|
if (!isSystemUserContextInitialized()) {
|
|
159
171
|
const errorMessage = isMultiTenant
|
|
160
172
|
? 'SystemUserContext has not been initialized. The meta-org migration (00000000000008_data-meta-org) should have run before this migration. ' +
|
|
@@ -187,17 +199,14 @@ export const getMongoInitialSchema = (config) => {
|
|
|
187
199
|
migrations.push({
|
|
188
200
|
name: '00000000000010_data-admin-authorizations',
|
|
189
201
|
up: async ({ context: db }) => {
|
|
190
|
-
if (!config.auth?.adminUser?.email) {
|
|
191
|
-
throw new Error('Admin user email not found in config');
|
|
192
|
-
}
|
|
193
202
|
const database = new MongoDBDatabase(db);
|
|
194
203
|
const organizationService = new OrganizationService(database);
|
|
195
|
-
const authService = new AuthService(database);
|
|
204
|
+
const authService = new AuthService(database, emailClient);
|
|
196
205
|
const metaOrg = await organizationService.getMetaOrg(EmptyUserContext);
|
|
197
206
|
if (!metaOrg) {
|
|
198
207
|
throw new Error('Meta organization not found. Ensure meta-org migration ran successfully.');
|
|
199
208
|
}
|
|
200
|
-
const adminUser = await authService.getUserByEmail(config.auth
|
|
209
|
+
const adminUser = await authService.getUserByEmail(config.auth.adminUser.email);
|
|
201
210
|
if (!adminUser) {
|
|
202
211
|
throw new Error('Admin user not found. Ensure admin-user migration ran successfully.');
|
|
203
212
|
}
|
|
@@ -26,10 +26,10 @@ export async function batchUpdate(client, entities, operations, queryObject, plu
|
|
|
26
26
|
const setClause = columns.map((col, index) => `${col} = $${index + 1}`).join(', ');
|
|
27
27
|
queryObject.filters._id = { eq: _id };
|
|
28
28
|
const { whereClause } = buildWhereClause(queryObject, values);
|
|
29
|
-
const query = `
|
|
30
|
-
UPDATE "${pluralResourceName}"
|
|
31
|
-
SET ${setClause}
|
|
32
|
-
${whereClause}
|
|
29
|
+
const query = `
|
|
30
|
+
UPDATE "${pluralResourceName}"
|
|
31
|
+
SET ${setClause}
|
|
32
|
+
${whereClause}
|
|
33
33
|
`;
|
|
34
34
|
await client.query(query, values);
|
|
35
35
|
}
|
|
@@ -39,9 +39,9 @@ export async function batchUpdate(client, entities, operations, queryObject, plu
|
|
|
39
39
|
const tablePrefix = hasJoins ? pluralResourceName : undefined;
|
|
40
40
|
queryObject.filters._id = { in: entityIds };
|
|
41
41
|
const { whereClause, values } = buildWhereClause(queryObject, [], tablePrefix);
|
|
42
|
-
const selectQuery = `
|
|
43
|
-
SELECT * FROM "${pluralResourceName}" ${joinClauses}
|
|
44
|
-
${whereClause}
|
|
42
|
+
const selectQuery = `
|
|
43
|
+
SELECT * FROM "${pluralResourceName}" ${joinClauses}
|
|
44
|
+
${whereClause}
|
|
45
45
|
`;
|
|
46
46
|
const result = await client.query(selectQuery, values);
|
|
47
47
|
return result.rows;
|
|
@@ -33,10 +33,10 @@ export async function createMany(client, pluralResourceName, entities) {
|
|
|
33
33
|
valueClauses.push(`(${placeholders})`);
|
|
34
34
|
allValues.push(...values);
|
|
35
35
|
});
|
|
36
|
-
const query = `
|
|
37
|
-
INSERT INTO "${pluralResourceName}" (${columns.map(col => `"${col}"`).join(', ')})
|
|
38
|
-
VALUES ${valueClauses.join(', ')}
|
|
39
|
-
RETURNING *
|
|
36
|
+
const query = `
|
|
37
|
+
INSERT INTO "${pluralResourceName}" (${columns.map(col => `"${col}"`).join(', ')})
|
|
38
|
+
VALUES ${valueClauses.join(', ')}
|
|
39
|
+
RETURNING *
|
|
40
40
|
`;
|
|
41
41
|
const result = await client.query(query, allValues);
|
|
42
42
|
if (result.rows.length !== entities.length) {
|