@loomcore/api 0.1.74 → 0.1.76
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/__tests__/common-test.utils.js +4 -1
- package/dist/__tests__/postgres-test-migrations/postgres-test-schema.js +76 -1
- package/dist/controllers/auth.controller.d.ts +1 -2
- package/dist/controllers/auth.controller.js +2 -2
- package/dist/databases/migrations/migration-runner.d.ts +1 -3
- package/dist/databases/migrations/migration-runner.js +3 -5
- package/dist/databases/mongo-db/migrations/mongo-initial-schema.d.ts +1 -2
- package/dist/databases/mongo-db/migrations/mongo-initial-schema.js +4 -4
- package/dist/databases/operations/__tests__/models/client-report.model.d.ts +24 -0
- package/dist/databases/operations/__tests__/models/client-report.model.js +3 -1
- package/dist/databases/operations/__tests__/models/policy.model.d.ts +30 -0
- package/dist/databases/operations/__tests__/models/policy.model.js +9 -0
- package/dist/databases/postgres/migrations/__tests__/test-migration-helper.js +1 -2
- package/dist/databases/postgres/migrations/postgres-initial-schema.d.ts +1 -2
- package/dist/databases/postgres/migrations/postgres-initial-schema.js +4 -4
- package/dist/databases/postgres/utils/build-join-clauses.js +185 -10
- package/dist/databases/postgres/utils/build-select-clause.js +21 -1
- package/dist/databases/postgres/utils/transform-join-results.js +38 -6
- package/dist/models/base-api-config.interface.d.ts +4 -0
- package/dist/services/auth.service.d.ts +1 -2
- package/dist/services/auth.service.js +2 -2
- package/dist/services/email.service.d.ts +1 -2
- package/dist/services/email.service.js +7 -2
- package/package.json +1 -1
|
@@ -27,7 +27,7 @@ const newUser1Email = 'one@test.com';
|
|
|
27
27
|
const newUser1Password = 'testone';
|
|
28
28
|
const constDeviceIdCookie = crypto.randomBytes(16).toString('hex');
|
|
29
29
|
function initialize(database) {
|
|
30
|
-
authService = new AuthService(database
|
|
30
|
+
authService = new AuthService(database);
|
|
31
31
|
organizationService = new OrganizationService(database);
|
|
32
32
|
deviceIdCookie = constDeviceIdCookie;
|
|
33
33
|
}
|
|
@@ -210,6 +210,9 @@ export function setupTestConfig(isMultiTenant = true, dbType) {
|
|
|
210
210
|
fromAddress: 'test@test.com',
|
|
211
211
|
systemEmailAddress: 'system@test.com'
|
|
212
212
|
},
|
|
213
|
+
thirdPartyClients: {
|
|
214
|
+
emailClient: new TestEmailClient()
|
|
215
|
+
},
|
|
213
216
|
multiTenant: isMultiTenant ? {
|
|
214
217
|
metaOrgName: 'Test Meta Organization',
|
|
215
218
|
metaOrgCode: 'TEST_META_ORG'
|
|
@@ -146,7 +146,30 @@ export const getPostgresTestSchema = (config) => {
|
|
|
146
146
|
}
|
|
147
147
|
});
|
|
148
148
|
migrations.push({
|
|
149
|
-
name: '00000000000105_6_schema-
|
|
149
|
+
name: '00000000000105_6_schema-policies',
|
|
150
|
+
up: async ({ context: pool }) => {
|
|
151
|
+
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : '';
|
|
152
|
+
await pool.query(`
|
|
153
|
+
CREATE TABLE IF NOT EXISTS "policies" (
|
|
154
|
+
"_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
155
|
+
${orgColumnDef}
|
|
156
|
+
"amount" NUMERIC NOT NULL,
|
|
157
|
+
"frequency" VARCHAR NOT NULL,
|
|
158
|
+
"_created" TIMESTAMPTZ NOT NULL,
|
|
159
|
+
"_createdBy" INTEGER NOT NULL,
|
|
160
|
+
"_updated" TIMESTAMPTZ NOT NULL,
|
|
161
|
+
"_updatedBy" INTEGER NOT NULL,
|
|
162
|
+
"_deleted" TIMESTAMPTZ,
|
|
163
|
+
"_deletedBy" INTEGER
|
|
164
|
+
)
|
|
165
|
+
`);
|
|
166
|
+
},
|
|
167
|
+
down: async ({ context: pool }) => {
|
|
168
|
+
await pool.query('DROP TABLE IF EXISTS "policies"');
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
migrations.push({
|
|
172
|
+
name: '00000000000105_7_schema-clients',
|
|
150
173
|
up: async ({ context: pool }) => {
|
|
151
174
|
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : '';
|
|
152
175
|
await pool.query(`
|
|
@@ -171,6 +194,58 @@ export const getPostgresTestSchema = (config) => {
|
|
|
171
194
|
await pool.query('DROP TABLE IF EXISTS "clients"');
|
|
172
195
|
}
|
|
173
196
|
});
|
|
197
|
+
migrations.push({
|
|
198
|
+
name: '00000000000105_8_schema-clients-policies',
|
|
199
|
+
up: async ({ context: pool }) => {
|
|
200
|
+
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : '';
|
|
201
|
+
await pool.query(`
|
|
202
|
+
CREATE TABLE IF NOT EXISTS clients_policies (
|
|
203
|
+
"_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
204
|
+
${orgColumnDef}
|
|
205
|
+
"client_id" INTEGER NOT NULL,
|
|
206
|
+
"policy_id" INTEGER NOT NULL,
|
|
207
|
+
"_created" TIMESTAMPTZ NOT NULL,
|
|
208
|
+
"_createdBy" INTEGER NOT NULL,
|
|
209
|
+
"_updated" TIMESTAMPTZ NOT NULL,
|
|
210
|
+
"_updatedBy" INTEGER NOT NULL,
|
|
211
|
+
"_deleted" TIMESTAMPTZ,
|
|
212
|
+
"_deletedBy" INTEGER,
|
|
213
|
+
CONSTRAINT fk_clients_policies_client_id FOREIGN KEY ("client_id") REFERENCES clients("_id") ON DELETE CASCADE,
|
|
214
|
+
CONSTRAINT fk_clients_policies_policy_id FOREIGN KEY ("policy_id") REFERENCES policies("_id") ON DELETE CASCADE,
|
|
215
|
+
CONSTRAINT uk_clients_policies_client_policy UNIQUE ("client_id", "policy_id")
|
|
216
|
+
)
|
|
217
|
+
`);
|
|
218
|
+
},
|
|
219
|
+
down: async ({ context: pool }) => {
|
|
220
|
+
await pool.query('DROP TABLE IF EXISTS "clients_policies"');
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
migrations.push({
|
|
224
|
+
name: '00000000000105_8_schema-agents-policies',
|
|
225
|
+
up: async ({ context: pool }) => {
|
|
226
|
+
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : '';
|
|
227
|
+
await pool.query(`
|
|
228
|
+
CREATE TABLE IF NOT EXISTS agents_policies (
|
|
229
|
+
"_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
230
|
+
${orgColumnDef}
|
|
231
|
+
"policy_id" INTEGER NOT NULL,
|
|
232
|
+
"agent_id" INTEGER NOT NULL,
|
|
233
|
+
"_created" TIMESTAMPTZ NOT NULL,
|
|
234
|
+
"_createdBy" INTEGER NOT NULL,
|
|
235
|
+
"_updated" TIMESTAMPTZ NOT NULL,
|
|
236
|
+
"_updatedBy" INTEGER NOT NULL,
|
|
237
|
+
"_deleted" TIMESTAMPTZ,
|
|
238
|
+
"_deletedBy" INTEGER,
|
|
239
|
+
CONSTRAINT fk_agents_policies_policy_id FOREIGN KEY ("policy_id") REFERENCES policies("_id") ON DELETE CASCADE,
|
|
240
|
+
CONSTRAINT fk_agents_policies_agent_id FOREIGN KEY ("agent_id") REFERENCES agents("_id") ON DELETE CASCADE,
|
|
241
|
+
CONSTRAINT uk_agents_policies_policy_agent UNIQUE ("policy_id", "agent_id")
|
|
242
|
+
)
|
|
243
|
+
`);
|
|
244
|
+
},
|
|
245
|
+
down: async ({ context: pool }) => {
|
|
246
|
+
await pool.query('DROP TABLE IF EXISTS "agents_policies"');
|
|
247
|
+
}
|
|
248
|
+
});
|
|
174
249
|
migrations.push({
|
|
175
250
|
name: '00000000000106_schema-email-addresses',
|
|
176
251
|
up: async ({ context: pool }) => {
|
|
@@ -1,10 +1,9 @@
|
|
|
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';
|
|
5
4
|
export declare class AuthController {
|
|
6
5
|
authService: AuthService;
|
|
7
|
-
constructor(app: Application, database: IDatabase
|
|
6
|
+
constructor(app: Application, database: IDatabase);
|
|
8
7
|
mapRoutes(app: Application): void;
|
|
9
8
|
login(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
10
9
|
registerUser(req: Request, res: Response): Promise<void>;
|
|
@@ -6,8 +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
|
-
this.authService = new AuthService(database
|
|
9
|
+
constructor(app, database) {
|
|
10
|
+
this.authService = new AuthService(database);
|
|
11
11
|
this.mapRoutes(app);
|
|
12
12
|
}
|
|
13
13
|
mapRoutes(app) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { IBaseApiConfig } from '../../models/base-api-config.interface.js';
|
|
2
|
-
import { IEmailClient } from '../../models/email-client.interface.js';
|
|
3
2
|
export declare class MigrationRunner {
|
|
4
3
|
private config;
|
|
5
4
|
private dbType;
|
|
@@ -7,8 +6,7 @@ export declare class MigrationRunner {
|
|
|
7
6
|
private migrationsDir;
|
|
8
7
|
private primaryTimezone;
|
|
9
8
|
private dbConnection;
|
|
10
|
-
|
|
11
|
-
constructor(config: IBaseApiConfig, emailClient?: IEmailClient);
|
|
9
|
+
constructor(config: IBaseApiConfig);
|
|
12
10
|
private getTimestamp;
|
|
13
11
|
private parseSql;
|
|
14
12
|
private getMigrator;
|
|
@@ -15,15 +15,13 @@ export class MigrationRunner {
|
|
|
15
15
|
migrationsDir;
|
|
16
16
|
primaryTimezone;
|
|
17
17
|
dbConnection;
|
|
18
|
-
|
|
19
|
-
constructor(config, emailClient) {
|
|
18
|
+
constructor(config) {
|
|
20
19
|
setBaseApiConfig(config);
|
|
21
20
|
this.config = config;
|
|
22
21
|
this.dbType = config.app.dbType;
|
|
23
22
|
this.dbUrl = this.dbType === 'postgres' ? buildPostgresUrl(config) : buildMongoUrl(config);
|
|
24
23
|
this.migrationsDir = path.join(process.cwd(), 'database', 'migrations');
|
|
25
24
|
this.primaryTimezone = config.app.primaryTimezone || 'UTC';
|
|
26
|
-
this.emailClient = emailClient;
|
|
27
25
|
}
|
|
28
26
|
getTimestamp() {
|
|
29
27
|
const now = new Date();
|
|
@@ -66,7 +64,7 @@ export class MigrationRunner {
|
|
|
66
64
|
this.dbConnection = pool;
|
|
67
65
|
return new Umzug({
|
|
68
66
|
migrations: async () => {
|
|
69
|
-
const initialSchema = getPostgresInitialSchema(this.config
|
|
67
|
+
const initialSchema = getPostgresInitialSchema(this.config).map(m => ({
|
|
70
68
|
name: m.name,
|
|
71
69
|
up: async () => {
|
|
72
70
|
console.log(` Running [LIBRARY] ${m.name}...`);
|
|
@@ -105,7 +103,7 @@ export class MigrationRunner {
|
|
|
105
103
|
console.log(`🔎 Looking for migrations in: ${globPattern}`);
|
|
106
104
|
return new Umzug({
|
|
107
105
|
migrations: async () => {
|
|
108
|
-
const initialSchema = getMongoInitialSchema(this.config
|
|
106
|
+
const initialSchema = getMongoInitialSchema(this.config).map(m => ({
|
|
109
107
|
name: m.name,
|
|
110
108
|
up: async () => {
|
|
111
109
|
console.log(` Running [LIBRARY] ${m.name}...`);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Db } from 'mongodb';
|
|
2
2
|
import { IBaseApiConfig } from '../../../models/base-api-config.interface.js';
|
|
3
|
-
import { IEmailClient } from '../../../models/email-client.interface.js';
|
|
4
3
|
export interface ISyntheticMigration {
|
|
5
4
|
name: string;
|
|
6
5
|
up: (context: {
|
|
@@ -10,4 +9,4 @@ export interface ISyntheticMigration {
|
|
|
10
9
|
context: Db;
|
|
11
10
|
}) => Promise<void>;
|
|
12
11
|
}
|
|
13
|
-
export declare const getMongoInitialSchema: (config: IBaseApiConfig
|
|
12
|
+
export declare const getMongoInitialSchema: (config: IBaseApiConfig) => ISyntheticMigration[];
|
|
@@ -2,7 +2,7 @@ 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) => {
|
|
6
6
|
const migrations = [];
|
|
7
7
|
const isMultiTenant = config.app.isMultiTenant;
|
|
8
8
|
if (isMultiTenant && !config.multiTenant) {
|
|
@@ -12,7 +12,7 @@ export const getMongoInitialSchema = (config, emailClient) => {
|
|
|
12
12
|
if (isAuthEnabled && !config.auth) {
|
|
13
13
|
throw new Error('Auth enabled without auth configuration');
|
|
14
14
|
}
|
|
15
|
-
if (isAuthEnabled && (!emailClient || !config.email)) {
|
|
15
|
+
if (isAuthEnabled && (!config.thirdPartyClients?.emailClient || !config.email)) {
|
|
16
16
|
throw new Error('Auth enabled without email client or email configuration');
|
|
17
17
|
}
|
|
18
18
|
if (isMultiTenant)
|
|
@@ -166,7 +166,7 @@ export const getMongoInitialSchema = (config, emailClient) => {
|
|
|
166
166
|
name: '00000000000009_data-admin-user',
|
|
167
167
|
up: async ({ context: db }) => {
|
|
168
168
|
const database = new MongoDBDatabase(db);
|
|
169
|
-
const authService = new AuthService(database
|
|
169
|
+
const authService = new AuthService(database);
|
|
170
170
|
if (!isSystemUserContextInitialized()) {
|
|
171
171
|
const errorMessage = isMultiTenant
|
|
172
172
|
? 'SystemUserContext has not been initialized. The meta-org migration (00000000000008_data-meta-org) should have run before this migration. ' +
|
|
@@ -201,7 +201,7 @@ export const getMongoInitialSchema = (config, emailClient) => {
|
|
|
201
201
|
up: async ({ context: db }) => {
|
|
202
202
|
const database = new MongoDBDatabase(db);
|
|
203
203
|
const organizationService = new OrganizationService(database);
|
|
204
|
-
const authService = new AuthService(database
|
|
204
|
+
const authService = new AuthService(database);
|
|
205
205
|
const metaOrg = await organizationService.getMetaOrg(EmptyUserContext);
|
|
206
206
|
if (!metaOrg) {
|
|
207
207
|
throw new Error('Meta organization not found. Ensure meta-org migration ran successfully.');
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { IPersonModel } from "./person.model.js";
|
|
2
2
|
import { IAgentModel } from "./agent.model.js";
|
|
3
|
+
import { IPolicyModel } from "./policy.model.js";
|
|
3
4
|
import type { IAuditable, IEntity } from "@loomcore/common/models";
|
|
4
5
|
export interface IClientReportsModel extends IEntity, IAuditable {
|
|
5
6
|
client_person: IPersonModel;
|
|
6
7
|
agent?: IAgentModel;
|
|
8
|
+
policies?: IPolicyModel[];
|
|
7
9
|
}
|
|
8
10
|
export declare const clientReportsSchema: import("@sinclair/typebox").TObject<{
|
|
9
11
|
client_person: import("@sinclair/typebox").TObject<{
|
|
@@ -39,5 +41,27 @@ export declare const clientReportsSchema: import("@sinclair/typebox").TObject<{
|
|
|
39
41
|
}>>;
|
|
40
42
|
}>>;
|
|
41
43
|
}>>;
|
|
44
|
+
policies: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
45
|
+
amount: import("@sinclair/typebox").TNumber;
|
|
46
|
+
frequency: import("@sinclair/typebox").TString;
|
|
47
|
+
agents: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
48
|
+
person_id: import("@sinclair/typebox").TNumber;
|
|
49
|
+
agent_person: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
50
|
+
first_name: import("@sinclair/typebox").TString;
|
|
51
|
+
middle_name: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
52
|
+
last_name: import("@sinclair/typebox").TString;
|
|
53
|
+
phone_numbers: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
54
|
+
phone_number: import("@sinclair/typebox").TString;
|
|
55
|
+
phone_number_type: import("@sinclair/typebox").TString;
|
|
56
|
+
is_default: import("@sinclair/typebox").TBoolean;
|
|
57
|
+
}>>;
|
|
58
|
+
email_addresses: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
59
|
+
person_id: import("@sinclair/typebox").TNumber;
|
|
60
|
+
email_address: import("@sinclair/typebox").TString;
|
|
61
|
+
is_default: import("@sinclair/typebox").TBoolean;
|
|
62
|
+
}>>;
|
|
63
|
+
}>>;
|
|
64
|
+
}>>>;
|
|
65
|
+
}>>>;
|
|
42
66
|
}>;
|
|
43
67
|
export declare const clientReportsModelSpec: import("@loomcore/common/models").IModelSpec<import("@sinclair/typebox").TSchema>;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { personSchema } from "./person.model.js";
|
|
2
2
|
import { agentSchema } from "./agent.model.js";
|
|
3
|
+
import { policySchema } from "./policy.model.js";
|
|
3
4
|
import { entityUtils } from "@loomcore/common/utils";
|
|
4
5
|
import { Type } from "@sinclair/typebox";
|
|
5
6
|
export const clientReportsSchema = Type.Object({
|
|
6
7
|
client_person: personSchema,
|
|
7
|
-
agent: Type.Optional(agentSchema)
|
|
8
|
+
agent: Type.Optional(agentSchema),
|
|
9
|
+
policies: Type.Optional(Type.Array(policySchema))
|
|
8
10
|
});
|
|
9
11
|
export const clientReportsModelSpec = entityUtils.getModelSpec(clientReportsSchema, { isAuditable: true });
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { IAuditable, IEntity } from "@loomcore/common/models";
|
|
2
|
+
import { IAgentModel } from "./agent.model.js";
|
|
3
|
+
export interface IPolicyModel extends IEntity, IAuditable {
|
|
4
|
+
amount: number;
|
|
5
|
+
frequency: string;
|
|
6
|
+
agents?: IAgentModel[];
|
|
7
|
+
}
|
|
8
|
+
export declare const policySchema: import("@sinclair/typebox").TObject<{
|
|
9
|
+
amount: import("@sinclair/typebox").TNumber;
|
|
10
|
+
frequency: import("@sinclair/typebox").TString;
|
|
11
|
+
agents: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
12
|
+
person_id: import("@sinclair/typebox").TNumber;
|
|
13
|
+
agent_person: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
14
|
+
first_name: import("@sinclair/typebox").TString;
|
|
15
|
+
middle_name: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
16
|
+
last_name: import("@sinclair/typebox").TString;
|
|
17
|
+
phone_numbers: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
18
|
+
phone_number: import("@sinclair/typebox").TString;
|
|
19
|
+
phone_number_type: import("@sinclair/typebox").TString;
|
|
20
|
+
is_default: import("@sinclair/typebox").TBoolean;
|
|
21
|
+
}>>;
|
|
22
|
+
email_addresses: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
23
|
+
person_id: import("@sinclair/typebox").TNumber;
|
|
24
|
+
email_address: import("@sinclair/typebox").TString;
|
|
25
|
+
is_default: import("@sinclair/typebox").TBoolean;
|
|
26
|
+
}>>;
|
|
27
|
+
}>>;
|
|
28
|
+
}>>>;
|
|
29
|
+
}>;
|
|
30
|
+
export declare const policyModelSpec: import("@loomcore/common/models").IModelSpec<import("@sinclair/typebox").TSchema>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { entityUtils } from "@loomcore/common/utils";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import { agentSchema } from "./agent.model.js";
|
|
4
|
+
export const policySchema = Type.Object({
|
|
5
|
+
amount: Type.Number(),
|
|
6
|
+
frequency: Type.String(),
|
|
7
|
+
agents: Type.Optional(Type.Array(agentSchema))
|
|
8
|
+
});
|
|
9
|
+
export const policyModelSpec = entityUtils.getModelSpec(policySchema, { isAuditable: true });
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { Umzug } from 'umzug';
|
|
2
2
|
import { getPostgresInitialSchema } from '../postgres-initial-schema.js';
|
|
3
3
|
import { getPostgresTestSchema } from '../../../../__tests__/postgres-test-migrations/postgres-test-schema.js';
|
|
4
|
-
import { TestEmailClient } from '../../../../__tests__/test-email-client.js';
|
|
5
4
|
export async function runInitialSchemaMigrations(pool, config) {
|
|
6
|
-
const initialSchema = getPostgresInitialSchema(config
|
|
5
|
+
const initialSchema = getPostgresInitialSchema(config);
|
|
7
6
|
const umzug = new Umzug({
|
|
8
7
|
migrations: async () => {
|
|
9
8
|
return initialSchema.map(m => ({
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Pool } from 'pg';
|
|
2
2
|
import { IBaseApiConfig } from '../../../models/base-api-config.interface.js';
|
|
3
|
-
import { IEmailClient } from '../../../models/email-client.interface.js';
|
|
4
3
|
export interface SyntheticMigration {
|
|
5
4
|
name: string;
|
|
6
5
|
up: (context: {
|
|
@@ -10,4 +9,4 @@ export interface SyntheticMigration {
|
|
|
10
9
|
context: Pool;
|
|
11
10
|
}) => Promise<void>;
|
|
12
11
|
}
|
|
13
|
-
export declare const getPostgresInitialSchema: (config: IBaseApiConfig
|
|
12
|
+
export declare const getPostgresInitialSchema: (config: IBaseApiConfig) => SyntheticMigration[];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { initializeSystemUserContext, EmptyUserContext, getSystemUserContext, isSystemUserContextInitialized } from '@loomcore/common/models';
|
|
2
2
|
import { PostgresDatabase } from '../postgres.database.js';
|
|
3
3
|
import { AuthService, OrganizationService } from '../../../services/index.js';
|
|
4
|
-
export const getPostgresInitialSchema = (config
|
|
4
|
+
export const getPostgresInitialSchema = (config) => {
|
|
5
5
|
const migrations = [];
|
|
6
6
|
const isMultiTenant = config.app.isMultiTenant;
|
|
7
7
|
if (isMultiTenant && !config.multiTenant) {
|
|
@@ -11,7 +11,7 @@ export const getPostgresInitialSchema = (config, emailClient) => {
|
|
|
11
11
|
if (isAuthEnabled && !config.auth) {
|
|
12
12
|
throw new Error('Auth enabled without auth configuration');
|
|
13
13
|
}
|
|
14
|
-
if (isAuthEnabled && (!emailClient || !config.email)) {
|
|
14
|
+
if (isAuthEnabled && (!config.thirdPartyClients?.emailClient || !config.email)) {
|
|
15
15
|
throw new Error('Auth enabled without email client or email configuration');
|
|
16
16
|
}
|
|
17
17
|
if (isMultiTenant) {
|
|
@@ -259,7 +259,7 @@ export const getPostgresInitialSchema = (config, emailClient) => {
|
|
|
259
259
|
const client = await pool.connect();
|
|
260
260
|
try {
|
|
261
261
|
const database = new PostgresDatabase(client);
|
|
262
|
-
const authService = new AuthService(database
|
|
262
|
+
const authService = new AuthService(database);
|
|
263
263
|
if (!isSystemUserContextInitialized()) {
|
|
264
264
|
const errorMessage = isMultiTenant
|
|
265
265
|
? 'SystemUserContext has not been initialized. The meta-org migration (00000000000009_data-meta-org) should have run before this migration. ' +
|
|
@@ -301,7 +301,7 @@ export const getPostgresInitialSchema = (config, emailClient) => {
|
|
|
301
301
|
try {
|
|
302
302
|
const database = new PostgresDatabase(client);
|
|
303
303
|
const organizationService = new OrganizationService(database);
|
|
304
|
-
const authService = new AuthService(database
|
|
304
|
+
const authService = new AuthService(database);
|
|
305
305
|
const metaOrg = isMultiTenant ? await organizationService.getMetaOrg(EmptyUserContext) : undefined;
|
|
306
306
|
if (isMultiTenant && !metaOrg) {
|
|
307
307
|
throw new Error('Meta organization not found. Ensure meta-org migration ran successfully.');
|
|
@@ -5,6 +5,23 @@ import { JoinThroughMany } from "../../operations/join-through-many.operation.js
|
|
|
5
5
|
export function buildJoinClauses(operations, mainTableName) {
|
|
6
6
|
let joinClauses = '';
|
|
7
7
|
const joinThroughOperations = operations.filter(op => op instanceof JoinThrough);
|
|
8
|
+
const processedAliases = new Set();
|
|
9
|
+
const aliasesToSkip = new Set();
|
|
10
|
+
for (const operation of operations) {
|
|
11
|
+
if (operation instanceof JoinThroughMany && operation.localField.includes('.')) {
|
|
12
|
+
const [tableAlias] = operation.localField.split('.');
|
|
13
|
+
const referencedJoinThroughMany = operations
|
|
14
|
+
.filter(op => op instanceof JoinThroughMany)
|
|
15
|
+
.find(jtm => jtm.as === tableAlias);
|
|
16
|
+
if (referencedJoinThroughMany) {
|
|
17
|
+
const referencedIndex = operations.indexOf(referencedJoinThroughMany);
|
|
18
|
+
const currentIndex = operations.indexOf(operation);
|
|
19
|
+
if (referencedIndex < currentIndex) {
|
|
20
|
+
aliasesToSkip.add(tableAlias);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
8
25
|
for (const operation of operations) {
|
|
9
26
|
if (operation instanceof Join) {
|
|
10
27
|
let localFieldRef;
|
|
@@ -67,24 +84,182 @@ export function buildJoinClauses(operations, mainTableName) {
|
|
|
67
84
|
}
|
|
68
85
|
else if (operation instanceof JoinThroughMany) {
|
|
69
86
|
let localFieldRef;
|
|
87
|
+
let shouldSkipOriginalJoin = false;
|
|
70
88
|
if (operation.localField.includes('.')) {
|
|
71
89
|
const [tableAlias, columnName] = operation.localField.split('.');
|
|
72
|
-
|
|
90
|
+
const referencedJoinThroughMany = operations
|
|
91
|
+
.filter(op => op instanceof JoinThroughMany)
|
|
92
|
+
.find(jtm => jtm.as === tableAlias);
|
|
93
|
+
if (referencedJoinThroughMany) {
|
|
94
|
+
const referencedIndex = operations.indexOf(referencedJoinThroughMany);
|
|
95
|
+
const currentIndex = operations.indexOf(operation);
|
|
96
|
+
if (referencedIndex < currentIndex) {
|
|
97
|
+
shouldSkipOriginalJoin = true;
|
|
98
|
+
const originalJoinWasSkipped = aliasesToSkip.has(tableAlias);
|
|
99
|
+
const originalJoin = referencedJoinThroughMany;
|
|
100
|
+
const mainTableRef = mainTableName ? `"${mainTableName}"."_id"` : '"_id"';
|
|
101
|
+
const isAgentsJoin = operation.from === 'agents';
|
|
102
|
+
if (isAgentsJoin) {
|
|
103
|
+
if (originalJoinWasSkipped) {
|
|
104
|
+
joinClauses += ` LEFT JOIN LATERAL (
|
|
105
|
+
SELECT COALESCE(
|
|
106
|
+
JSON_AGG(
|
|
107
|
+
policy_elem.value || jsonb_build_object(
|
|
108
|
+
'agents',
|
|
109
|
+
COALESCE(
|
|
110
|
+
(SELECT JSON_AGG(agent_elem.value || jsonb_build_object('agent_person', person_data.value))
|
|
111
|
+
FROM jsonb_array_elements(COALESCE(agents_agg.agents, '[]'::json)::jsonb) AS agent_elem
|
|
112
|
+
LEFT JOIN LATERAL (
|
|
113
|
+
SELECT row_to_json(p) AS value
|
|
114
|
+
FROM "persons" AS p
|
|
115
|
+
WHERE p."_id" = (agent_elem.value->>'person_id')::integer
|
|
116
|
+
AND p."_deleted" IS NULL
|
|
117
|
+
LIMIT 1
|
|
118
|
+
) AS person_data ON true),
|
|
119
|
+
'[]'::json
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
),
|
|
123
|
+
'[]'::json
|
|
124
|
+
) AS aggregated
|
|
125
|
+
FROM (
|
|
126
|
+
SELECT COALESCE(JSON_AGG(row_to_json(${tableAlias})), '[]'::json) AS aggregated
|
|
127
|
+
FROM "${originalJoin.through}"
|
|
128
|
+
INNER JOIN "${originalJoin.from}" AS ${tableAlias}
|
|
129
|
+
ON ${tableAlias}."${originalJoin.foreignField}" = "${originalJoin.through}"."${originalJoin.throughForeignField}"
|
|
130
|
+
WHERE "${originalJoin.through}"."${originalJoin.throughLocalField}" = ${mainTableRef}
|
|
131
|
+
AND "${originalJoin.through}"."_deleted" IS NULL
|
|
132
|
+
AND ${tableAlias}."_deleted" IS NULL
|
|
133
|
+
) AS policies_subquery
|
|
134
|
+
CROSS JOIN LATERAL jsonb_array_elements(policies_subquery.aggregated::jsonb) AS policy_elem
|
|
135
|
+
LEFT JOIN LATERAL (
|
|
136
|
+
SELECT COALESCE(JSON_AGG(row_to_json(${operation.as})), '[]'::json) AS agents
|
|
137
|
+
FROM "${operation.through}"
|
|
138
|
+
INNER JOIN "${operation.from}" AS ${operation.as}
|
|
139
|
+
ON ${operation.as}."${operation.foreignField}" = "${operation.through}"."${operation.throughForeignField}"
|
|
140
|
+
WHERE "${operation.through}"."${operation.throughLocalField}" = (policy_elem.value->>'${columnName}')::integer
|
|
141
|
+
AND "${operation.through}"."_deleted" IS NULL
|
|
142
|
+
AND ${operation.as}."_deleted" IS NULL
|
|
143
|
+
) AS agents_agg ON true
|
|
144
|
+
) AS ${operation.as} ON true`;
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
joinClauses += ` LEFT JOIN LATERAL (
|
|
148
|
+
SELECT COALESCE(
|
|
149
|
+
JSON_AGG(
|
|
150
|
+
policy_elem.value || jsonb_build_object(
|
|
151
|
+
'agents',
|
|
152
|
+
COALESCE(
|
|
153
|
+
(SELECT JSON_AGG(agent_elem.value || jsonb_build_object('agent_person', person_data.value))
|
|
154
|
+
FROM jsonb_array_elements(COALESCE(agents_agg.agents, '[]'::json)::jsonb) AS agent_elem
|
|
155
|
+
LEFT JOIN LATERAL (
|
|
156
|
+
SELECT row_to_json(p) AS value
|
|
157
|
+
FROM "persons" AS p
|
|
158
|
+
WHERE p."_id" = (agent_elem.value->>'person_id')::integer
|
|
159
|
+
AND p."_deleted" IS NULL
|
|
160
|
+
LIMIT 1
|
|
161
|
+
) AS person_data ON true),
|
|
162
|
+
'[]'::json
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
),
|
|
166
|
+
'[]'::json
|
|
167
|
+
) AS aggregated
|
|
168
|
+
FROM jsonb_array_elements(COALESCE(${tableAlias}.aggregated, '[]'::json)::jsonb) AS policy_elem
|
|
169
|
+
LEFT JOIN LATERAL (
|
|
170
|
+
SELECT COALESCE(JSON_AGG(row_to_json(${operation.as})), '[]'::json) AS agents
|
|
171
|
+
FROM "${operation.through}"
|
|
172
|
+
INNER JOIN "${operation.from}" AS ${operation.as}
|
|
173
|
+
ON ${operation.as}."${operation.foreignField}" = "${operation.through}"."${operation.throughForeignField}"
|
|
174
|
+
WHERE "${operation.through}"."${operation.throughLocalField}" = (policy_elem.value->>'${columnName}')::integer
|
|
175
|
+
AND "${operation.through}"."_deleted" IS NULL
|
|
176
|
+
AND ${operation.as}."_deleted" IS NULL
|
|
177
|
+
) AS agents_agg ON true
|
|
178
|
+
) AS ${operation.as} ON true`;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
if (originalJoinWasSkipped) {
|
|
183
|
+
joinClauses += ` LEFT JOIN LATERAL (
|
|
184
|
+
SELECT COALESCE(
|
|
185
|
+
JSON_AGG(
|
|
186
|
+
policy_elem.value || jsonb_build_object('agents', COALESCE(agents_agg.agents, '[]'::json))
|
|
187
|
+
),
|
|
188
|
+
'[]'::json
|
|
189
|
+
) AS aggregated
|
|
190
|
+
FROM (
|
|
191
|
+
SELECT COALESCE(JSON_AGG(row_to_json(${tableAlias})), '[]'::json) AS aggregated
|
|
192
|
+
FROM "${originalJoin.through}"
|
|
193
|
+
INNER JOIN "${originalJoin.from}" AS ${tableAlias}
|
|
194
|
+
ON ${tableAlias}."${originalJoin.foreignField}" = "${originalJoin.through}"."${originalJoin.throughForeignField}"
|
|
195
|
+
WHERE "${originalJoin.through}"."${originalJoin.throughLocalField}" = ${mainTableRef}
|
|
196
|
+
AND "${originalJoin.through}"."_deleted" IS NULL
|
|
197
|
+
AND ${tableAlias}."_deleted" IS NULL
|
|
198
|
+
) AS policies_subquery
|
|
199
|
+
CROSS JOIN LATERAL jsonb_array_elements(policies_subquery.aggregated::jsonb) AS policy_elem
|
|
200
|
+
LEFT JOIN LATERAL (
|
|
201
|
+
SELECT COALESCE(JSON_AGG(row_to_json(${operation.as})), '[]'::json) AS agents
|
|
202
|
+
FROM "${operation.through}"
|
|
203
|
+
INNER JOIN "${operation.from}" AS ${operation.as}
|
|
204
|
+
ON ${operation.as}."${operation.foreignField}" = "${operation.through}"."${operation.throughForeignField}"
|
|
205
|
+
WHERE "${operation.through}"."${operation.throughLocalField}" = (policy_elem.value->>'${columnName}')::integer
|
|
206
|
+
AND "${operation.through}"."_deleted" IS NULL
|
|
207
|
+
AND ${operation.as}."_deleted" IS NULL
|
|
208
|
+
) AS agents_agg ON true
|
|
209
|
+
) AS ${operation.as} ON true`;
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
joinClauses += ` LEFT JOIN LATERAL (
|
|
213
|
+
SELECT COALESCE(
|
|
214
|
+
JSON_AGG(
|
|
215
|
+
policy_elem.value || jsonb_build_object('agents', COALESCE(agents_agg.agents, '[]'::json))
|
|
216
|
+
),
|
|
217
|
+
'[]'::json
|
|
218
|
+
) AS aggregated
|
|
219
|
+
FROM jsonb_array_elements(COALESCE(${tableAlias}.aggregated, '[]'::json)::jsonb) AS policy_elem
|
|
220
|
+
LEFT JOIN LATERAL (
|
|
221
|
+
SELECT COALESCE(JSON_AGG(row_to_json(${operation.as})), '[]'::json) AS agents
|
|
222
|
+
FROM "${operation.through}"
|
|
223
|
+
INNER JOIN "${operation.from}" AS ${operation.as}
|
|
224
|
+
ON ${operation.as}."${operation.foreignField}" = "${operation.through}"."${operation.throughForeignField}"
|
|
225
|
+
WHERE "${operation.through}"."${operation.throughLocalField}" = (policy_elem.value->>'${columnName}')::integer
|
|
226
|
+
AND "${operation.through}"."_deleted" IS NULL
|
|
227
|
+
AND ${operation.as}."_deleted" IS NULL
|
|
228
|
+
) AS agents_agg ON true
|
|
229
|
+
) AS ${operation.as} ON true`;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
processedAliases.add(operation.as);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
localFieldRef = `${tableAlias}."${columnName}"`;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
localFieldRef = `${tableAlias}."${columnName}"`;
|
|
241
|
+
}
|
|
73
242
|
}
|
|
74
243
|
else {
|
|
75
244
|
localFieldRef = mainTableName
|
|
76
245
|
? `"${mainTableName}"."${operation.localField}"`
|
|
77
246
|
: `"${operation.localField}"`;
|
|
78
247
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
248
|
+
if (!shouldSkipOriginalJoin && !aliasesToSkip.has(operation.as)) {
|
|
249
|
+
joinClauses += ` LEFT JOIN LATERAL (
|
|
250
|
+
SELECT COALESCE(JSON_AGG(row_to_json(${operation.as})), '[]'::json) AS aggregated
|
|
251
|
+
FROM "${operation.through}"
|
|
252
|
+
INNER JOIN "${operation.from}" AS ${operation.as}
|
|
253
|
+
ON ${operation.as}."${operation.foreignField}" = "${operation.through}"."${operation.throughForeignField}"
|
|
254
|
+
WHERE "${operation.through}"."${operation.throughLocalField}" = ${localFieldRef}
|
|
255
|
+
AND "${operation.through}"."_deleted" IS NULL
|
|
256
|
+
AND ${operation.as}."_deleted" IS NULL
|
|
257
|
+
) AS ${operation.as} ON true`;
|
|
258
|
+
processedAliases.add(operation.as);
|
|
259
|
+
}
|
|
260
|
+
else if (aliasesToSkip.has(operation.as)) {
|
|
261
|
+
processedAliases.add(operation.as);
|
|
262
|
+
}
|
|
88
263
|
}
|
|
89
264
|
}
|
|
90
265
|
return joinClauses;
|
|
@@ -35,8 +35,28 @@ export async function buildSelectClause(client, mainTableName, mainTableAlias, o
|
|
|
35
35
|
for (const joinThrough of joinThroughOperations) {
|
|
36
36
|
joinSelects.push(`${joinThrough.as}.aggregated AS "${joinThrough.as}"`);
|
|
37
37
|
}
|
|
38
|
+
const replacedJoins = new Map();
|
|
38
39
|
for (const joinThroughMany of joinThroughManyOperations) {
|
|
39
|
-
|
|
40
|
+
if (joinThroughMany.localField.includes('.')) {
|
|
41
|
+
const [tableAlias] = joinThroughMany.localField.split('.');
|
|
42
|
+
const referencedJoin = joinThroughManyOperations.find(j => j.as === tableAlias);
|
|
43
|
+
if (referencedJoin) {
|
|
44
|
+
const referencedIndex = operations.indexOf(referencedJoin);
|
|
45
|
+
const currentIndex = operations.indexOf(joinThroughMany);
|
|
46
|
+
if (referencedIndex < currentIndex) {
|
|
47
|
+
replacedJoins.set(tableAlias, joinThroughMany.as);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (const joinThroughMany of joinThroughManyOperations) {
|
|
53
|
+
if (replacedJoins.has(joinThroughMany.as)) {
|
|
54
|
+
const replacingAlias = replacedJoins.get(joinThroughMany.as);
|
|
55
|
+
joinSelects.push(`${replacingAlias}.aggregated AS "${joinThroughMany.as}"`);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
joinSelects.push(`${joinThroughMany.as}.aggregated AS "${joinThroughMany.as}"`);
|
|
59
|
+
}
|
|
40
60
|
}
|
|
41
61
|
const allSelects = [...mainSelects, ...joinSelects];
|
|
42
62
|
return allSelects.join(', ');
|
|
@@ -145,8 +145,27 @@ export function transformJoinResults(rows, operations) {
|
|
|
145
145
|
transformed[joinMany.as] = parsedValue;
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
|
+
const replacedJoins = new Map();
|
|
148
149
|
for (const joinThroughMany of joinThroughManyOperations) {
|
|
149
|
-
|
|
150
|
+
if (joinThroughMany.localField.includes('.')) {
|
|
151
|
+
const [tableAlias] = joinThroughMany.localField.split('.');
|
|
152
|
+
const referencedJoin = joinThroughManyOperations.find(j => j.as === tableAlias);
|
|
153
|
+
if (referencedJoin) {
|
|
154
|
+
const referencedIndex = operations.indexOf(referencedJoin);
|
|
155
|
+
const currentIndex = operations.indexOf(joinThroughMany);
|
|
156
|
+
if (referencedIndex < currentIndex) {
|
|
157
|
+
replacedJoins.set(tableAlias, joinThroughMany.as);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
for (const joinThroughMany of joinThroughManyOperations) {
|
|
163
|
+
if (replacedJoins.has(joinThroughMany.as)) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const originalAlias = Array.from(replacedJoins.entries()).find(([_, replacing]) => replacing === joinThroughMany.as)?.[0];
|
|
167
|
+
const aliasToUse = originalAlias || joinThroughMany.as;
|
|
168
|
+
const jsonValue = row[aliasToUse];
|
|
150
169
|
let parsedValue;
|
|
151
170
|
if (jsonValue !== null && jsonValue !== undefined) {
|
|
152
171
|
parsedValue = typeof jsonValue === 'string'
|
|
@@ -160,16 +179,29 @@ export function transformJoinResults(rows, operations) {
|
|
|
160
179
|
const [tableAlias] = joinThroughMany.localField.split('.');
|
|
161
180
|
const relatedJoin = joinOperations.find(j => j.as === tableAlias);
|
|
162
181
|
const relatedJoinThrough = joinThroughOperations.find(j => j.as === tableAlias);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
transformed[
|
|
182
|
+
const relatedJoinThroughMany = joinThroughManyOperations.find(j => j.as === tableAlias);
|
|
183
|
+
if ((relatedJoin && transformed[relatedJoin.as]) ||
|
|
184
|
+
(relatedJoinThrough && transformed[relatedJoinThrough.as]) ||
|
|
185
|
+
(relatedJoinThroughMany && transformed[relatedJoinThroughMany.as])) {
|
|
186
|
+
const targetAlias = relatedJoin ? relatedJoin.as :
|
|
187
|
+
(relatedJoinThrough ? relatedJoinThrough.as : relatedJoinThroughMany.as);
|
|
188
|
+
if (replacedJoins.get(targetAlias) === joinThroughMany.as) {
|
|
189
|
+
transformed[targetAlias] = parsedValue;
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
let fieldName = joinThroughMany.as;
|
|
193
|
+
if (fieldName === 'policy_agents' && targetAlias === 'policies') {
|
|
194
|
+
fieldName = 'agents';
|
|
195
|
+
}
|
|
196
|
+
transformed[targetAlias][fieldName] = parsedValue;
|
|
197
|
+
}
|
|
166
198
|
}
|
|
167
199
|
else {
|
|
168
|
-
transformed[
|
|
200
|
+
transformed[aliasToUse] = parsedValue;
|
|
169
201
|
}
|
|
170
202
|
}
|
|
171
203
|
else {
|
|
172
|
-
transformed[
|
|
204
|
+
transformed[aliasToUse] = parsedValue;
|
|
173
205
|
}
|
|
174
206
|
}
|
|
175
207
|
return transformed;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DbType } from "../databases/db-type.type.js";
|
|
2
2
|
import { IAuthConfig } from "./auth-config.interface.js";
|
|
3
3
|
import { IEmailConfig } from "./email-config.interface.js";
|
|
4
|
+
import { IEmailClient } from "./email-client.interface.js";
|
|
4
5
|
import { IMultiTenantConfig } from "./multi-tenant-config.interface.js";
|
|
5
6
|
export interface IBaseApiConfig {
|
|
6
7
|
app: {
|
|
@@ -30,4 +31,7 @@ export interface IBaseApiConfig {
|
|
|
30
31
|
hostName: string;
|
|
31
32
|
internalPort?: number;
|
|
32
33
|
};
|
|
34
|
+
thirdPartyClients?: {
|
|
35
|
+
emailClient?: IEmailClient;
|
|
36
|
+
};
|
|
33
37
|
}
|
|
@@ -5,14 +5,13 @@ import { MultiTenantApiService } from './multi-tenant-api.service.js';
|
|
|
5
5
|
import { UpdateResult } from '../databases/models/update-result.js';
|
|
6
6
|
import { IRefreshToken } from '../models/refresh-token.model.js';
|
|
7
7
|
import { IDatabase } from '../databases/models/index.js';
|
|
8
|
-
import { IEmailClient } from '../models/email-client.interface.js';
|
|
9
8
|
export declare class AuthService extends MultiTenantApiService<IUser> {
|
|
10
9
|
private refreshTokenService;
|
|
11
10
|
private passwordResetTokenService;
|
|
12
11
|
private emailService;
|
|
13
12
|
private organizationService;
|
|
14
13
|
private authConfig;
|
|
15
|
-
constructor(database: IDatabase
|
|
14
|
+
constructor(database: IDatabase);
|
|
16
15
|
attemptLogin(req: Request, res: Response, email: string, password: string): Promise<ILoginResponse | null>;
|
|
17
16
|
logUserIn(userContext: IUserContext, deviceId: string): Promise<{
|
|
18
17
|
tokens: {
|
|
@@ -18,11 +18,11 @@ export class AuthService extends MultiTenantApiService {
|
|
|
18
18
|
emailService;
|
|
19
19
|
organizationService;
|
|
20
20
|
authConfig;
|
|
21
|
-
constructor(database
|
|
21
|
+
constructor(database) {
|
|
22
22
|
super(database, 'users', 'user', UserSpec);
|
|
23
23
|
this.refreshTokenService = new GenericApiService(database, 'refresh_tokens', 'refresh_token', refreshTokenModelSpec);
|
|
24
24
|
this.passwordResetTokenService = new PasswordResetTokenService(database);
|
|
25
|
-
this.emailService = new EmailService(
|
|
25
|
+
this.emailService = new EmailService();
|
|
26
26
|
this.organizationService = new OrganizationService(database);
|
|
27
27
|
if (!config.auth) {
|
|
28
28
|
throw new ServerError('Auth configuration is not set');
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { IEmailClient } from '../models/email-client.interface.js';
|
|
2
1
|
export declare class EmailService {
|
|
3
2
|
private emailConfig;
|
|
4
3
|
private emailClient;
|
|
5
|
-
constructor(
|
|
4
|
+
constructor();
|
|
6
5
|
sendHtmlEmail(emailAddress: string, subject: string, body: string): Promise<void>;
|
|
7
6
|
}
|
|
@@ -3,14 +3,19 @@ import { config } from '../config/index.js';
|
|
|
3
3
|
export class EmailService {
|
|
4
4
|
emailConfig;
|
|
5
5
|
emailClient;
|
|
6
|
-
constructor(
|
|
6
|
+
constructor() {
|
|
7
7
|
if (config.email) {
|
|
8
8
|
this.emailConfig = config.email;
|
|
9
9
|
}
|
|
10
10
|
else {
|
|
11
11
|
throw new ServerError('Email configuration is not available. Email API credentials are not set in the config.');
|
|
12
12
|
}
|
|
13
|
-
|
|
13
|
+
if (config.thirdPartyClients?.emailClient) {
|
|
14
|
+
this.emailClient = config.thirdPartyClients.emailClient;
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
throw new ServerError('Email client is not available. Email client is not set in the config.');
|
|
18
|
+
}
|
|
14
19
|
}
|
|
15
20
|
async sendHtmlEmail(emailAddress, subject, body) {
|
|
16
21
|
const messageData = {
|