@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.
- package/dist/__tests__/common-test.utils.d.ts +8 -2
- package/dist/__tests__/common-test.utils.js +50 -16
- package/dist/__tests__/index.d.ts +0 -1
- package/dist/__tests__/index.js +0 -1
- package/dist/__tests__/postgres-test-migrations/postgres-test-schema.d.ts +3 -0
- package/dist/__tests__/postgres-test-migrations/postgres-test-schema.js +97 -0
- package/dist/__tests__/postgres.test-database.js +9 -8
- package/dist/__tests__/setup/vitest-setup.js +15 -0
- package/dist/__tests__/test-objects.d.ts +6 -3
- package/dist/__tests__/test-objects.js +10 -2
- package/dist/controllers/api.controller.d.ts +2 -0
- package/dist/controllers/api.controller.js +66 -11
- package/dist/databases/migrations/migration-runner.js +6 -6
- package/dist/databases/models/database.interface.d.ts +9 -8
- package/dist/databases/mongo-db/commands/mongo-delete-by-id.command.d.ts +2 -1
- package/dist/databases/mongo-db/commands/mongo-full-updateby-id.command.d.ts +2 -1
- package/dist/databases/mongo-db/commands/mongo-partial-update-by-id.command.d.ts +2 -1
- package/dist/databases/mongo-db/migrations/{mongo-foundational.d.ts → mongo-initial-schema.d.ts} +1 -1
- package/dist/databases/mongo-db/migrations/mongo-initial-schema.js +264 -0
- package/dist/databases/mongo-db/mongo-db.database.d.ts +9 -8
- package/dist/databases/mongo-db/mongo-db.database.js +2 -2
- package/dist/databases/mongo-db/queries/mongo-get-by-id.query.d.ts +2 -1
- package/dist/databases/postgres/commands/postgres-batch-update.command.js +1 -1
- package/dist/databases/postgres/commands/postgres-create-many.command.d.ts +2 -1
- package/dist/databases/postgres/commands/postgres-create-many.command.js +23 -28
- package/dist/databases/postgres/commands/postgres-create.command.d.ts +2 -1
- package/dist/databases/postgres/commands/postgres-create.command.js +6 -5
- package/dist/databases/postgres/commands/postgres-delete-by-id.command.d.ts +2 -1
- package/dist/databases/postgres/commands/postgres-full-update-by-id.command.d.ts +2 -1
- package/dist/databases/postgres/commands/postgres-partial-update-by-id.command.d.ts +2 -1
- package/dist/databases/postgres/migrations/__tests__/test-migration-helper.d.ts +4 -0
- package/dist/databases/postgres/migrations/__tests__/test-migration-helper.js +67 -0
- package/dist/databases/postgres/migrations/index.d.ts +1 -2
- package/dist/databases/postgres/migrations/index.js +1 -2
- package/dist/databases/postgres/migrations/{postgres-foundational.d.ts → postgres-initial-schema.d.ts} +1 -1
- package/dist/databases/postgres/migrations/postgres-initial-schema.js +436 -0
- package/dist/databases/postgres/postgres.database.d.ts +10 -9
- package/dist/databases/postgres/postgres.database.js +2 -2
- package/dist/databases/postgres/queries/postgres-get-by-id.query.d.ts +2 -1
- package/dist/models/refresh-token.model.d.ts +5 -4
- package/dist/models/refresh-token.model.js +3 -3
- package/dist/services/auth.service.d.ts +3 -2
- package/dist/services/auth.service.js +4 -4
- package/dist/services/generic-api-service/generic-api-service.interface.d.ts +8 -7
- package/dist/services/generic-api-service/generic-api.service.d.ts +8 -7
- package/dist/services/generic-api-service/generic-api.service.js +23 -23
- package/dist/services/multi-tenant-api.service.d.ts +1 -1
- package/dist/services/multi-tenant-api.service.js +3 -3
- package/dist/services/organization.service.d.ts +4 -3
- package/dist/services/organization.service.js +3 -3
- package/dist/services/user.service.d.ts +3 -2
- package/dist/services/user.service.js +2 -2
- package/dist/services/utils/audit-for-create.util.js +2 -1
- package/dist/services/utils/audit-for-update.util.js +2 -1
- package/dist/services/utils/strip-sender-provided-system-properties.util.js +2 -1
- package/package.json +2 -2
- package/dist/__tests__/postgres-test-migrations/100-create-test-entities-table.migration.d.ts +0 -21
- package/dist/__tests__/postgres-test-migrations/100-create-test-entities-table.migration.js +0 -61
- package/dist/__tests__/postgres-test-migrations/101-create-categories-table.migration.d.ts +0 -21
- package/dist/__tests__/postgres-test-migrations/101-create-categories-table.migration.js +0 -51
- package/dist/__tests__/postgres-test-migrations/102-create-products-table.migration.d.ts +0 -21
- package/dist/__tests__/postgres-test-migrations/102-create-products-table.migration.js +0 -60
- package/dist/__tests__/postgres-test-migrations/103-create-test-items-table.migration.d.ts +0 -21
- package/dist/__tests__/postgres-test-migrations/103-create-test-items-table.migration.js +0 -59
- package/dist/__tests__/postgres-test-migrations/test-migrations.d.ts +0 -3
- package/dist/__tests__/postgres-test-migrations/test-migrations.js +0 -12
- package/dist/databases/mongo-db/migrations/mongo-foundational.js +0 -30
- package/dist/databases/postgres/migrations/001-create-migrations-table.migration.d.ts +0 -21
- package/dist/databases/postgres/migrations/001-create-migrations-table.migration.js +0 -51
- package/dist/databases/postgres/migrations/002-create-organizations-table.migration.d.ts +0 -21
- package/dist/databases/postgres/migrations/002-create-organizations-table.migration.js +0 -76
- package/dist/databases/postgres/migrations/003-create-users-table.migration.d.ts +0 -21
- package/dist/databases/postgres/migrations/003-create-users-table.migration.js +0 -77
- package/dist/databases/postgres/migrations/004-create-refresh-tokens-table.migration.d.ts +0 -21
- package/dist/databases/postgres/migrations/004-create-refresh-tokens-table.migration.js +0 -72
- package/dist/databases/postgres/migrations/005-create-meta-org.migration.d.ts +0 -22
- package/dist/databases/postgres/migrations/005-create-meta-org.migration.js +0 -64
- package/dist/databases/postgres/migrations/006-create-admin-user.migration.d.ts +0 -19
- package/dist/databases/postgres/migrations/006-create-admin-user.migration.js +0 -52
- package/dist/databases/postgres/migrations/007-create-roles-table.migration.d.ts +0 -21
- package/dist/databases/postgres/migrations/007-create-roles-table.migration.js +0 -65
- package/dist/databases/postgres/migrations/008-create-user-roles-table.migration.d.ts +0 -21
- package/dist/databases/postgres/migrations/008-create-user-roles-table.migration.js +0 -75
- package/dist/databases/postgres/migrations/009-create-features-table.migration.d.ts +0 -21
- package/dist/databases/postgres/migrations/009-create-features-table.migration.js +0 -65
- package/dist/databases/postgres/migrations/010-create-authorizations-table.migration.d.ts +0 -21
- package/dist/databases/postgres/migrations/010-create-authorizations-table.migration.js +0 -77
- package/dist/databases/postgres/migrations/011-create-admin-authorization.migration.d.ts +0 -23
- package/dist/databases/postgres/migrations/011-create-admin-authorization.migration.js +0 -130
- package/dist/databases/postgres/migrations/database-builder.d.ts +0 -15
- package/dist/databases/postgres/migrations/database-builder.interface.d.ts +0 -10
- package/dist/databases/postgres/migrations/database-builder.js +0 -65
- package/dist/databases/postgres/migrations/migration.interface.d.ts +0 -11
- package/dist/databases/postgres/migrations/migration.interface.js +0 -1
- package/dist/databases/postgres/migrations/postgres-foundational.js +0 -47
- /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
|
-
|
|
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
|
-
|
|
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(
|
|
50
|
+
const existingMetaOrg = await organizationService.getMetaOrg(EmptyUserContext);
|
|
39
51
|
if (!existingMetaOrg) {
|
|
40
|
-
const metaOrgInsertResult = await organizationService.create(
|
|
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(
|
|
93
|
+
const existingMetaOrg = await organizationService.getMetaOrg(EmptyUserContext);
|
|
75
94
|
if (!existingMetaOrg) {
|
|
76
|
-
|
|
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: {
|
|
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
|
-
|
|
233
|
+
postProcessEntity(userContext, single) {
|
|
203
234
|
if (single && single.category) {
|
|
204
235
|
const categoryService = new CategoryService(this.db);
|
|
205
|
-
single.category = categoryService.
|
|
236
|
+
single.category = categoryService.postProcessEntity(userContext, single.category);
|
|
206
237
|
}
|
|
207
|
-
return super.
|
|
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
|
-
|
|
268
|
+
postProcessEntity(userContext, single) {
|
|
238
269
|
if (single && single.category) {
|
|
239
270
|
const categoryService = new CategoryService(this.db);
|
|
240
|
-
single.category = categoryService.
|
|
271
|
+
single.category = categoryService.postProcessEntity(userContext, single.category);
|
|
241
272
|
}
|
|
242
|
-
return super.
|
|
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;
|
package/dist/__tests__/index.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
55
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
|
82
|
-
|
|
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
|
|
88
|
-
|
|
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
|
|
93
|
-
|
|
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 {
|
|
9
|
-
import {
|
|
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
|
|
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 [...
|
|
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
|
|
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 [...
|
|
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
|
-
|
|
7
|
-
|
|
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:
|
|
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:
|
|
14
|
+
insertedId: AppIdType;
|
|
14
15
|
entity: T;
|
|
15
16
|
}>;
|
|
16
17
|
createMany<T extends IEntity>(entities: Partial<T>[], pluralResourceName: string): Promise<{
|
|
17
|
-
insertedIds:
|
|
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:
|
|
22
|
-
partialUpdateById<T extends IEntity>(operations: Operation[], id:
|
|
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:
|
|
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>;
|