@loomcore/api 0.2.11 → 0.2.13
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 +16 -3
- package/dist/__tests__/postgres-test-migrations/postgres-test-schema.d.ts +2 -2
- package/dist/__tests__/postgres-test-migrations/postgres-test-schema.js +95 -54
- package/dist/__tests__/test-objects.d.ts +4 -2
- package/dist/__tests__/test-objects.js +17 -7
- package/dist/controllers/auth.controller.js +3 -9
- package/dist/databases/mongo-db/migrations/mongo-initial-schema.js +34 -41
- package/dist/databases/postgres/migrations/__tests__/test-migration-helper.js +1 -1
- package/dist/databases/postgres/migrations/postgres-initial-schema.js +49 -72
- package/dist/models/initial-database-config.interface.d.ts +1 -1
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/organization-domain.service.d.ts +6 -0
- package/dist/services/organization-domain.service.js +7 -0
- package/dist/services/organization.service.d.ts +2 -0
- package/dist/services/organization.service.js +14 -0
- package/package.json +2 -2
|
@@ -11,8 +11,9 @@ import { PostgresDatabase } from "../databases/postgres/postgres.database.js";
|
|
|
11
11
|
import { GenericApiService, UserService } from "../services/index.js";
|
|
12
12
|
import { MultiTenantApiService } from "../services/multi-tenant-api.service.js";
|
|
13
13
|
import { OrganizationService } from "../services/organization.service.js";
|
|
14
|
+
import { OrganizationDomainService } from "../services/organization-domain.service.js";
|
|
14
15
|
import * as testObjectsModule from "./test-objects.js";
|
|
15
|
-
const { getTestMetaOrg, getTestMetaOrgRefererUrl, getTestOrg, getTestMetaOrgUser, getTestMetaOrgUserContext, getTestOrgUser, getTestOrgUserContext, setTestOrgId, setTestMetaOrgId, setTestMetaOrgUserId, setTestOrgUserId, } = testObjectsModule;
|
|
16
|
+
const { getTestMetaOrg, getTestMetaOrgDomain, getTestMetaOrgRefererUrl, getTestOrg, getTestOrgDomain, getTestMetaOrgUser, getTestMetaOrgUserContext, getTestOrgUser, getTestOrgUserContext, setTestOrgId, setTestMetaOrgId, setTestMetaOrgUserId, setTestOrgUserId, TEST_META_ORG_DOMAIN, TEST_ORG_DOMAIN, } = testObjectsModule;
|
|
16
17
|
import { entityUtils } from "@loomcore/common/utils";
|
|
17
18
|
import { config, setBaseApiConfig } from "../config/index.js";
|
|
18
19
|
import { attemptLogin } from "../utils/auth/index.js";
|
|
@@ -24,6 +25,7 @@ import { TestEmailClient } from "./test-email-client.js";
|
|
|
24
25
|
let deviceIdCookie;
|
|
25
26
|
let database;
|
|
26
27
|
let organizationService;
|
|
28
|
+
let organizationDomainService;
|
|
27
29
|
let userService;
|
|
28
30
|
const JWT_SECRET = "test-secret";
|
|
29
31
|
const newUser1Email = "one@test.com";
|
|
@@ -32,6 +34,7 @@ const constDeviceIdCookie = crypto.randomBytes(16).toString("hex");
|
|
|
32
34
|
function initialize(db) {
|
|
33
35
|
database = db;
|
|
34
36
|
organizationService = new OrganizationService(db);
|
|
37
|
+
organizationDomainService = new OrganizationDomainService(db);
|
|
35
38
|
userService = new UserService(db);
|
|
36
39
|
deviceIdCookie = constDeviceIdCookie;
|
|
37
40
|
}
|
|
@@ -51,7 +54,7 @@ async function createMetaOrg() {
|
|
|
51
54
|
if (!config.app.isMultiTenant) {
|
|
52
55
|
return;
|
|
53
56
|
}
|
|
54
|
-
if (!organizationService) {
|
|
57
|
+
if (!organizationService || !organizationDomainService) {
|
|
55
58
|
throw new Error("OrganizationService not initialized. Call initialize() first.");
|
|
56
59
|
}
|
|
57
60
|
try {
|
|
@@ -60,10 +63,15 @@ async function createMetaOrg() {
|
|
|
60
63
|
const metaOrgInsertResult = await organizationService.create(EmptyUserContext, getTestMetaOrg());
|
|
61
64
|
if (metaOrgInsertResult) {
|
|
62
65
|
setTestMetaOrgId(metaOrgInsertResult._id);
|
|
66
|
+
await organizationDomainService.create(EmptyUserContext, getTestMetaOrgDomain(metaOrgInsertResult._id));
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
else {
|
|
66
70
|
setTestMetaOrgId(existingMetaOrg._id);
|
|
71
|
+
const existingDomain = await organizationDomainService.findOne(EmptyUserContext, { filters: { domain: { eq: TEST_META_ORG_DOMAIN } } });
|
|
72
|
+
if (!existingDomain) {
|
|
73
|
+
await organizationDomainService.create(EmptyUserContext, getTestMetaOrgDomain(existingMetaOrg._id));
|
|
74
|
+
}
|
|
67
75
|
}
|
|
68
76
|
}
|
|
69
77
|
catch (error) {
|
|
@@ -96,7 +104,7 @@ async function setupTestUsers() {
|
|
|
96
104
|
}
|
|
97
105
|
}
|
|
98
106
|
async function createTestUsers() {
|
|
99
|
-
if (!organizationService || !userService) {
|
|
107
|
+
if (!organizationService || !organizationDomainService || !userService) {
|
|
100
108
|
throw new Error("Database not initialized. Call initialize() first.");
|
|
101
109
|
}
|
|
102
110
|
try {
|
|
@@ -112,9 +120,14 @@ async function createTestUsers() {
|
|
|
112
120
|
throw new Error("Failed to create test organization");
|
|
113
121
|
}
|
|
114
122
|
setTestOrgId(createdTestOrg._id);
|
|
123
|
+
await organizationDomainService.create(EmptyUserContext, getTestOrgDomain(createdTestOrg._id));
|
|
115
124
|
}
|
|
116
125
|
else {
|
|
117
126
|
setTestOrgId(existingTestOrg._id);
|
|
127
|
+
const existingDomain = await organizationDomainService.findOne(EmptyUserContext, { filters: { domain: { eq: TEST_ORG_DOMAIN } } });
|
|
128
|
+
if (!existingDomain) {
|
|
129
|
+
await organizationDomainService.create(EmptyUserContext, getTestOrgDomain(existingTestOrg._id));
|
|
130
|
+
}
|
|
118
131
|
}
|
|
119
132
|
const createdTestOrgUser = await userService.create(getTestOrgUserContext(), getTestOrgUser());
|
|
120
133
|
const createdMetaOrgUser = await userService.create(getTestMetaOrgUserContext(), getTestMetaOrgUser());
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { SyntheticMigration } from "../../databases/postgres/migrations/postgres-initial-schema.js";
|
|
2
|
+
import type { IBaseApiConfig } from "../../models/base-api-config.interface.js";
|
|
3
3
|
export declare const getPostgresTestSchema: (config: IBaseApiConfig) => SyntheticMigration[];
|
|
@@ -2,9 +2,9 @@ export const getPostgresTestSchema = (config) => {
|
|
|
2
2
|
const migrations = [];
|
|
3
3
|
const isMultiTenant = config.app.isMultiTenant === true;
|
|
4
4
|
migrations.push({
|
|
5
|
-
name:
|
|
5
|
+
name: "00000000000100_schema-test-entities",
|
|
6
6
|
up: async ({ context: pool }) => {
|
|
7
|
-
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' :
|
|
7
|
+
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : "";
|
|
8
8
|
await pool.query(`
|
|
9
9
|
CREATE TABLE IF NOT EXISTS "testEntities" (
|
|
10
10
|
"_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
@@ -25,12 +25,12 @@ export const getPostgresTestSchema = (config) => {
|
|
|
25
25
|
},
|
|
26
26
|
down: async ({ context: pool }) => {
|
|
27
27
|
await pool.query('DROP TABLE IF EXISTS "testEntities"');
|
|
28
|
-
}
|
|
28
|
+
},
|
|
29
29
|
});
|
|
30
30
|
migrations.push({
|
|
31
|
-
name:
|
|
31
|
+
name: "00000000000101_schema-categories",
|
|
32
32
|
up: async ({ context: pool }) => {
|
|
33
|
-
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' :
|
|
33
|
+
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : "";
|
|
34
34
|
await pool.query(`
|
|
35
35
|
CREATE TABLE IF NOT EXISTS "categories" (
|
|
36
36
|
"_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
@@ -41,12 +41,12 @@ export const getPostgresTestSchema = (config) => {
|
|
|
41
41
|
},
|
|
42
42
|
down: async ({ context: pool }) => {
|
|
43
43
|
await pool.query('DROP TABLE IF EXISTS "categories"');
|
|
44
|
-
}
|
|
44
|
+
},
|
|
45
45
|
});
|
|
46
46
|
migrations.push({
|
|
47
|
-
name:
|
|
47
|
+
name: "00000000000102_schema-products",
|
|
48
48
|
up: async ({ context: pool }) => {
|
|
49
|
-
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' :
|
|
49
|
+
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : "";
|
|
50
50
|
await pool.query(`
|
|
51
51
|
CREATE TABLE IF NOT EXISTS "products" (
|
|
52
52
|
"_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
@@ -67,12 +67,12 @@ export const getPostgresTestSchema = (config) => {
|
|
|
67
67
|
},
|
|
68
68
|
down: async ({ context: pool }) => {
|
|
69
69
|
await pool.query('DROP TABLE IF EXISTS "products"');
|
|
70
|
-
}
|
|
70
|
+
},
|
|
71
71
|
});
|
|
72
72
|
migrations.push({
|
|
73
|
-
name:
|
|
73
|
+
name: "00000000000103_schema-test-items",
|
|
74
74
|
up: async ({ context: pool }) => {
|
|
75
|
-
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' :
|
|
75
|
+
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : "";
|
|
76
76
|
await pool.query(`
|
|
77
77
|
CREATE TABLE IF NOT EXISTS "testItems" (
|
|
78
78
|
"_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
@@ -91,12 +91,53 @@ export const getPostgresTestSchema = (config) => {
|
|
|
91
91
|
},
|
|
92
92
|
down: async ({ context: pool }) => {
|
|
93
93
|
await pool.query('DROP TABLE IF EXISTS "testItems"');
|
|
94
|
-
}
|
|
94
|
+
},
|
|
95
95
|
});
|
|
96
96
|
migrations.push({
|
|
97
|
-
name:
|
|
97
|
+
name: "00000000000104_schema-persons",
|
|
98
98
|
up: async ({ context: pool }) => {
|
|
99
|
-
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' :
|
|
99
|
+
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER NOT NULL,' : "";
|
|
100
|
+
const personsUniqueConstraints = isMultiTenant
|
|
101
|
+
? `CONSTRAINT "uk_persons_org_external_id" UNIQUE ("_orgId", "external_id"),
|
|
102
|
+
CONSTRAINT "uk_persons_org_ssn" UNIQUE ("_orgId", "ssn")`
|
|
103
|
+
: `CONSTRAINT "uk_persons_external_id" UNIQUE ("external_id"),
|
|
104
|
+
CONSTRAINT "uk_persons_ssn" UNIQUE ("ssn")`;
|
|
105
|
+
await pool.query(`
|
|
106
|
+
CREATE TABLE IF NOT EXISTS "persons" (
|
|
107
|
+
"_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
108
|
+
${orgColumnDef}
|
|
109
|
+
"external_id" VARCHAR(255),
|
|
110
|
+
"first_name" VARCHAR(255) NOT NULL,
|
|
111
|
+
"middle_name" VARCHAR(255),
|
|
112
|
+
"last_name" VARCHAR(255) NOT NULL,
|
|
113
|
+
"date_of_birth" DATE,
|
|
114
|
+
"ssn" VARCHAR(255),
|
|
115
|
+
"is_agent" BOOLEAN NOT NULL DEFAULT FALSE,
|
|
116
|
+
"is_client" BOOLEAN NOT NULL DEFAULT FALSE,
|
|
117
|
+
"is_employee" BOOLEAN NOT NULL DEFAULT FALSE,
|
|
118
|
+
"extended_types" INTEGER,
|
|
119
|
+
"_created" TIMESTAMPTZ NOT NULL,
|
|
120
|
+
"_createdBy" INTEGER NOT NULL,
|
|
121
|
+
"_updated" TIMESTAMPTZ,
|
|
122
|
+
"_updatedBy" INTEGER,
|
|
123
|
+
"_deleted" TIMESTAMPTZ,
|
|
124
|
+
"_deletedBy" INTEGER,
|
|
125
|
+
${personsUniqueConstraints}
|
|
126
|
+
)
|
|
127
|
+
`);
|
|
128
|
+
await pool.query(`CREATE INDEX IF NOT EXISTS "idx_persons_external_id" ON "persons" ("external_id")`);
|
|
129
|
+
await pool.query(`CREATE INDEX IF NOT EXISTS "idx_persons_ssn" ON "persons" ("ssn")`);
|
|
130
|
+
},
|
|
131
|
+
down: async ({ context: pool }) => {
|
|
132
|
+
await pool.query(`DROP INDEX IF EXISTS "idx_persons_external_id"`);
|
|
133
|
+
await pool.query(`DROP INDEX IF EXISTS "idx_persons_ssn"`);
|
|
134
|
+
await pool.query('DROP TABLE IF EXISTS "persons"');
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
migrations.push({
|
|
138
|
+
name: "00000000000105_5_schema-agents",
|
|
139
|
+
up: async ({ context: pool }) => {
|
|
140
|
+
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : "";
|
|
100
141
|
await pool.query(`
|
|
101
142
|
CREATE TABLE IF NOT EXISTS "agents" (
|
|
102
143
|
"_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
@@ -114,12 +155,12 @@ export const getPostgresTestSchema = (config) => {
|
|
|
114
155
|
},
|
|
115
156
|
down: async ({ context: pool }) => {
|
|
116
157
|
await pool.query('DROP TABLE IF EXISTS "agents"');
|
|
117
|
-
}
|
|
158
|
+
},
|
|
118
159
|
});
|
|
119
160
|
migrations.push({
|
|
120
|
-
name:
|
|
161
|
+
name: "00000000000105_6_schema-clients",
|
|
121
162
|
up: async ({ context: pool }) => {
|
|
122
|
-
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' :
|
|
163
|
+
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : "";
|
|
123
164
|
await pool.query(`
|
|
124
165
|
CREATE TABLE IF NOT EXISTS "clients" (
|
|
125
166
|
"_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
@@ -140,12 +181,12 @@ export const getPostgresTestSchema = (config) => {
|
|
|
140
181
|
},
|
|
141
182
|
down: async ({ context: pool }) => {
|
|
142
183
|
await pool.query('DROP TABLE IF EXISTS "clients"');
|
|
143
|
-
}
|
|
184
|
+
},
|
|
144
185
|
});
|
|
145
186
|
migrations.push({
|
|
146
|
-
name:
|
|
187
|
+
name: "00000000000105_7_schema-policies",
|
|
147
188
|
up: async ({ context: pool }) => {
|
|
148
|
-
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' :
|
|
189
|
+
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : "";
|
|
149
190
|
await pool.query(`
|
|
150
191
|
CREATE TABLE IF NOT EXISTS "policies" (
|
|
151
192
|
"_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
@@ -165,12 +206,12 @@ export const getPostgresTestSchema = (config) => {
|
|
|
165
206
|
},
|
|
166
207
|
down: async ({ context: pool }) => {
|
|
167
208
|
await pool.query('DROP TABLE IF EXISTS "policies"');
|
|
168
|
-
}
|
|
209
|
+
},
|
|
169
210
|
});
|
|
170
211
|
migrations.push({
|
|
171
|
-
name:
|
|
212
|
+
name: "00000000000105_8_schema-agents-policies",
|
|
172
213
|
up: async ({ context: pool }) => {
|
|
173
|
-
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' :
|
|
214
|
+
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : "";
|
|
174
215
|
await pool.query(`
|
|
175
216
|
CREATE TABLE IF NOT EXISTS agents_policies (
|
|
176
217
|
"_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
@@ -191,12 +232,12 @@ export const getPostgresTestSchema = (config) => {
|
|
|
191
232
|
},
|
|
192
233
|
down: async ({ context: pool }) => {
|
|
193
234
|
await pool.query('DROP TABLE IF EXISTS "agents_policies"');
|
|
194
|
-
}
|
|
235
|
+
},
|
|
195
236
|
});
|
|
196
237
|
migrations.push({
|
|
197
|
-
name:
|
|
238
|
+
name: "00000000000105_9_schema-premiums",
|
|
198
239
|
up: async ({ context: pool }) => {
|
|
199
|
-
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' :
|
|
240
|
+
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : "";
|
|
200
241
|
await pool.query(`
|
|
201
242
|
CREATE TABLE IF NOT EXISTS "premiums" (
|
|
202
243
|
"_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
@@ -216,12 +257,12 @@ export const getPostgresTestSchema = (config) => {
|
|
|
216
257
|
},
|
|
217
258
|
down: async ({ context: pool }) => {
|
|
218
259
|
await pool.query('DROP TABLE IF EXISTS "premiums"');
|
|
219
|
-
}
|
|
260
|
+
},
|
|
220
261
|
});
|
|
221
262
|
migrations.push({
|
|
222
|
-
name:
|
|
263
|
+
name: "00000000000106_schema-email-addresses",
|
|
223
264
|
up: async ({ context: pool }) => {
|
|
224
|
-
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' :
|
|
265
|
+
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : "";
|
|
225
266
|
await pool.query(`
|
|
226
267
|
CREATE TABLE IF NOT EXISTS email_addresses (
|
|
227
268
|
"_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
@@ -243,12 +284,12 @@ export const getPostgresTestSchema = (config) => {
|
|
|
243
284
|
},
|
|
244
285
|
down: async ({ context: pool }) => {
|
|
245
286
|
await pool.query('DROP TABLE IF EXISTS "email_addresses"');
|
|
246
|
-
}
|
|
287
|
+
},
|
|
247
288
|
});
|
|
248
289
|
migrations.push({
|
|
249
|
-
name:
|
|
290
|
+
name: "00000000000107_schema-phone-numbers",
|
|
250
291
|
up: async ({ context: pool }) => {
|
|
251
|
-
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' :
|
|
292
|
+
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : "";
|
|
252
293
|
await pool.query(`
|
|
253
294
|
CREATE TABLE IF NOT EXISTS phone_numbers (
|
|
254
295
|
"_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
@@ -268,12 +309,12 @@ export const getPostgresTestSchema = (config) => {
|
|
|
268
309
|
},
|
|
269
310
|
down: async ({ context: pool }) => {
|
|
270
311
|
await pool.query('DROP TABLE IF EXISTS "phone_numbers"');
|
|
271
|
-
}
|
|
312
|
+
},
|
|
272
313
|
});
|
|
273
314
|
migrations.push({
|
|
274
|
-
name:
|
|
315
|
+
name: "00000000000108_schema-addresses",
|
|
275
316
|
up: async ({ context: pool }) => {
|
|
276
|
-
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' :
|
|
317
|
+
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : "";
|
|
277
318
|
await pool.query(`
|
|
278
319
|
CREATE TABLE IF NOT EXISTS addresses (
|
|
279
320
|
"_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
@@ -297,12 +338,12 @@ export const getPostgresTestSchema = (config) => {
|
|
|
297
338
|
},
|
|
298
339
|
down: async ({ context: pool }) => {
|
|
299
340
|
await pool.query('DROP TABLE IF EXISTS "addresses"');
|
|
300
|
-
}
|
|
341
|
+
},
|
|
301
342
|
});
|
|
302
343
|
migrations.push({
|
|
303
|
-
name:
|
|
344
|
+
name: "00000000000109_schema-person-addresses",
|
|
304
345
|
up: async ({ context: pool }) => {
|
|
305
|
-
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' :
|
|
346
|
+
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : "";
|
|
306
347
|
await pool.query(`
|
|
307
348
|
CREATE TABLE IF NOT EXISTS persons_addresses (
|
|
308
349
|
"_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
@@ -323,12 +364,12 @@ export const getPostgresTestSchema = (config) => {
|
|
|
323
364
|
},
|
|
324
365
|
down: async ({ context: pool }) => {
|
|
325
366
|
await pool.query('DROP TABLE IF EXISTS "persons_addresses"');
|
|
326
|
-
}
|
|
367
|
+
},
|
|
327
368
|
});
|
|
328
369
|
migrations.push({
|
|
329
|
-
name:
|
|
370
|
+
name: "00000000000110_schema-person-phone-numbers",
|
|
330
371
|
up: async ({ context: pool }) => {
|
|
331
|
-
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' :
|
|
372
|
+
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : "";
|
|
332
373
|
await pool.query(`
|
|
333
374
|
CREATE TABLE IF NOT EXISTS persons_phone_numbers (
|
|
334
375
|
"_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
@@ -349,12 +390,12 @@ export const getPostgresTestSchema = (config) => {
|
|
|
349
390
|
},
|
|
350
391
|
down: async ({ context: pool }) => {
|
|
351
392
|
await pool.query('DROP TABLE IF EXISTS "persons_phone_numbers"');
|
|
352
|
-
}
|
|
393
|
+
},
|
|
353
394
|
});
|
|
354
395
|
migrations.push({
|
|
355
|
-
name:
|
|
396
|
+
name: "00000000000111_schema-states",
|
|
356
397
|
up: async ({ context: pool }) => {
|
|
357
|
-
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' :
|
|
398
|
+
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : "";
|
|
358
399
|
await pool.query(`
|
|
359
400
|
CREATE TABLE IF NOT EXISTS states (
|
|
360
401
|
"_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
@@ -371,12 +412,12 @@ export const getPostgresTestSchema = (config) => {
|
|
|
371
412
|
},
|
|
372
413
|
down: async ({ context: pool }) => {
|
|
373
414
|
await pool.query('DROP TABLE IF EXISTS "states"');
|
|
374
|
-
}
|
|
415
|
+
},
|
|
375
416
|
});
|
|
376
417
|
migrations.push({
|
|
377
|
-
name:
|
|
418
|
+
name: "00000000000112_schema-districts",
|
|
378
419
|
up: async ({ context: pool }) => {
|
|
379
|
-
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' :
|
|
420
|
+
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : "";
|
|
380
421
|
await pool.query(`
|
|
381
422
|
CREATE TABLE IF NOT EXISTS districts (
|
|
382
423
|
"_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
@@ -395,12 +436,12 @@ export const getPostgresTestSchema = (config) => {
|
|
|
395
436
|
},
|
|
396
437
|
down: async ({ context: pool }) => {
|
|
397
438
|
await pool.query('DROP TABLE IF EXISTS "districts"');
|
|
398
|
-
}
|
|
439
|
+
},
|
|
399
440
|
});
|
|
400
441
|
migrations.push({
|
|
401
|
-
name:
|
|
442
|
+
name: "00000000000113_schema-schools",
|
|
402
443
|
up: async ({ context: pool }) => {
|
|
403
|
-
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' :
|
|
444
|
+
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : "";
|
|
404
445
|
await pool.query(`
|
|
405
446
|
CREATE TABLE IF NOT EXISTS schools (
|
|
406
447
|
"_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
@@ -419,12 +460,12 @@ export const getPostgresTestSchema = (config) => {
|
|
|
419
460
|
},
|
|
420
461
|
down: async ({ context: pool }) => {
|
|
421
462
|
await pool.query('DROP TABLE IF EXISTS "schools"');
|
|
422
|
-
}
|
|
463
|
+
},
|
|
423
464
|
});
|
|
424
465
|
migrations.push({
|
|
425
|
-
name:
|
|
466
|
+
name: "00000000000114_schema-person-schools",
|
|
426
467
|
up: async ({ context: pool }) => {
|
|
427
|
-
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' :
|
|
468
|
+
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : "";
|
|
428
469
|
await pool.query(`
|
|
429
470
|
CREATE TABLE IF NOT EXISTS persons_schools (
|
|
430
471
|
"_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
@@ -445,7 +486,7 @@ export const getPostgresTestSchema = (config) => {
|
|
|
445
486
|
},
|
|
446
487
|
down: async ({ context: pool }) => {
|
|
447
488
|
await pool.query('DROP TABLE IF EXISTS "persons_schools"');
|
|
448
|
-
}
|
|
489
|
+
},
|
|
449
490
|
});
|
|
450
491
|
return migrations;
|
|
451
492
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IOrganization, IUser, IUserContext } from "@loomcore/common/models";
|
|
1
|
+
import type { IOrganization, IOrganizationDomain, IUser, IUserContext } from "@loomcore/common/models";
|
|
2
2
|
export declare let TEST_META_ORG_ID: string | number;
|
|
3
3
|
export declare let TEST_META_ORG_USER_ID: string | number;
|
|
4
4
|
export declare function setTestMetaOrgId(metaOrgId: string | number): void;
|
|
@@ -6,15 +6,17 @@ export declare function setTestMetaOrgUserId(userId: string | number): void;
|
|
|
6
6
|
export declare const TEST_META_ORG_USER_PASSWORD = "test-meta-org-user-password";
|
|
7
7
|
export declare const TEST_META_ORG_DOMAIN = "test-meta-org.example.com";
|
|
8
8
|
export declare const TEST_ORG_DOMAIN = "test-org.example.com";
|
|
9
|
-
export declare function getRefererUrlForOrg(
|
|
9
|
+
export declare function getRefererUrlForOrg(domain: string): string;
|
|
10
10
|
export declare function getTestMetaOrgRefererUrl(): string;
|
|
11
11
|
export declare function getTestOrgRefererUrl(): string;
|
|
12
12
|
export declare function getTestMetaOrg(): IOrganization;
|
|
13
|
+
export declare function getTestMetaOrgDomain(organizationId?: string | number): Partial<IOrganizationDomain>;
|
|
13
14
|
export declare function getTestMetaOrgUser(): IUser;
|
|
14
15
|
export declare function getTestMetaOrgUserContext(): IUserContext;
|
|
15
16
|
export declare function setTestOrgId(orgId: string | number): void;
|
|
16
17
|
export declare function setTestOrgUserId(userId: string | number): void;
|
|
17
18
|
export declare function getTestOrg(): IOrganization;
|
|
19
|
+
export declare function getTestOrgDomain(organizationId?: string | number): Partial<IOrganizationDomain>;
|
|
18
20
|
export declare const TEST_ORG_USER_PASSWORD = "test-org-user-password";
|
|
19
21
|
export declare function getTestOrgUser(): IUser;
|
|
20
22
|
export declare function getTestOrgUserContext(): IUserContext;
|
|
@@ -9,30 +9,35 @@ export function setTestMetaOrgUserId(userId) {
|
|
|
9
9
|
export const TEST_META_ORG_USER_PASSWORD = "test-meta-org-user-password";
|
|
10
10
|
export const TEST_META_ORG_DOMAIN = "test-meta-org.example.com";
|
|
11
11
|
export const TEST_ORG_DOMAIN = "test-org.example.com";
|
|
12
|
-
export function getRefererUrlForOrg(
|
|
13
|
-
if (!
|
|
12
|
+
export function getRefererUrlForOrg(domain) {
|
|
13
|
+
if (!domain) {
|
|
14
14
|
throw new Error("Organization domain is required for auth referer URL");
|
|
15
15
|
}
|
|
16
|
-
return `https://${
|
|
16
|
+
return `https://${domain}`;
|
|
17
17
|
}
|
|
18
18
|
export function getTestMetaOrgRefererUrl() {
|
|
19
|
-
return getRefererUrlForOrg(
|
|
19
|
+
return getRefererUrlForOrg(TEST_META_ORG_DOMAIN);
|
|
20
20
|
}
|
|
21
21
|
export function getTestOrgRefererUrl() {
|
|
22
|
-
return getRefererUrlForOrg(
|
|
22
|
+
return getRefererUrlForOrg(TEST_ORG_DOMAIN);
|
|
23
23
|
}
|
|
24
24
|
export function getTestMetaOrg() {
|
|
25
25
|
return {
|
|
26
26
|
_id: TEST_META_ORG_ID,
|
|
27
27
|
name: "Test Meta Organization",
|
|
28
28
|
code: "test-meta-org",
|
|
29
|
-
domain: TEST_META_ORG_DOMAIN,
|
|
30
29
|
status: 1,
|
|
31
30
|
isMetaOrg: true,
|
|
32
31
|
_created: new Date(),
|
|
33
32
|
_createdBy: "system",
|
|
34
33
|
};
|
|
35
34
|
}
|
|
35
|
+
export function getTestMetaOrgDomain(organizationId = TEST_META_ORG_ID) {
|
|
36
|
+
return {
|
|
37
|
+
organizationId,
|
|
38
|
+
domain: TEST_META_ORG_DOMAIN,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
36
41
|
export function getTestMetaOrgUser() {
|
|
37
42
|
return {
|
|
38
43
|
_id: TEST_META_ORG_USER_ID,
|
|
@@ -73,13 +78,18 @@ export function getTestOrg() {
|
|
|
73
78
|
_id: TEST_ORG_ID,
|
|
74
79
|
name: "Test Organization",
|
|
75
80
|
code: "test-org",
|
|
76
|
-
domain: TEST_ORG_DOMAIN,
|
|
77
81
|
status: 1,
|
|
78
82
|
isMetaOrg: false,
|
|
79
83
|
_created: new Date(),
|
|
80
84
|
_createdBy: "system",
|
|
81
85
|
};
|
|
82
86
|
}
|
|
87
|
+
export function getTestOrgDomain(organizationId = TEST_ORG_ID) {
|
|
88
|
+
return {
|
|
89
|
+
organizationId,
|
|
90
|
+
domain: TEST_ORG_DOMAIN,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
83
93
|
export const TEST_ORG_USER_PASSWORD = "test-org-user-password";
|
|
84
94
|
export function getTestOrgUser() {
|
|
85
95
|
return {
|
|
@@ -38,9 +38,7 @@ export class AuthController {
|
|
|
38
38
|
if (!referer) {
|
|
39
39
|
throw new BadRequestError("Missing required fields: referer is required.");
|
|
40
40
|
}
|
|
41
|
-
organization = await this.organizationService.
|
|
42
|
-
filters: { domain: { eq: referer.split("/")[2] } },
|
|
43
|
-
});
|
|
41
|
+
organization = await this.organizationService.findByDomain(EmptyUserContext, referer.split("/")[2]);
|
|
44
42
|
if (!organization) {
|
|
45
43
|
throw new BadRequestError("Missing required fields: organization is required.");
|
|
46
44
|
}
|
|
@@ -96,9 +94,7 @@ export class AuthController {
|
|
|
96
94
|
referer = referer.replace(/\/$/, "");
|
|
97
95
|
let organization = null;
|
|
98
96
|
if (config.app.isMultiTenant) {
|
|
99
|
-
organization = await this.organizationService.
|
|
100
|
-
filters: { domain: { eq: referer.split("/")[2] } },
|
|
101
|
-
});
|
|
97
|
+
organization = await this.organizationService.findByDomain(EmptyUserContext, referer.split("/")[2]);
|
|
102
98
|
if (!organization) {
|
|
103
99
|
throw new BadRequestError("Missing required fields: organization is required.");
|
|
104
100
|
}
|
|
@@ -126,9 +122,7 @@ export class AuthController {
|
|
|
126
122
|
if (!referer) {
|
|
127
123
|
throw new BadRequestError("Missing required fields: referer is required.");
|
|
128
124
|
}
|
|
129
|
-
organization = await this.organizationService.
|
|
130
|
-
filters: { domain: { eq: referer.split("/")[2] } },
|
|
131
|
-
});
|
|
125
|
+
organization = await this.organizationService.findByDomain(EmptyUserContext, referer.split("/")[2]);
|
|
132
126
|
if (!organization) {
|
|
133
127
|
throw new BadRequestError("Missing required fields: organization is required.");
|
|
134
128
|
}
|
|
@@ -9,7 +9,7 @@ export const getMongoInitialSchema = (dbConfig) => {
|
|
|
9
9
|
throw new Error("Multi-tenant configuration is enabled but multi-tenant configuration is not provided");
|
|
10
10
|
}
|
|
11
11
|
const isAuthEnabled = dbConfig.app.isAuthEnabled;
|
|
12
|
-
if (isMultiTenant)
|
|
12
|
+
if (isMultiTenant) {
|
|
13
13
|
migrations.push({
|
|
14
14
|
name: "00000000000001_schema-organizations",
|
|
15
15
|
up: async ({ context: db }) => {
|
|
@@ -26,30 +26,25 @@ export const getMongoInitialSchema = (dbConfig) => {
|
|
|
26
26
|
await db.collection("organizations").drop();
|
|
27
27
|
},
|
|
28
28
|
});
|
|
29
|
-
if (isAuthEnabled)
|
|
30
29
|
migrations.push({
|
|
31
|
-
name: "
|
|
30
|
+
name: "00000000000001_schema-organization-domains",
|
|
32
31
|
up: async ({ context: db }) => {
|
|
33
|
-
await db.createCollection("
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
else {
|
|
41
|
-
await db
|
|
42
|
-
.collection("persons")
|
|
43
|
-
.createIndex({ externalId: 1 }, { unique: true, sparse: true });
|
|
44
|
-
}
|
|
32
|
+
await db.createCollection("organization_domains");
|
|
33
|
+
await db
|
|
34
|
+
.collection("organization_domains")
|
|
35
|
+
.createIndex({ domain: 1 }, { unique: true });
|
|
36
|
+
await db
|
|
37
|
+
.collection("organization_domains")
|
|
38
|
+
.createIndex({ organizationId: 1 });
|
|
45
39
|
},
|
|
46
40
|
down: async ({ context: db }) => {
|
|
47
|
-
await db.collection("
|
|
41
|
+
await db.collection("organization_domains").drop();
|
|
48
42
|
},
|
|
49
43
|
});
|
|
44
|
+
}
|
|
50
45
|
if (isAuthEnabled)
|
|
51
46
|
migrations.push({
|
|
52
|
-
name: "
|
|
47
|
+
name: "00000000000002_schema-users",
|
|
53
48
|
up: async ({ context: db }) => {
|
|
54
49
|
await db.createCollection("users");
|
|
55
50
|
if (dbConfig.app.isMultiTenant) {
|
|
@@ -70,7 +65,7 @@ export const getMongoInitialSchema = (dbConfig) => {
|
|
|
70
65
|
});
|
|
71
66
|
if (isAuthEnabled)
|
|
72
67
|
migrations.push({
|
|
73
|
-
name: "
|
|
68
|
+
name: "00000000000003_schema-refresh-tokens",
|
|
74
69
|
up: async ({ context: db }) => {
|
|
75
70
|
await db.createCollection("refresh_tokens");
|
|
76
71
|
await db
|
|
@@ -88,7 +83,7 @@ export const getMongoInitialSchema = (dbConfig) => {
|
|
|
88
83
|
});
|
|
89
84
|
if (isAuthEnabled)
|
|
90
85
|
migrations.push({
|
|
91
|
-
name: "
|
|
86
|
+
name: "00000000000004_schema-password-reset-tokens",
|
|
92
87
|
up: async ({ context: db }) => {
|
|
93
88
|
await db.createCollection("password_reset_tokens");
|
|
94
89
|
if (isMultiTenant) {
|
|
@@ -111,7 +106,7 @@ export const getMongoInitialSchema = (dbConfig) => {
|
|
|
111
106
|
});
|
|
112
107
|
if (isAuthEnabled)
|
|
113
108
|
migrations.push({
|
|
114
|
-
name: "
|
|
109
|
+
name: "00000000000005_schema-roles",
|
|
115
110
|
up: async ({ context: db }) => {
|
|
116
111
|
await db.createCollection("roles");
|
|
117
112
|
if (isMultiTenant) {
|
|
@@ -132,7 +127,7 @@ export const getMongoInitialSchema = (dbConfig) => {
|
|
|
132
127
|
});
|
|
133
128
|
if (isAuthEnabled)
|
|
134
129
|
migrations.push({
|
|
135
|
-
name: "
|
|
130
|
+
name: "00000000000006_schema-user-roles",
|
|
136
131
|
up: async ({ context: db }) => {
|
|
137
132
|
await db.createCollection("user_roles");
|
|
138
133
|
if (isMultiTenant) {
|
|
@@ -155,7 +150,7 @@ export const getMongoInitialSchema = (dbConfig) => {
|
|
|
155
150
|
});
|
|
156
151
|
if (isAuthEnabled)
|
|
157
152
|
migrations.push({
|
|
158
|
-
name: "
|
|
153
|
+
name: "00000000000007_schema-features",
|
|
159
154
|
up: async ({ context: db }) => {
|
|
160
155
|
await db.createCollection("features");
|
|
161
156
|
if (isMultiTenant) {
|
|
@@ -176,7 +171,7 @@ export const getMongoInitialSchema = (dbConfig) => {
|
|
|
176
171
|
});
|
|
177
172
|
if (isAuthEnabled)
|
|
178
173
|
migrations.push({
|
|
179
|
-
name: "
|
|
174
|
+
name: "00000000000008_schema-authorizations",
|
|
180
175
|
up: async ({ context: db }) => {
|
|
181
176
|
await db.createCollection("authorizations");
|
|
182
177
|
if (isMultiTenant) {
|
|
@@ -199,7 +194,7 @@ export const getMongoInitialSchema = (dbConfig) => {
|
|
|
199
194
|
});
|
|
200
195
|
if (isMultiTenant) {
|
|
201
196
|
migrations.push({
|
|
202
|
-
name: "
|
|
197
|
+
name: "00000000000009_data-meta-org",
|
|
203
198
|
up: async ({ context: db }) => {
|
|
204
199
|
const metaOrgDoc = {
|
|
205
200
|
name: dbConfig.multiTenant.metaOrgName,
|
|
@@ -222,20 +217,32 @@ export const getMongoInitialSchema = (dbConfig) => {
|
|
|
222
217
|
...metaOrgDoc,
|
|
223
218
|
_id: result.insertedId,
|
|
224
219
|
};
|
|
220
|
+
const metaOrgDomains = dbConfig.multiTenant.metaOrgDomains ?? [];
|
|
221
|
+
if (metaOrgDomains.length > 0) {
|
|
222
|
+
await db.collection("organization_domains").insertMany(metaOrgDomains.map((domain) => ({
|
|
223
|
+
organizationId: metaOrg._id,
|
|
224
|
+
domain,
|
|
225
|
+
_created: new Date(),
|
|
226
|
+
_createdBy: "system",
|
|
227
|
+
_updated: new Date(),
|
|
228
|
+
_updatedBy: "system",
|
|
229
|
+
})));
|
|
230
|
+
}
|
|
225
231
|
initializeSystemUserContext(dbConfig.email?.systemEmailAddress || "system@example.com", metaOrg);
|
|
226
232
|
},
|
|
227
233
|
down: async ({ context: db }) => {
|
|
234
|
+
await db.collection("organization_domains").deleteMany({});
|
|
228
235
|
await db.collection("organizations").deleteMany({ isMetaOrg: true });
|
|
229
236
|
},
|
|
230
237
|
});
|
|
231
238
|
}
|
|
232
239
|
if (isAuthEnabled && dbConfig.adminUser) {
|
|
233
240
|
migrations.push({
|
|
234
|
-
name: "
|
|
241
|
+
name: "00000000000010_data-admin-user",
|
|
235
242
|
up: async ({ context: db }) => {
|
|
236
243
|
if (!isSystemUserContextInitialized()) {
|
|
237
244
|
const errorMessage = isMultiTenant
|
|
238
|
-
? "SystemUserContext has not been initialized. The meta-org migration (
|
|
245
|
+
? "SystemUserContext has not been initialized. The meta-org migration (00000000000009_data-meta-org) should have run before this migration. " +
|
|
239
246
|
"Please ensure metaOrgName and metaOrgCode are provided in your dbConfig."
|
|
240
247
|
: "BUG: SystemUserContext has not been initialized. For non-multi-tenant setups, SystemUserContext should be initialized before migrations run.";
|
|
241
248
|
console.error("❌ Migration Error:", errorMessage);
|
|
@@ -247,26 +254,12 @@ export const getMongoInitialSchema = (dbConfig) => {
|
|
|
247
254
|
: {};
|
|
248
255
|
const hashedPassword = await passwordUtils.hashPassword(dbConfig.adminUser.password);
|
|
249
256
|
const email = dbConfig.adminUser.email.toLowerCase();
|
|
250
|
-
const personResult = await db.collection("persons").insertOne({
|
|
251
|
-
...orgDoc,
|
|
252
|
-
externalId: "admin-user-person-external-id",
|
|
253
|
-
firstName: "Admin",
|
|
254
|
-
lastName: "User",
|
|
255
|
-
isAgent: false,
|
|
256
|
-
isClient: false,
|
|
257
|
-
isEmployee: false,
|
|
258
|
-
_created: new Date(),
|
|
259
|
-
_createdBy: "system",
|
|
260
|
-
_updated: new Date(),
|
|
261
|
-
_updatedBy: "system",
|
|
262
|
-
});
|
|
263
257
|
await db.collection("users").insertOne({
|
|
264
258
|
...orgDoc,
|
|
265
259
|
externalId: "admin-user-external-id",
|
|
266
260
|
email,
|
|
267
261
|
password: hashedPassword,
|
|
268
262
|
displayName: "Admin User",
|
|
269
|
-
personId: personResult.insertedId,
|
|
270
263
|
_created: new Date(),
|
|
271
264
|
_createdBy: "system",
|
|
272
265
|
_updated: new Date(),
|
|
@@ -284,7 +277,7 @@ export const getMongoInitialSchema = (dbConfig) => {
|
|
|
284
277
|
}
|
|
285
278
|
if (isAuthEnabled && dbConfig.adminUser) {
|
|
286
279
|
migrations.push({
|
|
287
|
-
name: "
|
|
280
|
+
name: "00000000000011_data-admin-authorizations",
|
|
288
281
|
up: async ({ context: db }) => {
|
|
289
282
|
const database = new MongoDBDatabase(db);
|
|
290
283
|
const organizationService = new OrganizationService(database);
|
|
@@ -8,7 +8,7 @@ export async function runInitialSchemaMigrations(pool, config) {
|
|
|
8
8
|
app: config.app,
|
|
9
9
|
database: config.database,
|
|
10
10
|
adminUser: config.adminUser ?? { email: 'admin@test.com', password: 'admin-password' },
|
|
11
|
-
multiTenant: config.multiTenant ?? (config.app.isMultiTenant ? { metaOrgName: 'Test Meta Organization', metaOrgCode: 'TEST_META_ORG',
|
|
11
|
+
multiTenant: config.multiTenant ?? (config.app.isMultiTenant ? { metaOrgName: 'Test Meta Organization', metaOrgCode: 'TEST_META_ORG', metaOrgDomains: [TEST_META_ORG_DOMAIN] } : { metaOrgName: '', metaOrgCode: '', metaOrgDomains: [] }),
|
|
12
12
|
email: config.email,
|
|
13
13
|
};
|
|
14
14
|
const initialSchema = getPostgresInitialSchema(migrationConfig);
|
|
@@ -34,7 +34,6 @@ export const getPostgresInitialSchema = (dbConfig) => {
|
|
|
34
34
|
"_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
35
35
|
"name" VARCHAR(255) NOT NULL UNIQUE,
|
|
36
36
|
"code" VARCHAR(255) UNIQUE,
|
|
37
|
-
"domain" VARCHAR(255) UNIQUE,
|
|
38
37
|
"description" TEXT,
|
|
39
38
|
"status" INTEGER NOT NULL,
|
|
40
39
|
"is_meta_org" BOOLEAN NOT NULL,
|
|
@@ -52,67 +51,48 @@ export const getPostgresInitialSchema = (dbConfig) => {
|
|
|
52
51
|
await pool.query('DROP TABLE IF EXISTS "organizations"');
|
|
53
52
|
},
|
|
54
53
|
});
|
|
55
|
-
}
|
|
56
|
-
if (isAuthEnabled)
|
|
57
54
|
migrations.push({
|
|
58
|
-
name: "
|
|
55
|
+
name: "00000000000002_schema-organization-domains",
|
|
59
56
|
up: async ({ context: pool }) => {
|
|
60
|
-
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER NOT NULL,' : "";
|
|
61
|
-
const personsUniqueConstraints = isMultiTenant
|
|
62
|
-
? `CONSTRAINT "uk_persons_org_external_id" UNIQUE ("_orgId", "external_id"),
|
|
63
|
-
CONSTRAINT "uk_persons_org_ssn" UNIQUE ("_orgId", "ssn")`
|
|
64
|
-
: `CONSTRAINT "uk_persons_external_id" UNIQUE ("external_id"),
|
|
65
|
-
CONSTRAINT "uk_persons_ssn" UNIQUE ("ssn")`;
|
|
66
57
|
await pool.query(`
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"_updated" TIMESTAMPTZ,
|
|
83
|
-
"_updatedBy" INTEGER,
|
|
84
|
-
"_deleted" TIMESTAMPTZ,
|
|
85
|
-
"_deletedBy" INTEGER,
|
|
86
|
-
${personsUniqueConstraints}
|
|
87
|
-
)`);
|
|
88
|
-
await pool.query(`CREATE INDEX IF NOT EXISTS "idx_persons_external_id" ON "persons" ("external_id")`);
|
|
89
|
-
await pool.query(`CREATE INDEX IF NOT EXISTS "idx_persons_ssn" ON "persons" ("ssn")`);
|
|
58
|
+
CREATE TABLE IF NOT EXISTS "organization_domains" (
|
|
59
|
+
"_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
60
|
+
"organization_id" INTEGER NOT NULL,
|
|
61
|
+
"domain" VARCHAR(255) NOT NULL UNIQUE,
|
|
62
|
+
"_created" TIMESTAMPTZ NOT NULL,
|
|
63
|
+
"_createdBy" INTEGER NOT NULL,
|
|
64
|
+
"_updated" TIMESTAMPTZ,
|
|
65
|
+
"_updatedBy" INTEGER,
|
|
66
|
+
"_deleted" TIMESTAMPTZ,
|
|
67
|
+
"_deletedBy" INTEGER,
|
|
68
|
+
CONSTRAINT "fk_organization_domains_organization"
|
|
69
|
+
FOREIGN KEY("organization_id") REFERENCES "organizations"("_id") ON DELETE CASCADE
|
|
70
|
+
)
|
|
71
|
+
`);
|
|
72
|
+
await pool.query(`CREATE INDEX IF NOT EXISTS "idx_organization_domains_organization_id" ON "organization_domains"("organization_id")`);
|
|
90
73
|
},
|
|
91
74
|
down: async ({ context: pool }) => {
|
|
92
|
-
await pool.query(`DROP INDEX IF EXISTS "
|
|
93
|
-
await pool.query(
|
|
94
|
-
await pool.query(`DROP TABLE IF EXISTS "persons"`);
|
|
75
|
+
await pool.query(`DROP INDEX IF EXISTS "idx_organization_domains_organization_id"`);
|
|
76
|
+
await pool.query('DROP TABLE IF EXISTS "organization_domains"');
|
|
95
77
|
},
|
|
96
78
|
});
|
|
79
|
+
}
|
|
97
80
|
if (isAuthEnabled)
|
|
98
81
|
migrations.push({
|
|
99
|
-
name: "
|
|
82
|
+
name: "00000000000003_schema-users",
|
|
100
83
|
up: async ({ context: pool }) => {
|
|
101
84
|
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER NOT NULL,' : "";
|
|
102
|
-
|
|
85
|
+
const uniqueConstraint = isMultiTenant
|
|
103
86
|
? 'CONSTRAINT "uk_users_email" UNIQUE ("_orgId", "email")'
|
|
104
87
|
: 'CONSTRAINT "uk_users_email" UNIQUE ("email")';
|
|
105
|
-
uniqueConstraint += `,
|
|
106
|
-
CONSTRAINT "fk_users_person_id" FOREIGN KEY("person_id") REFERENCES "persons"("_id") ON DELETE CASCADE`;
|
|
107
88
|
await pool.query(`
|
|
108
89
|
CREATE TABLE IF NOT EXISTS "users"(
|
|
109
90
|
"_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
110
91
|
${orgColumnDef}
|
|
111
|
-
|
|
92
|
+
"external_id" VARCHAR(255) UNIQUE,
|
|
112
93
|
"email" VARCHAR(255) NOT NULL,
|
|
113
94
|
"display_name" VARCHAR(255),
|
|
114
95
|
"password" VARCHAR(255) NOT NULL,
|
|
115
|
-
"person_id" INTEGER UNIQUE,
|
|
116
96
|
"_lastLoggedIn" TIMESTAMPTZ,
|
|
117
97
|
"_lastPasswordChange" TIMESTAMPTZ,
|
|
118
98
|
"_created" TIMESTAMPTZ NOT NULL,
|
|
@@ -125,18 +105,16 @@ export const getPostgresInitialSchema = (dbConfig) => {
|
|
|
125
105
|
)`);
|
|
126
106
|
await pool.query(`CREATE INDEX IF NOT EXISTS "idx_users_external_id" ON "users"("external_id")`);
|
|
127
107
|
await pool.query(`CREATE INDEX IF NOT EXISTS "idx_users_email" ON "users"("email")`);
|
|
128
|
-
await pool.query(`CREATE INDEX IF NOT EXISTS "idx_users_person_id" ON "users"("person_id")`);
|
|
129
108
|
},
|
|
130
109
|
down: async ({ context: pool }) => {
|
|
131
110
|
await pool.query(`DROP INDEX IF EXISTS "idx_users_external_id"`);
|
|
132
111
|
await pool.query(`DROP INDEX IF EXISTS "idx_users_email"`);
|
|
133
|
-
await pool.query(`DROP INDEX IF EXISTS "idx_users_person_id"`);
|
|
134
112
|
await pool.query(`DROP TABLE IF EXISTS "users"`);
|
|
135
113
|
},
|
|
136
114
|
});
|
|
137
115
|
if (isAuthEnabled)
|
|
138
116
|
migrations.push({
|
|
139
|
-
name: "
|
|
117
|
+
name: "00000000000004_schema-refresh-tokens",
|
|
140
118
|
up: async ({ context: pool }) => {
|
|
141
119
|
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER NOT NULL,' : "";
|
|
142
120
|
await pool.query(`
|
|
@@ -159,7 +137,7 @@ export const getPostgresInitialSchema = (dbConfig) => {
|
|
|
159
137
|
});
|
|
160
138
|
if (isAuthEnabled)
|
|
161
139
|
migrations.push({
|
|
162
|
-
name: "
|
|
140
|
+
name: "00000000000005_schema-password-reset-tokens",
|
|
163
141
|
up: async ({ context: pool }) => {
|
|
164
142
|
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER NOT NULL,' : "";
|
|
165
143
|
const uniqueConstraint = isMultiTenant
|
|
@@ -188,7 +166,7 @@ export const getPostgresInitialSchema = (dbConfig) => {
|
|
|
188
166
|
});
|
|
189
167
|
if (isAuthEnabled)
|
|
190
168
|
migrations.push({
|
|
191
|
-
name: "
|
|
169
|
+
name: "00000000000006_schema-roles",
|
|
192
170
|
up: async ({ context: pool }) => {
|
|
193
171
|
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER NOT NULL,' : "";
|
|
194
172
|
const uniqueConstraint = isMultiTenant
|
|
@@ -210,7 +188,7 @@ export const getPostgresInitialSchema = (dbConfig) => {
|
|
|
210
188
|
});
|
|
211
189
|
if (isAuthEnabled)
|
|
212
190
|
migrations.push({
|
|
213
|
-
name: "
|
|
191
|
+
name: "00000000000007_schema-user-roles",
|
|
214
192
|
up: async ({ context: pool }) => {
|
|
215
193
|
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER NOT NULL,' : "";
|
|
216
194
|
const uniqueConstraint = isMultiTenant
|
|
@@ -240,7 +218,7 @@ export const getPostgresInitialSchema = (dbConfig) => {
|
|
|
240
218
|
});
|
|
241
219
|
if (isAuthEnabled)
|
|
242
220
|
migrations.push({
|
|
243
|
-
name: "
|
|
221
|
+
name: "00000000000008_schema-features",
|
|
244
222
|
up: async ({ context: pool }) => {
|
|
245
223
|
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER NOT NULL,' : "";
|
|
246
224
|
const uniqueConstraint = isMultiTenant
|
|
@@ -262,7 +240,7 @@ export const getPostgresInitialSchema = (dbConfig) => {
|
|
|
262
240
|
});
|
|
263
241
|
if (isAuthEnabled)
|
|
264
242
|
migrations.push({
|
|
265
|
-
name: "
|
|
243
|
+
name: "00000000000009_schema-authorizations",
|
|
266
244
|
up: async ({ context: pool }) => {
|
|
267
245
|
const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER NOT NULL,' : "";
|
|
268
246
|
const uniqueConstraint = isMultiTenant
|
|
@@ -295,21 +273,28 @@ export const getPostgresInitialSchema = (dbConfig) => {
|
|
|
295
273
|
});
|
|
296
274
|
if (isMultiTenant) {
|
|
297
275
|
migrations.push({
|
|
298
|
-
name: "
|
|
276
|
+
name: "00000000000010_data-meta-org",
|
|
299
277
|
up: async ({ context: pool }) => {
|
|
300
278
|
const result = await pool.query(`
|
|
301
|
-
INSERT INTO "organizations"("name", "code", "
|
|
302
|
-
VALUES($1, $2,
|
|
303
|
-
RETURNING "_id", "name", "code", "
|
|
279
|
+
INSERT INTO "organizations"("name", "code", "status", "is_meta_org", "_created", "_createdBy")
|
|
280
|
+
VALUES($1, $2, 1, true, NOW(), 0)
|
|
281
|
+
RETURNING "_id", "name", "code", "status", "is_meta_org", "_created", "_createdBy"
|
|
304
282
|
`, [
|
|
305
283
|
dbConfig.multiTenant?.metaOrgName,
|
|
306
284
|
dbConfig.multiTenant?.metaOrgCode,
|
|
307
|
-
dbConfig.multiTenant?.metaOrgDomain,
|
|
308
285
|
]);
|
|
309
286
|
if (result.rowCount === 0) {
|
|
310
287
|
throw new Error("Failed to create meta organization");
|
|
311
288
|
}
|
|
312
|
-
|
|
289
|
+
const metaOrg = result.rows[0];
|
|
290
|
+
const metaOrgDomains = dbConfig.multiTenant?.metaOrgDomains ?? [];
|
|
291
|
+
for (const domain of metaOrgDomains) {
|
|
292
|
+
await pool.query(`
|
|
293
|
+
INSERT INTO "organization_domains"("organization_id", "domain", "_created", "_createdBy")
|
|
294
|
+
VALUES($1, $2, NOW(), 0)
|
|
295
|
+
`, [metaOrg._id, domain]);
|
|
296
|
+
}
|
|
297
|
+
initializeSystemUserContext(dbConfig.email?.systemEmailAddress || "system@example.com", metaOrg);
|
|
313
298
|
},
|
|
314
299
|
down: async ({ context: pool }) => {
|
|
315
300
|
await pool.query(`DELETE FROM "organizations" WHERE "is_meta_org" = TRUE`);
|
|
@@ -318,11 +303,11 @@ export const getPostgresInitialSchema = (dbConfig) => {
|
|
|
318
303
|
}
|
|
319
304
|
if (isAuthEnabled && dbConfig.adminUser) {
|
|
320
305
|
migrations.push({
|
|
321
|
-
name: "
|
|
306
|
+
name: "00000000000011_data-admin-user",
|
|
322
307
|
up: async ({ context: pool }) => {
|
|
323
308
|
if (!isSystemUserContextInitialized()) {
|
|
324
309
|
const errorMessage = isMultiTenant
|
|
325
|
-
? "SystemUserContext has not been initialized. The meta-org migration (
|
|
310
|
+
? "SystemUserContext has not been initialized. The meta-org migration (00000000000010_data-meta-org) should have run before this migration. " +
|
|
326
311
|
"Please ensure metaOrgName and metaOrgCode are provided in your dbConfig."
|
|
327
312
|
: "BUG: SystemUserContext has not been initialized. For non-multi-tenant setups, SystemUserContext should be initialized before migrations run.";
|
|
328
313
|
console.error("❌ Migration Error:", errorMessage);
|
|
@@ -336,21 +321,13 @@ export const getPostgresInitialSchema = (dbConfig) => {
|
|
|
336
321
|
const email = dbConfig.adminUser.email.toLowerCase();
|
|
337
322
|
const client = await pool.connect();
|
|
338
323
|
try {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
RETURNING "_id"`, [orgId])
|
|
343
|
-
: await client.query(`INSERT INTO "persons"("first_name", "last_name", "is_agent", "is_client", "is_employee", "_created", "_createdBy")
|
|
344
|
-
VALUES('Admin', 'User', false, false, false, NOW(), 0)
|
|
345
|
-
RETURNING "_id"`);
|
|
346
|
-
const personId = personResult.rows[0]._id;
|
|
347
|
-
if (isMultiTenant && orgId) {
|
|
348
|
-
await client.query(`INSERT INTO "users"("_orgId", "email", "display_name", "password", "person_id", "_created", "_createdBy")
|
|
349
|
-
VALUES($1, $2, 'Admin User', $3, $4, NOW(), 0)`, [orgId, email, hashedPassword, personId]);
|
|
324
|
+
if (isMultiTenant) {
|
|
325
|
+
await client.query(`INSERT INTO "users"("_orgId", "email", "display_name", "password", "_created", "_createdBy")
|
|
326
|
+
VALUES($1, $2, 'Admin User', $3, NOW(), 0)`, [orgId, email, hashedPassword]);
|
|
350
327
|
}
|
|
351
328
|
else {
|
|
352
|
-
await client.query(`INSERT INTO "users"("email", "display_name", "password", "
|
|
353
|
-
|
|
329
|
+
await client.query(`INSERT INTO "users"("email", "display_name", "password", "_created", "_createdBy")
|
|
330
|
+
VALUES($1, 'Admin User', $2, NOW(), 0)`, [email, hashedPassword]);
|
|
354
331
|
}
|
|
355
332
|
}
|
|
356
333
|
finally {
|
|
@@ -368,7 +345,7 @@ export const getPostgresInitialSchema = (dbConfig) => {
|
|
|
368
345
|
}
|
|
369
346
|
if (isAuthEnabled && dbConfig.adminUser) {
|
|
370
347
|
migrations.push({
|
|
371
|
-
name: "
|
|
348
|
+
name: "00000000000012_data-admin-authorizations",
|
|
372
349
|
up: async ({ context: pool }) => {
|
|
373
350
|
const client = await pool.connect();
|
|
374
351
|
try {
|
package/dist/services/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export * from "./generic-query-service/generic-query.service.js";
|
|
|
5
5
|
export * from "./generic-query-service/generic-query-service.interface.js";
|
|
6
6
|
export * from "./multi-tenant-api.service.js";
|
|
7
7
|
export * from "./organization.service.js";
|
|
8
|
+
export * from "./organization-domain.service.js";
|
|
8
9
|
export * from "./password-reset-token.service.js";
|
|
9
10
|
export * from "./refresh-token.service.js";
|
|
10
11
|
export * from "./tenant-query-decorator.js";
|
package/dist/services/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export * from "./generic-query-service/generic-query.service.js";
|
|
|
5
5
|
export * from "./generic-query-service/generic-query-service.interface.js";
|
|
6
6
|
export * from "./multi-tenant-api.service.js";
|
|
7
7
|
export * from "./organization.service.js";
|
|
8
|
+
export * from "./organization-domain.service.js";
|
|
8
9
|
export * from "./password-reset-token.service.js";
|
|
9
10
|
export * from "./refresh-token.service.js";
|
|
10
11
|
export * from "./tenant-query-decorator.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type IOrganizationDomain } from "@loomcore/common/models";
|
|
2
|
+
import type { IDatabase } from "../databases/models/index.js";
|
|
3
|
+
import { GenericApiService } from "./generic-api-service/generic-api.service.js";
|
|
4
|
+
export declare class OrganizationDomainService extends GenericApiService<IOrganizationDomain> {
|
|
5
|
+
constructor(database: IDatabase);
|
|
6
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { OrganizationDomainSpec, } from "@loomcore/common/models";
|
|
2
|
+
import { GenericApiService } from "./generic-api-service/generic-api.service.js";
|
|
3
|
+
export class OrganizationDomainService extends GenericApiService {
|
|
4
|
+
constructor(database) {
|
|
5
|
+
super(database, "organization_domains", "organization_domain", OrganizationDomainSpec);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
@@ -3,9 +3,11 @@ import type { AppIdType } from "@loomcore/common/types";
|
|
|
3
3
|
import type { IDatabase } from "../databases/models/index.js";
|
|
4
4
|
import { GenericApiService } from "./generic-api-service/generic-api.service.js";
|
|
5
5
|
export declare class OrganizationService extends GenericApiService<IOrganization> {
|
|
6
|
+
private organizationDomainService;
|
|
6
7
|
constructor(database: IDatabase);
|
|
7
8
|
preProcessEntity(userContext: IUserContext, entity: Partial<IOrganization>, isCreate: boolean, allowId?: boolean): Promise<Partial<IOrganization>>;
|
|
8
9
|
getAuthTokenByRepoCode(userContext: IUserContext, orgId: AppIdType): Promise<string | null>;
|
|
9
10
|
validateRepoAuthToken(userContext: IUserContext, orgCode: string, authToken: string): Promise<AppIdType | null>;
|
|
10
11
|
getMetaOrg(userContext: IUserContext): Promise<IOrganization | null>;
|
|
12
|
+
findByDomain(userContext: IUserContext, domain: string): Promise<IOrganization | null>;
|
|
11
13
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { OrganizationSpec, } from "@loomcore/common/models";
|
|
2
2
|
import { BadRequestError } from "../errors/index.js";
|
|
3
3
|
import { GenericApiService } from "./generic-api-service/generic-api.service.js";
|
|
4
|
+
import { OrganizationDomainService } from "./organization-domain.service.js";
|
|
4
5
|
export class OrganizationService extends GenericApiService {
|
|
6
|
+
organizationDomainService;
|
|
5
7
|
constructor(database) {
|
|
6
8
|
super(database, "organizations", "organization", OrganizationSpec);
|
|
9
|
+
this.organizationDomainService = new OrganizationDomainService(database);
|
|
7
10
|
}
|
|
8
11
|
async preProcessEntity(userContext, entity, isCreate, allowId = true) {
|
|
9
12
|
if (isCreate) {
|
|
@@ -38,4 +41,15 @@ export class OrganizationService extends GenericApiService {
|
|
|
38
41
|
});
|
|
39
42
|
return org;
|
|
40
43
|
}
|
|
44
|
+
async findByDomain(userContext, domain) {
|
|
45
|
+
const organizationDomain = await this.organizationDomainService.findOne(userContext, {
|
|
46
|
+
filters: { domain: { eq: domain } },
|
|
47
|
+
});
|
|
48
|
+
if (!organizationDomain) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return this.findOne(userContext, {
|
|
52
|
+
filters: { _id: { eq: organizationDomain.organizationId } },
|
|
53
|
+
});
|
|
54
|
+
}
|
|
41
55
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loomcore/api",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.13",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Loom Core Api - An opinionated Node.js api using Typescript, Express, and MongoDb or PostgreSQL",
|
|
6
6
|
"scripts": {
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"qs": "^6.15.2"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@loomcore/common": "^0.0.
|
|
61
|
+
"@loomcore/common": "^0.0.77",
|
|
62
62
|
"@sinclair/typebox": "0.34.33",
|
|
63
63
|
"cookie-parser": "^1.4.6",
|
|
64
64
|
"cors": "^2.8.5",
|