@loomcore/api 0.2.12 → 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.
@@ -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,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(org: Pick<IOrganization, "domain">): string;
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(org) {
13
- if (!org.domain) {
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://${org.domain}`;
16
+ return `https://${domain}`;
17
17
  }
18
18
  export function getTestMetaOrgRefererUrl() {
19
- return getRefererUrlForOrg(getTestMetaOrg());
19
+ return getRefererUrlForOrg(TEST_META_ORG_DOMAIN);
20
20
  }
21
21
  export function getTestOrgRefererUrl() {
22
- return getRefererUrlForOrg(getTestOrg());
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.findOne(EmptyUserContext, {
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.findOne(EmptyUserContext, {
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.findOne(EmptyUserContext, {
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,6 +26,22 @@ export const getMongoInitialSchema = (dbConfig) => {
26
26
  await db.collection("organizations").drop();
27
27
  },
28
28
  });
29
+ migrations.push({
30
+ name: "00000000000001_schema-organization-domains",
31
+ up: async ({ context: db }) => {
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 });
39
+ },
40
+ down: async ({ context: db }) => {
41
+ await db.collection("organization_domains").drop();
42
+ },
43
+ });
44
+ }
29
45
  if (isAuthEnabled)
30
46
  migrations.push({
31
47
  name: "00000000000002_schema-users",
@@ -201,9 +217,21 @@ export const getMongoInitialSchema = (dbConfig) => {
201
217
  ...metaOrgDoc,
202
218
  _id: result.insertedId,
203
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
+ }
204
231
  initializeSystemUserContext(dbConfig.email?.systemEmailAddress || "system@example.com", metaOrg);
205
232
  },
206
233
  down: async ({ context: db }) => {
234
+ await db.collection("organization_domains").deleteMany({});
207
235
  await db.collection("organizations").deleteMany({ isMetaOrg: true });
208
236
  },
209
237
  });
@@ -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', metaOrgDomain: TEST_META_ORG_DOMAIN } : { metaOrgName: '', metaOrgCode: '', metaOrgDomain: '' }),
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,6 +51,31 @@ export const getPostgresInitialSchema = (dbConfig) => {
52
51
  await pool.query('DROP TABLE IF EXISTS "organizations"');
53
52
  },
54
53
  });
54
+ migrations.push({
55
+ name: "00000000000002_schema-organization-domains",
56
+ up: async ({ context: pool }) => {
57
+ await pool.query(`
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")`);
73
+ },
74
+ down: async ({ context: pool }) => {
75
+ await pool.query(`DROP INDEX IF EXISTS "idx_organization_domains_organization_id"`);
76
+ await pool.query('DROP TABLE IF EXISTS "organization_domains"');
77
+ },
78
+ });
55
79
  }
56
80
  if (isAuthEnabled)
57
81
  migrations.push({
@@ -252,18 +276,25 @@ export const getPostgresInitialSchema = (dbConfig) => {
252
276
  name: "00000000000010_data-meta-org",
253
277
  up: async ({ context: pool }) => {
254
278
  const result = await pool.query(`
255
- INSERT INTO "organizations"("name", "code", "domain", "status", "is_meta_org", "_created", "_createdBy")
256
- VALUES($1, $2, $3, 1, true, NOW(), 0)
257
- RETURNING "_id", "name", "code", "domain", "status", "is_meta_org", "_created", "_createdBy"
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"
258
282
  `, [
259
283
  dbConfig.multiTenant?.metaOrgName,
260
284
  dbConfig.multiTenant?.metaOrgCode,
261
- dbConfig.multiTenant?.metaOrgDomain,
262
285
  ]);
263
286
  if (result.rowCount === 0) {
264
287
  throw new Error("Failed to create meta organization");
265
288
  }
266
- initializeSystemUserContext(dbConfig.email?.systemEmailAddress || "system@example.com", result.rows[0]);
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);
267
298
  },
268
299
  down: async ({ context: pool }) => {
269
300
  await pool.query(`DELETE FROM "organizations" WHERE "is_meta_org" = TRUE`);
@@ -12,7 +12,7 @@ export interface IInitialDbMigrationConfig {
12
12
  multiTenant: {
13
13
  metaOrgName: string;
14
14
  metaOrgCode: string;
15
- metaOrgDomain: string;
15
+ metaOrgDomains: string[];
16
16
  };
17
17
  email?: IEmailConfig;
18
18
  }
@@ -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";
@@ -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.12",
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.74",
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",