@loomcore/api 0.1.53 → 0.1.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/__tests__/common-test.utils.d.ts +8 -2
  2. package/dist/__tests__/common-test.utils.js +50 -16
  3. package/dist/__tests__/index.d.ts +0 -1
  4. package/dist/__tests__/index.js +0 -1
  5. package/dist/__tests__/postgres-test-migrations/postgres-test-schema.d.ts +3 -0
  6. package/dist/__tests__/postgres-test-migrations/postgres-test-schema.js +97 -0
  7. package/dist/__tests__/postgres.test-database.js +9 -8
  8. package/dist/__tests__/setup/vitest-setup.js +15 -0
  9. package/dist/__tests__/test-objects.d.ts +6 -3
  10. package/dist/__tests__/test-objects.js +10 -2
  11. package/dist/controllers/api.controller.d.ts +2 -0
  12. package/dist/controllers/api.controller.js +66 -11
  13. package/dist/databases/migrations/migration-runner.js +6 -6
  14. package/dist/databases/models/database.interface.d.ts +9 -8
  15. package/dist/databases/mongo-db/commands/mongo-delete-by-id.command.d.ts +2 -1
  16. package/dist/databases/mongo-db/commands/mongo-full-updateby-id.command.d.ts +2 -1
  17. package/dist/databases/mongo-db/commands/mongo-partial-update-by-id.command.d.ts +2 -1
  18. package/dist/databases/mongo-db/migrations/{mongo-foundational.d.ts → mongo-initial-schema.d.ts} +1 -1
  19. package/dist/databases/mongo-db/migrations/mongo-initial-schema.js +264 -0
  20. package/dist/databases/mongo-db/mongo-db.database.d.ts +9 -8
  21. package/dist/databases/mongo-db/mongo-db.database.js +2 -2
  22. package/dist/databases/mongo-db/queries/mongo-get-by-id.query.d.ts +2 -1
  23. package/dist/databases/postgres/commands/postgres-batch-update.command.js +1 -1
  24. package/dist/databases/postgres/commands/postgres-create-many.command.d.ts +2 -1
  25. package/dist/databases/postgres/commands/postgres-create-many.command.js +23 -28
  26. package/dist/databases/postgres/commands/postgres-create.command.d.ts +2 -1
  27. package/dist/databases/postgres/commands/postgres-create.command.js +6 -5
  28. package/dist/databases/postgres/commands/postgres-delete-by-id.command.d.ts +2 -1
  29. package/dist/databases/postgres/commands/postgres-full-update-by-id.command.d.ts +2 -1
  30. package/dist/databases/postgres/commands/postgres-partial-update-by-id.command.d.ts +2 -1
  31. package/dist/databases/postgres/migrations/__tests__/test-migration-helper.d.ts +4 -0
  32. package/dist/databases/postgres/migrations/__tests__/test-migration-helper.js +67 -0
  33. package/dist/databases/postgres/migrations/index.d.ts +1 -2
  34. package/dist/databases/postgres/migrations/index.js +1 -2
  35. package/dist/databases/postgres/migrations/{postgres-foundational.d.ts → postgres-initial-schema.d.ts} +1 -1
  36. package/dist/databases/postgres/migrations/postgres-initial-schema.js +436 -0
  37. package/dist/databases/postgres/postgres.database.d.ts +10 -9
  38. package/dist/databases/postgres/postgres.database.js +2 -2
  39. package/dist/databases/postgres/queries/postgres-get-by-id.query.d.ts +2 -1
  40. package/dist/models/refresh-token.model.d.ts +5 -4
  41. package/dist/models/refresh-token.model.js +3 -3
  42. package/dist/services/auth.service.d.ts +3 -2
  43. package/dist/services/auth.service.js +4 -4
  44. package/dist/services/generic-api-service/generic-api-service.interface.d.ts +8 -7
  45. package/dist/services/generic-api-service/generic-api.service.d.ts +8 -7
  46. package/dist/services/generic-api-service/generic-api.service.js +23 -23
  47. package/dist/services/multi-tenant-api.service.d.ts +1 -1
  48. package/dist/services/multi-tenant-api.service.js +3 -3
  49. package/dist/services/organization.service.d.ts +4 -3
  50. package/dist/services/organization.service.js +3 -3
  51. package/dist/services/user.service.d.ts +3 -2
  52. package/dist/services/user.service.js +2 -2
  53. package/dist/services/utils/audit-for-create.util.js +2 -1
  54. package/dist/services/utils/audit-for-update.util.js +2 -1
  55. package/dist/services/utils/strip-sender-provided-system-properties.util.js +2 -1
  56. package/package.json +2 -2
  57. package/dist/__tests__/postgres-test-migrations/100-create-test-entities-table.migration.d.ts +0 -21
  58. package/dist/__tests__/postgres-test-migrations/100-create-test-entities-table.migration.js +0 -61
  59. package/dist/__tests__/postgres-test-migrations/101-create-categories-table.migration.d.ts +0 -21
  60. package/dist/__tests__/postgres-test-migrations/101-create-categories-table.migration.js +0 -51
  61. package/dist/__tests__/postgres-test-migrations/102-create-products-table.migration.d.ts +0 -21
  62. package/dist/__tests__/postgres-test-migrations/102-create-products-table.migration.js +0 -60
  63. package/dist/__tests__/postgres-test-migrations/103-create-test-items-table.migration.d.ts +0 -21
  64. package/dist/__tests__/postgres-test-migrations/103-create-test-items-table.migration.js +0 -59
  65. package/dist/__tests__/postgres-test-migrations/test-migrations.d.ts +0 -3
  66. package/dist/__tests__/postgres-test-migrations/test-migrations.js +0 -12
  67. package/dist/databases/mongo-db/migrations/mongo-foundational.js +0 -30
  68. package/dist/databases/postgres/migrations/001-create-migrations-table.migration.d.ts +0 -21
  69. package/dist/databases/postgres/migrations/001-create-migrations-table.migration.js +0 -51
  70. package/dist/databases/postgres/migrations/002-create-organizations-table.migration.d.ts +0 -21
  71. package/dist/databases/postgres/migrations/002-create-organizations-table.migration.js +0 -76
  72. package/dist/databases/postgres/migrations/003-create-users-table.migration.d.ts +0 -21
  73. package/dist/databases/postgres/migrations/003-create-users-table.migration.js +0 -77
  74. package/dist/databases/postgres/migrations/004-create-refresh-tokens-table.migration.d.ts +0 -21
  75. package/dist/databases/postgres/migrations/004-create-refresh-tokens-table.migration.js +0 -72
  76. package/dist/databases/postgres/migrations/005-create-meta-org.migration.d.ts +0 -22
  77. package/dist/databases/postgres/migrations/005-create-meta-org.migration.js +0 -64
  78. package/dist/databases/postgres/migrations/006-create-admin-user.migration.d.ts +0 -19
  79. package/dist/databases/postgres/migrations/006-create-admin-user.migration.js +0 -52
  80. package/dist/databases/postgres/migrations/007-create-roles-table.migration.d.ts +0 -21
  81. package/dist/databases/postgres/migrations/007-create-roles-table.migration.js +0 -65
  82. package/dist/databases/postgres/migrations/008-create-user-roles-table.migration.d.ts +0 -21
  83. package/dist/databases/postgres/migrations/008-create-user-roles-table.migration.js +0 -75
  84. package/dist/databases/postgres/migrations/009-create-features-table.migration.d.ts +0 -21
  85. package/dist/databases/postgres/migrations/009-create-features-table.migration.js +0 -65
  86. package/dist/databases/postgres/migrations/010-create-authorizations-table.migration.d.ts +0 -21
  87. package/dist/databases/postgres/migrations/010-create-authorizations-table.migration.js +0 -77
  88. package/dist/databases/postgres/migrations/011-create-admin-authorization.migration.d.ts +0 -23
  89. package/dist/databases/postgres/migrations/011-create-admin-authorization.migration.js +0 -130
  90. package/dist/databases/postgres/migrations/database-builder.d.ts +0 -15
  91. package/dist/databases/postgres/migrations/database-builder.interface.d.ts +0 -10
  92. package/dist/databases/postgres/migrations/database-builder.js +0 -65
  93. package/dist/databases/postgres/migrations/migration.interface.d.ts +0 -11
  94. package/dist/databases/postgres/migrations/migration.interface.js +0 -1
  95. package/dist/databases/postgres/migrations/postgres-foundational.js +0 -47
  96. /package/dist/{databases/postgres/migrations/database-builder.interface.js → __tests__/setup/vitest-setup.d.ts} +0 -0
@@ -9,6 +9,9 @@ import { ICategory } from './models/category.model.js';
9
9
  import { IProduct } from './models/product.model.js';
10
10
  declare function initialize(database: IDatabase): void;
11
11
  declare function getRandomId(): string;
12
+ declare function isMongoDatabase(database: IDatabase): boolean;
13
+ declare function isPostgresDatabase(database: IDatabase): boolean;
14
+ declare function getExpectedIdType(database: IDatabase): 'string' | 'number';
12
15
  declare function createMetaOrg(): Promise<void>;
13
16
  declare function deleteMetaOrg(): Promise<void>;
14
17
  declare function setupTestUsers(): Promise<{
@@ -33,7 +36,7 @@ export declare class ProductService extends GenericApiService<IProduct> {
33
36
  queryObject: IQueryOptions;
34
37
  operations: Operation[];
35
38
  };
36
- postprocessEntity(userContext: IUserContext, single: any): any;
39
+ postProcessEntity(userContext: IUserContext, single: any): any;
37
40
  }
38
41
  export declare class ProductsController extends ApiController<IProduct> {
39
42
  constructor(app: Application, database: IDatabase);
@@ -45,7 +48,7 @@ export declare class MultiTenantProductService extends MultiTenantApiService<IPr
45
48
  queryObject: IQueryOptions;
46
49
  operations: Operation[];
47
50
  };
48
- postprocessEntity(userContext: IUserContext, single: any): any;
51
+ postProcessEntity(userContext: IUserContext, single: any): any;
49
52
  }
50
53
  export declare class MultiTenantProductsController extends ApiController<IProduct> {
51
54
  constructor(app: Application, database: IDatabase);
@@ -70,5 +73,8 @@ declare const testUtils: {
70
73
  setupTestUsers: typeof setupTestUsers;
71
74
  simulateloginWithTestUser: typeof simulateloginWithTestUser;
72
75
  verifyToken: typeof verifyToken;
76
+ isMongoDatabase: typeof isMongoDatabase;
77
+ isPostgresDatabase: typeof isPostgresDatabase;
78
+ getExpectedIdType: typeof getExpectedIdType;
73
79
  };
74
80
  export default testUtils;
@@ -1,5 +1,6 @@
1
1
  import crypto from 'crypto';
2
2
  import jwt from 'jsonwebtoken';
3
+ import { EmptyUserContext } from '@loomcore/common/models';
3
4
  import { Type } from '@sinclair/typebox';
4
5
  import { JwtService } from '../services/jwt.service.js';
5
6
  import { ApiController } from '../controllers/api.controller.js';
@@ -8,8 +9,10 @@ import { Join } from '../databases/operations/join.operation.js';
8
9
  import { OrganizationService } from '../services/organization.service.js';
9
10
  import { AuthService, GenericApiService } from '../services/index.js';
10
11
  import { ObjectId } from 'mongodb';
12
+ import { MongoDBDatabase } from '../databases/mongo-db/mongo-db.database.js';
13
+ import { PostgresDatabase } from '../databases/postgres/postgres.database.js';
11
14
  import * as testObjectsModule from './test-objects.js';
12
- const { getTestMetaOrg, getTestOrg, getTestMetaOrgUser, getTestMetaOrgUserContext, getTestOrgUserContext, setTestOrgId, setTestMetaOrgId } = testObjectsModule;
15
+ const { getTestMetaOrg, getTestOrg, getTestMetaOrgUser, getTestMetaOrgUserContext, getTestOrgUserContext, setTestOrgId, setTestMetaOrgId, setTestMetaOrgUserId, setTestOrgUserId } = testObjectsModule;
13
16
  import { CategorySpec } from './models/category.model.js';
14
17
  import { ProductSpec } from './models/product.model.js';
15
18
  import { setBaseApiConfig } from '../config/index.js';
@@ -30,14 +33,29 @@ function initialize(database) {
30
33
  function getRandomId() {
31
34
  return new ObjectId().toString();
32
35
  }
36
+ function isMongoDatabase(database) {
37
+ return database instanceof MongoDBDatabase;
38
+ }
39
+ function isPostgresDatabase(database) {
40
+ return database instanceof PostgresDatabase;
41
+ }
42
+ function getExpectedIdType(database) {
43
+ return isPostgresDatabase(database) ? 'number' : 'string';
44
+ }
33
45
  async function createMetaOrg() {
34
46
  if (!organizationService) {
35
47
  throw new Error('OrganizationService not initialized. Call initialize() first.');
36
48
  }
37
49
  try {
38
- const existingMetaOrg = await organizationService.getMetaOrg(getTestMetaOrgUserContext());
50
+ const existingMetaOrg = await organizationService.getMetaOrg(EmptyUserContext);
39
51
  if (!existingMetaOrg) {
40
- const metaOrgInsertResult = await organizationService.create(getTestMetaOrgUserContext(), getTestMetaOrg());
52
+ const metaOrgInsertResult = await organizationService.create(EmptyUserContext, getTestMetaOrg());
53
+ if (metaOrgInsertResult) {
54
+ setTestMetaOrgId(metaOrgInsertResult._id);
55
+ }
56
+ }
57
+ else {
58
+ setTestMetaOrgId(existingMetaOrg._id);
41
59
  }
42
60
  }
43
61
  catch (error) {
@@ -58,6 +76,7 @@ async function deleteMetaOrg() {
58
76
  }
59
77
  async function setupTestUsers() {
60
78
  try {
79
+ await createMetaOrg();
61
80
  await deleteTestUser();
62
81
  return createTestUsers();
63
82
  }
@@ -71,13 +90,11 @@ async function createTestUsers() {
71
90
  throw new Error('Database not initialized. Call initialize() first.');
72
91
  }
73
92
  try {
74
- const existingMetaOrg = await organizationService.getMetaOrg(getTestMetaOrgUserContext());
93
+ const existingMetaOrg = await organizationService.getMetaOrg(EmptyUserContext);
75
94
  if (!existingMetaOrg) {
76
- await organizationService.create(getTestMetaOrgUserContext(), getTestMetaOrg());
77
- }
78
- else {
79
- setTestMetaOrgId(existingMetaOrg._id);
95
+ throw new Error('Meta organization does not exist. Test setup is incorrect - meta org should be created by migrations or createMetaOrg().');
80
96
  }
97
+ setTestMetaOrgId(existingMetaOrg._id);
81
98
  const existingTestOrg = await organizationService.findOne(getTestMetaOrgUserContext(), { filters: { _id: { eq: getTestOrg()._id } } });
82
99
  if (!existingTestOrg) {
83
100
  const createdTestOrg = await organizationService.create(getTestMetaOrgUserContext(), getTestOrg());
@@ -94,6 +111,8 @@ async function createTestUsers() {
94
111
  if (!createdTestOrgUser || !createdMetaOrgUser) {
95
112
  throw new Error('Failed to create test user');
96
113
  }
114
+ setTestMetaOrgUserId(createdMetaOrgUser._id);
115
+ setTestOrgUserId(createdTestOrgUser._id);
97
116
  return { metaOrgUser: createdMetaOrgUser, testOrgUser: createdTestOrgUser };
98
117
  }
99
118
  catch (error) {
@@ -102,6 +121,9 @@ async function createTestUsers() {
102
121
  }
103
122
  }
104
123
  async function deleteTestUser() {
124
+ if (!authService || !organizationService) {
125
+ return;
126
+ }
105
127
  await authService.deleteById(getTestMetaOrgUserContext(), getTestMetaOrgUser()._id).catch((error) => {
106
128
  return null;
107
129
  });
@@ -124,6 +146,9 @@ async function simulateloginWithTestUser() {
124
146
  return res;
125
147
  }
126
148
  };
149
+ if (!authService) {
150
+ throw new Error('AuthService not initialized. Call initialize() first.');
151
+ }
127
152
  const loginResponse = await authService.attemptLogin(req, res, getTestMetaOrgUser().email, testObjectsModule.TEST_META_ORG_USER_PASSWORD);
128
153
  if (!loginResponse?.tokens?.accessToken) {
129
154
  throw new Error('Failed to login with test user');
@@ -167,7 +192,13 @@ export function setupTestConfig(isMultiTenant = true) {
167
192
  debug: {
168
193
  showErrors: false
169
194
  },
170
- app: { isMultiTenant: isMultiTenant },
195
+ app: {
196
+ isMultiTenant: isMultiTenant,
197
+ ...(isMultiTenant && {
198
+ metaOrgName: 'Test Meta Organization',
199
+ metaOrgCode: 'TEST_META_ORG'
200
+ })
201
+ },
171
202
  auth: {
172
203
  jwtExpirationInSeconds: 3600,
173
204
  refreshTokenExpirationInDays: 7,
@@ -199,12 +230,12 @@ export class ProductService extends GenericApiService {
199
230
  ];
200
231
  return super.prepareQuery(userContext, queryObject, newOperations);
201
232
  }
202
- postprocessEntity(userContext, single) {
233
+ postProcessEntity(userContext, single) {
203
234
  if (single && single.category) {
204
235
  const categoryService = new CategoryService(this.db);
205
- single.category = categoryService.postprocessEntity(userContext, single.category);
236
+ single.category = categoryService.postProcessEntity(userContext, single.category);
206
237
  }
207
- return super.postprocessEntity(userContext, single);
238
+ return super.postProcessEntity(userContext, single);
208
239
  }
209
240
  }
210
241
  export class ProductsController extends ApiController {
@@ -234,12 +265,12 @@ export class MultiTenantProductService extends MultiTenantApiService {
234
265
  ];
235
266
  return super.prepareQuery(userContext, queryObject, newOperations);
236
267
  }
237
- postprocessEntity(userContext, single) {
268
+ postProcessEntity(userContext, single) {
238
269
  if (single && single.category) {
239
270
  const categoryService = new CategoryService(this.db);
240
- single.category = categoryService.postprocessEntity(userContext, single.category);
271
+ single.category = categoryService.postProcessEntity(userContext, single.category);
241
272
  }
242
- return super.postprocessEntity(userContext, single);
273
+ return super.postProcessEntity(userContext, single);
243
274
  }
244
275
  }
245
276
  export class MultiTenantProductsController extends ApiController {
@@ -303,6 +334,9 @@ const testUtils = {
303
334
  newUser1Password,
304
335
  setupTestUsers,
305
336
  simulateloginWithTestUser,
306
- verifyToken
337
+ verifyToken,
338
+ isMongoDatabase,
339
+ isPostgresDatabase,
340
+ getExpectedIdType
307
341
  };
308
342
  export default testUtils;
@@ -1,4 +1,3 @@
1
- export * from './postgres-test-migrations/100-create-test-entities-table.migration.js';
2
1
  export * from './models/test-entity.model.js';
3
2
  export * from './postgres.test-database.js';
4
3
  export * from './mongo-db.test-database.js';
@@ -1,4 +1,3 @@
1
- export * from './postgres-test-migrations/100-create-test-entities-table.migration.js';
2
1
  export * from './models/test-entity.model.js';
3
2
  export * from './postgres.test-database.js';
4
3
  export * from './mongo-db.test-database.js';
@@ -0,0 +1,3 @@
1
+ import { IBaseApiConfig } from '../../models/base-api-config.interface.js';
2
+ import { SyntheticMigration } from '../../databases/postgres/migrations/postgres-initial-schema.js';
3
+ export declare const getPostgresTestSchema: (config: IBaseApiConfig) => SyntheticMigration[];
@@ -0,0 +1,97 @@
1
+ export const getPostgresTestSchema = (config) => {
2
+ const migrations = [];
3
+ const isMultiTenant = config.app.isMultiTenant === true;
4
+ migrations.push({
5
+ name: '00000000000100_schema-test-entities',
6
+ up: async ({ context: pool }) => {
7
+ const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : '';
8
+ await pool.query(`
9
+ CREATE TABLE IF NOT EXISTS "testEntities" (
10
+ "_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
11
+ ${orgColumnDef}
12
+ "name" VARCHAR(255) NOT NULL,
13
+ "description" TEXT,
14
+ "isActive" BOOLEAN,
15
+ "tags" TEXT[],
16
+ "count" INTEGER,
17
+ "_created" TIMESTAMPTZ NOT NULL,
18
+ "_createdBy" INTEGER NOT NULL,
19
+ "_updated" TIMESTAMPTZ NOT NULL,
20
+ "_updatedBy" INTEGER NOT NULL,
21
+ "_deleted" TIMESTAMPTZ,
22
+ "_deletedBy" INTEGER
23
+ )
24
+ `);
25
+ },
26
+ down: async ({ context: pool }) => {
27
+ await pool.query('DROP TABLE IF EXISTS "testEntities"');
28
+ }
29
+ });
30
+ migrations.push({
31
+ name: '00000000000101_schema-categories',
32
+ up: async ({ context: pool }) => {
33
+ const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : '';
34
+ await pool.query(`
35
+ CREATE TABLE IF NOT EXISTS "categories" (
36
+ "_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
37
+ ${orgColumnDef}
38
+ "name" VARCHAR(255) NOT NULL
39
+ )
40
+ `);
41
+ },
42
+ down: async ({ context: pool }) => {
43
+ await pool.query('DROP TABLE IF EXISTS "categories"');
44
+ }
45
+ });
46
+ migrations.push({
47
+ name: '00000000000102_schema-products',
48
+ up: async ({ context: pool }) => {
49
+ const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : '';
50
+ await pool.query(`
51
+ CREATE TABLE IF NOT EXISTS "products" (
52
+ "_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
53
+ ${orgColumnDef}
54
+ "name" VARCHAR(255) NOT NULL,
55
+ "description" TEXT,
56
+ "internalNumber" VARCHAR(255),
57
+ "categoryId" INTEGER NOT NULL,
58
+ "_created" TIMESTAMPTZ NOT NULL,
59
+ "_createdBy" INTEGER NOT NULL,
60
+ "_updated" TIMESTAMPTZ NOT NULL,
61
+ "_updatedBy" INTEGER NOT NULL,
62
+ "_deleted" TIMESTAMPTZ,
63
+ "_deletedBy" INTEGER,
64
+ CONSTRAINT "fk_products_category" FOREIGN KEY ("categoryId") REFERENCES "categories"("_id") ON DELETE CASCADE
65
+ )
66
+ `);
67
+ },
68
+ down: async ({ context: pool }) => {
69
+ await pool.query('DROP TABLE IF EXISTS "products"');
70
+ }
71
+ });
72
+ migrations.push({
73
+ name: '00000000000103_schema-test-items',
74
+ up: async ({ context: pool }) => {
75
+ const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : '';
76
+ await pool.query(`
77
+ CREATE TABLE IF NOT EXISTS "testItems" (
78
+ "_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
79
+ ${orgColumnDef}
80
+ "name" VARCHAR(255) NOT NULL,
81
+ "value" INTEGER,
82
+ "eventDate" TIMESTAMPTZ,
83
+ "_created" TIMESTAMPTZ NOT NULL,
84
+ "_createdBy" INTEGER NOT NULL,
85
+ "_updated" TIMESTAMPTZ NOT NULL,
86
+ "_updatedBy" INTEGER NOT NULL,
87
+ "_deleted" TIMESTAMPTZ,
88
+ "_deletedBy" INTEGER
89
+ )
90
+ `);
91
+ },
92
+ down: async ({ context: pool }) => {
93
+ await pool.query('DROP TABLE IF EXISTS "testItems"');
94
+ }
95
+ });
96
+ return migrations;
97
+ };
@@ -1,9 +1,8 @@
1
- import { randomUUID } from 'crypto';
2
1
  import testUtils from './common-test.utils.js';
3
2
  import { newDb } from "pg-mem";
4
- import { testMigrations } from './postgres-test-migrations/test-migrations.js';
5
3
  import { PostgresDatabase } from '../databases/postgres/postgres.database.js';
6
- import { DatabaseBuilder } from '../databases/postgres/migrations/database-builder.js';
4
+ import { config } from '../config/base-api-config.js';
5
+ import { runInitialSchemaMigrations, runTestSchemaMigrations } from '../databases/postgres/migrations/__tests__/test-migration-helper.js';
7
6
  export class TestPostgresDatabase {
8
7
  database = null;
9
8
  postgresClient = null;
@@ -16,7 +15,7 @@ export class TestPostgresDatabase {
16
15
  return this.initPromise;
17
16
  }
18
17
  getRandomId() {
19
- return randomUUID();
18
+ throw new Error('getRandomId() should not be used for PostgreSQL _id fields. PostgreSQL uses auto-generated integer IDs. Remove _id from entities and let the database generate it.');
20
19
  }
21
20
  async _performInit(adminUsername, adminPassword) {
22
21
  if (!this.database) {
@@ -26,11 +25,13 @@ export class TestPostgresDatabase {
26
25
  const testDatabase = new PostgresDatabase(postgresClient);
27
26
  this.database = testDatabase;
28
27
  this.postgresClient = postgresClient;
29
- const builder = new DatabaseBuilder(postgresClient);
30
- const result = await builder.withMultitenant().withAuth().withMigrations(testMigrations(postgresClient)).build();
31
- if (!result.success) {
32
- throw new Error('Failed to setup test database');
28
+ const { initializeSystemUserContext, isSystemUserContextInitialized } = await import('@loomcore/common/models');
29
+ if (!isSystemUserContextInitialized()) {
30
+ initializeSystemUserContext(config.email?.systemEmailAddress || 'system@test.com', undefined);
33
31
  }
32
+ const pool = postgresClient;
33
+ await runInitialSchemaMigrations(pool, config);
34
+ await runTestSchemaMigrations(pool, config);
34
35
  testUtils.initialize(testDatabase);
35
36
  await this.createIndexes(postgresClient);
36
37
  await testUtils.createMetaOrg();
@@ -0,0 +1,15 @@
1
+ import { setIdSchema } from '@loomcore/common/validation';
2
+ import { Type } from '@sinclair/typebox';
3
+ if (!process.env.TEST_DATABASE) {
4
+ process.env.TEST_DATABASE = 'postgres';
5
+ }
6
+ const testDatabase = process.env.TEST_DATABASE;
7
+ if (testDatabase === 'postgres') {
8
+ setIdSchema(Type.Number({ title: 'ID', integer: true, minimum: 1 }));
9
+ }
10
+ else if (testDatabase === 'mongodb') {
11
+ setIdSchema(Type.String({ title: 'ID', pattern: '^[0-9a-fA-F]{24}$' }));
12
+ }
13
+ else {
14
+ throw new Error(`Invalid TEST_DATABASE value: ${testDatabase}. Must be 'postgres' or 'mongodb'`);
15
+ }
@@ -1,11 +1,14 @@
1
1
  import { IOrganization, IUserContext, IUser } from "@loomcore/common/models";
2
- export declare let TEST_META_ORG_ID: string;
3
- export declare function setTestMetaOrgId(metaOrgId: string): void;
2
+ export declare let TEST_META_ORG_ID: string | number;
3
+ export declare let TEST_META_ORG_USER_ID: string | number;
4
+ export declare function setTestMetaOrgId(metaOrgId: string | number): void;
5
+ export declare function setTestMetaOrgUserId(userId: string | number): void;
4
6
  export declare const TEST_META_ORG_USER_PASSWORD = "test-meta-org-user-password";
5
7
  export declare function getTestMetaOrg(): IOrganization;
6
8
  export declare function getTestMetaOrgUser(): IUser;
7
9
  export declare function getTestMetaOrgUserContext(): IUserContext;
8
- export declare function setTestOrgId(orgId: string): void;
10
+ export declare function setTestOrgId(orgId: string | number): void;
11
+ export declare function setTestOrgUserId(userId: string | number): void;
9
12
  export declare function getTestOrg(): IOrganization;
10
13
  export declare const TEST_ORG_USER_PASSWORD = "test-org-user-password";
11
14
  export declare function getTestOrgUser(): IUser;
@@ -1,7 +1,11 @@
1
1
  export let TEST_META_ORG_ID = '69261691f936c45f85da24d0';
2
+ export let TEST_META_ORG_USER_ID = '69261672f48fb7bf76e54dfb';
2
3
  export function setTestMetaOrgId(metaOrgId) {
3
4
  TEST_META_ORG_ID = metaOrgId;
4
5
  }
6
+ export function setTestMetaOrgUserId(userId) {
7
+ TEST_META_ORG_USER_ID = userId;
8
+ }
5
9
  export const TEST_META_ORG_USER_PASSWORD = 'test-meta-org-user-password';
6
10
  export function getTestMetaOrg() {
7
11
  return {
@@ -19,7 +23,7 @@ export function getTestMetaOrg() {
19
23
  ;
20
24
  export function getTestMetaOrgUser() {
21
25
  return {
22
- _id: '69261672f48fb7bf76e54dfb',
26
+ _id: TEST_META_ORG_USER_ID,
23
27
  _orgId: getTestMetaOrg()._id,
24
28
  email: 'test@example.com',
25
29
  firstName: 'Test',
@@ -47,9 +51,13 @@ export function getTestMetaOrgUserContext() {
47
51
  }
48
52
  ;
49
53
  let TEST_ORG_ID = '6926167d06c0073a778a124f';
54
+ let TEST_ORG_USER_ID = '6926167d06c0073a778a1250';
50
55
  export function setTestOrgId(orgId) {
51
56
  TEST_ORG_ID = orgId;
52
57
  }
58
+ export function setTestOrgUserId(userId) {
59
+ TEST_ORG_USER_ID = userId;
60
+ }
53
61
  export function getTestOrg() {
54
62
  return {
55
63
  _id: TEST_ORG_ID,
@@ -67,7 +75,7 @@ export function getTestOrg() {
67
75
  export const TEST_ORG_USER_PASSWORD = 'test-org-user-password';
68
76
  export function getTestOrgUser() {
69
77
  return {
70
- _id: '6926167d06c0073a778a1250',
78
+ _id: TEST_ORG_USER_ID,
71
79
  _orgId: getTestOrg()._id,
72
80
  email: 'test-org-user@example.com',
73
81
  firstName: 'Test',
@@ -1,5 +1,6 @@
1
1
  import { Application, NextFunction, Request, Response } from 'express';
2
2
  import { IEntity, IModelSpec } from '@loomcore/common/models';
3
+ import type { TSchema } from '@sinclair/typebox';
3
4
  import { IGenericApiService } from '../services/index.js';
4
5
  export declare abstract class ApiController<T extends IEntity> {
5
6
  protected app: Application;
@@ -8,6 +9,7 @@ export declare abstract class ApiController<T extends IEntity> {
8
9
  protected apiResourceName: string;
9
10
  protected modelSpec?: IModelSpec;
10
11
  protected publicSpec?: IModelSpec;
12
+ protected idSchema: TSchema;
11
13
  protected constructor(slug: string, app: Application, service: IGenericApiService<T>, resourceName?: string, modelSpec?: IModelSpec, publicSpec?: IModelSpec);
12
14
  mapRoutes(app: Application): void;
13
15
  protected validate(entity: any, isPartial?: boolean): void;
@@ -1,5 +1,7 @@
1
1
  import { BadRequestError } from '../errors/index.js';
2
2
  import { entityUtils } from '@loomcore/common/utils';
3
+ import { Value } from '@sinclair/typebox/value';
4
+ import { getIdSchema } from '@loomcore/common/validation';
3
5
  import { apiUtils } from '../utils/index.js';
4
6
  import { isAuthorized } from '../middleware/index.js';
5
7
  export class ApiController {
@@ -9,6 +11,7 @@ export class ApiController {
9
11
  apiResourceName;
10
12
  modelSpec;
11
13
  publicSpec;
14
+ idSchema;
12
15
  constructor(slug, app, service, resourceName = '', modelSpec, publicSpec) {
13
16
  this.slug = slug;
14
17
  this.app = app;
@@ -16,6 +19,7 @@ export class ApiController {
16
19
  this.apiResourceName = resourceName;
17
20
  this.modelSpec = modelSpec;
18
21
  this.publicSpec = publicSpec;
22
+ this.idSchema = getIdSchema();
19
23
  this.mapRoutes(app);
20
24
  }
21
25
  mapRoutes(app) {
@@ -49,10 +53,19 @@ export class ApiController {
49
53
  apiUtils.apiResponse(res, 200, { data: pagedResult }, this.modelSpec, this.publicSpec);
50
54
  }
51
55
  async getById(req, res, next) {
52
- let id = req.params?.id;
53
56
  res.set('Content-Type', 'application/json');
54
- const entity = await this.service.getById(req.userContext, id);
55
- apiUtils.apiResponse(res, 200, { data: entity }, this.modelSpec, this.publicSpec);
57
+ const idParam = req.params?.id;
58
+ if (!idParam) {
59
+ throw new BadRequestError('ID parameter is required');
60
+ }
61
+ try {
62
+ const id = Value.Convert(this.idSchema, idParam);
63
+ const entity = await this.service.getById(req.userContext, id);
64
+ apiUtils.apiResponse(res, 200, { data: entity }, this.modelSpec, this.publicSpec);
65
+ }
66
+ catch (error) {
67
+ throw new BadRequestError(`Invalid ID format: ${error.message || error}`);
68
+ }
56
69
  }
57
70
  async getCount(req, res, next) {
58
71
  res.set('Content-Type', 'application/json');
@@ -71,25 +84,67 @@ export class ApiController {
71
84
  if (!Array.isArray(entities)) {
72
85
  throw new BadRequestError('Request body must be an array of entities.');
73
86
  }
74
- this.validateMany(entities, true);
75
- const updatedEntities = await this.service.batchUpdate(req.userContext, entities);
87
+ const convertedEntities = entities.map(entity => {
88
+ if (entity._id !== undefined) {
89
+ try {
90
+ const convertedId = Value.Convert(this.idSchema, entity._id);
91
+ return { ...entity, _id: convertedId };
92
+ }
93
+ catch (error) {
94
+ throw new BadRequestError(`Invalid ID format for entity: ${error.message || error}`);
95
+ }
96
+ }
97
+ return entity;
98
+ });
99
+ this.validateMany(convertedEntities, true);
100
+ const updatedEntities = await this.service.batchUpdate(req.userContext, convertedEntities);
76
101
  apiUtils.apiResponse(res, 200, { data: updatedEntities }, this.modelSpec, this.publicSpec);
77
102
  }
78
103
  async fullUpdateById(req, res, next) {
79
104
  res.set('Content-Type', 'application/json');
80
105
  this.validate(req.body);
81
- const updateResult = await this.service.fullUpdateById(req.userContext, req.params.id, req.body);
82
- apiUtils.apiResponse(res, 200, { data: updateResult }, this.modelSpec, this.publicSpec);
106
+ const idParam = req.params?.id;
107
+ if (!idParam) {
108
+ throw new BadRequestError('ID parameter is required');
109
+ }
110
+ try {
111
+ const id = Value.Convert(this.idSchema, idParam);
112
+ const updateResult = await this.service.fullUpdateById(req.userContext, id, req.body);
113
+ apiUtils.apiResponse(res, 200, { data: updateResult }, this.modelSpec, this.publicSpec);
114
+ }
115
+ catch (error) {
116
+ throw new BadRequestError(`Invalid ID format: ${error.message || error}`);
117
+ }
83
118
  }
84
119
  async partialUpdateById(req, res, next) {
85
120
  res.set('Content-Type', 'application/json');
86
121
  this.validate(req.body, true);
87
- const updateResult = await this.service.partialUpdateById(req.userContext, req.params.id, req.body);
88
- apiUtils.apiResponse(res, 200, { data: updateResult }, this.modelSpec, this.publicSpec);
122
+ const idParam = req.params?.id;
123
+ if (!idParam) {
124
+ throw new BadRequestError('ID parameter is required');
125
+ }
126
+ try {
127
+ const id = Value.Convert(this.idSchema, idParam);
128
+ const updateResult = await this.service.partialUpdateById(req.userContext, id, req.body);
129
+ apiUtils.apiResponse(res, 200, { data: updateResult }, this.modelSpec, this.publicSpec);
130
+ }
131
+ catch (error) {
132
+ throw new BadRequestError(`Invalid ID format: ${error.message || error}`);
133
+ }
89
134
  }
90
135
  async deleteById(req, res, next) {
91
136
  res.set('Content-Type', 'application/json');
92
- const deleteResult = await this.service.deleteById(req.userContext, req.params.id);
93
- apiUtils.apiResponse(res, 200, { data: deleteResult }, this.modelSpec, this.publicSpec);
137
+ const idParam = req.params?.id;
138
+ if (!idParam) {
139
+ throw new BadRequestError('ID parameter is required');
140
+ }
141
+ try {
142
+ const id = Value.Convert(this.idSchema, idParam);
143
+ const deleteResult = await this.service.deleteById(req.userContext, id);
144
+ apiUtils.apiResponse(res, 200, { data: deleteResult }, this.modelSpec, this.publicSpec);
145
+ }
146
+ catch (error) {
147
+ throw new BadRequestError(`Invalid ID format: ${error.message || error}`);
148
+ }
94
149
  }
95
150
  }
@@ -5,8 +5,8 @@ import fs from 'fs';
5
5
  import path from 'path';
6
6
  import { buildMongoUrl } from '../mongo-db/utils/build-mongo-url.util.js';
7
7
  import { buildPostgresUrl } from '../postgres/utils/build-postgres-url.util.js';
8
- import { getPostgresFoundational } from '../postgres/migrations/postgres-foundational.js';
9
- import { getMongoFoundational } from '../mongo-db/migrations/mongo-foundational.js';
8
+ import { getPostgresInitialSchema } from '../postgres/migrations/postgres-initial-schema.js';
9
+ import { getMongoInitialSchema } from '../mongo-db/migrations/mongo-initial-schema.js';
10
10
  export class MigrationRunner {
11
11
  config;
12
12
  dbType;
@@ -56,7 +56,7 @@ export class MigrationRunner {
56
56
  this.dbConnection = pool;
57
57
  return new Umzug({
58
58
  migrations: async () => {
59
- const foundationals = getPostgresFoundational(this.config).map(m => ({
59
+ const initialSchema = getPostgresInitialSchema(this.config).map(m => ({
60
60
  name: m.name,
61
61
  up: async () => {
62
62
  console.log(` Running [LIBRARY] ${m.name}...`);
@@ -68,7 +68,7 @@ export class MigrationRunner {
68
68
  }
69
69
  }));
70
70
  const fileMigrations = this.loadFileMigrations(this.migrationsDir, 'sql', pool);
71
- return [...foundationals, ...fileMigrations].sort((a, b) => a.name.localeCompare(b.name));
71
+ return [...initialSchema, ...fileMigrations].sort((a, b) => a.name.localeCompare(b.name));
72
72
  },
73
73
  context: pool,
74
74
  storage: {
@@ -95,7 +95,7 @@ export class MigrationRunner {
95
95
  console.log(`🔎 Looking for migrations in: ${globPattern}`);
96
96
  return new Umzug({
97
97
  migrations: async () => {
98
- const foundationals = getMongoFoundational(this.config).map(m => ({
98
+ const initialSchema = getMongoInitialSchema(this.config).map(m => ({
99
99
  name: m.name,
100
100
  up: async () => {
101
101
  console.log(` Running [LIBRARY] ${m.name}...`);
@@ -107,7 +107,7 @@ export class MigrationRunner {
107
107
  }
108
108
  }));
109
109
  const fileMigrations = this.loadFileMigrations(this.migrationsDir, 'ts', db);
110
- return [...foundationals, ...fileMigrations].sort((a, b) => a.name.localeCompare(b.name));
110
+ return [...initialSchema, ...fileMigrations].sort((a, b) => a.name.localeCompare(b.name));
111
111
  },
112
112
  context: db,
113
113
  storage: new MongoDBStorage({ collection: db.collection('migrations') }),
@@ -1,27 +1,28 @@
1
1
  import { IModelSpec, IQueryOptions, IPagedResult, IEntity } from "@loomcore/common/models";
2
+ import type { AppIdType } from "@loomcore/common/types";
2
3
  import { DeleteResult } from "./delete-result.js";
3
4
  import { TSchema } from "@sinclair/typebox";
4
5
  import { Operation } from "../operations/operation.js";
5
6
  export interface IDatabase {
6
- preprocessEntity<T extends IEntity>(entity: Partial<T>, modelSpec: TSchema): Partial<T>;
7
- postprocessEntity<T extends IEntity>(entity: T, modelSpec: TSchema): T;
7
+ preProcessEntity<T extends IEntity>(entity: Partial<T>, modelSpec: TSchema): Partial<T>;
8
+ postProcessEntity<T extends IEntity>(entity: T, modelSpec: TSchema): T;
8
9
  getAll<T extends IEntity>(operations: Operation[], pluralResourceName: string): Promise<T[]>;
9
10
  get<T extends IEntity>(operations: Operation[], queryOptions: IQueryOptions, modelSpec: IModelSpec, pluralResourceName: string): Promise<IPagedResult<T>>;
10
- getById<T extends IEntity>(operations: Operation[], queryObject: IQueryOptions, id: string, pluralResourceName: string): Promise<T | null>;
11
+ getById<T extends IEntity>(operations: Operation[], queryObject: IQueryOptions, id: AppIdType, pluralResourceName: string): Promise<T | null>;
11
12
  getCount(pluralResourceName: string): Promise<number>;
12
13
  create<T extends IEntity>(entity: Partial<T>, pluralResourceName: string): Promise<{
13
- insertedId: string;
14
+ insertedId: AppIdType;
14
15
  entity: T;
15
16
  }>;
16
17
  createMany<T extends IEntity>(entities: Partial<T>[], pluralResourceName: string): Promise<{
17
- insertedIds: string[];
18
+ insertedIds: AppIdType[];
18
19
  entities: T[];
19
20
  }>;
20
21
  batchUpdate<T extends IEntity>(entities: Partial<T>[], operations: Operation[], queryObject: IQueryOptions, pluralResourceName: string): Promise<T[]>;
21
- fullUpdateById<T extends IEntity>(operations: Operation[], id: string, entity: Partial<T>, pluralResourceName: string): Promise<T>;
22
- partialUpdateById<T extends IEntity>(operations: Operation[], id: string, entity: Partial<T>, pluralResourceName: string): Promise<T>;
22
+ fullUpdateById<T extends IEntity>(operations: Operation[], id: AppIdType, entity: Partial<T>, pluralResourceName: string): Promise<T>;
23
+ partialUpdateById<T extends IEntity>(operations: Operation[], id: AppIdType, entity: Partial<T>, pluralResourceName: string): Promise<T>;
23
24
  update<T extends IEntity>(queryObject: IQueryOptions, entity: Partial<T>, operations: Operation[], pluralResourceName: string): Promise<T[]>;
24
- deleteById(id: string, pluralResourceName: string): Promise<DeleteResult>;
25
+ deleteById(id: AppIdType, pluralResourceName: string): Promise<DeleteResult>;
25
26
  deleteMany(queryObject: IQueryOptions, pluralResourceName: string): Promise<DeleteResult>;
26
27
  find<T extends IEntity>(queryObject: IQueryOptions, pluralResourceName: string): Promise<T[]>;
27
28
  findOne<T extends IEntity>(queryObject: IQueryOptions, pluralResourceName: string): Promise<T | null>;