@loomcore/api 0.2.4 → 0.2.5
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 +12 -12
- package/dist/__tests__/common-test.utils.js +96 -90
- package/dist/__tests__/postgres.test-database.d.ts +2 -2
- package/dist/__tests__/postgres.test-database.js +16 -15
- package/dist/config/base-api-config.d.ts +2 -2
- package/dist/config/base-api-config.js +17 -10
- package/dist/databases/mongo-db/migrations/mongo-initial-schema.d.ts +2 -2
- package/dist/databases/mongo-db/migrations/mongo-initial-schema.js +185 -128
- package/dist/services/auth.service.d.ts +7 -7
- package/dist/services/auth.service.js +71 -58
- package/dist/services/email.service.js +5 -5
- package/dist/services/jwt.service.js +2 -2
- package/dist/services/multi-tenant-api.service.d.ts +4 -4
- package/dist/services/multi-tenant-api.service.js +8 -8
- package/dist/services/organization.service.d.ts +4 -4
- package/dist/services/organization.service.js +12 -8
- package/dist/services/password-reset-token.service.d.ts +4 -4
- package/dist/services/password-reset-token.service.js +13 -9
- package/dist/services/person.service.d.ts +4 -4
- package/dist/services/person.service.js +3 -3
- package/dist/services/tenant-query-decorator.d.ts +1 -1
- package/dist/services/tenant-query-decorator.js +17 -11
- package/dist/services/user.service.d.ts +6 -6
- package/dist/services/user.service.js +7 -7
- package/dist/services/utils/audit-for-create.util.d.ts +1 -1
- package/dist/services/utils/audit-for-update.util.d.ts +1 -1
- package/dist/services/utils/audit-for-update.util.js +0 -1
- package/dist/services/utils/getUserContextAuthorizations.util.d.ts +2 -2
- package/dist/services/utils/strip-sender-provided-system-properties.util.d.ts +1 -1
- package/dist/services/utils/strip-sender-provided-system-properties.util.js +5 -3
- package/package.json +1 -1
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { ApiController } from
|
|
4
|
-
import {
|
|
5
|
-
import { Operation } from
|
|
6
|
-
import { GenericApiService } from
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
1
|
+
import { IQueryOptions, IUser, IUserContext } from "@loomcore/common/models";
|
|
2
|
+
import { Application, NextFunction, Request, Response } from "express";
|
|
3
|
+
import { ApiController } from "../controllers/api.controller.js";
|
|
4
|
+
import { IDatabase } from "../databases/models/index.js";
|
|
5
|
+
import { Operation } from "../databases/operations/operation.js";
|
|
6
|
+
import { GenericApiService } from "../services/index.js";
|
|
7
|
+
import { MultiTenantApiService } from "../services/multi-tenant-api.service.js";
|
|
8
|
+
import { DbType } from "../databases/db-type.type.js";
|
|
9
|
+
import { ICategory } from "./models/category.model.js";
|
|
10
|
+
import { IProduct } from "./models/product.model.js";
|
|
11
|
+
import { IProductWithCategory } from "./models/product-with-category.model.js";
|
|
12
12
|
declare function initialize(database: IDatabase): void;
|
|
13
13
|
declare function getRandomId(): string;
|
|
14
14
|
declare function isMongoDatabase(database: IDatabase): boolean;
|
|
15
15
|
declare function isPostgresDatabase(database: IDatabase): boolean;
|
|
16
|
-
declare function getExpectedIdType(database: IDatabase):
|
|
16
|
+
declare function getExpectedIdType(database: IDatabase): "string" | "number";
|
|
17
17
|
declare function createMetaOrg(): Promise<void>;
|
|
18
18
|
declare function deleteMetaOrg(): Promise<void>;
|
|
19
19
|
declare function setupTestUsers(): Promise<{
|
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import { ApiController } from
|
|
8
|
-
import {
|
|
9
|
-
import { LeftJoin } from
|
|
10
|
-
import {
|
|
11
|
-
import { AuthService, GenericApiService } from
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import * as testObjectsModule from
|
|
16
|
-
const { getTestMetaOrg, getTestOrg, getTestMetaOrgUser, getTestMetaOrgUserContext, getTestOrgUserContext, setTestOrgId, setTestMetaOrgId, setTestMetaOrgUserId, setTestOrgUserId } = testObjectsModule;
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
1
|
+
import { EmptyUserContext, } from "@loomcore/common/models";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import { Value } from "@sinclair/typebox/value";
|
|
4
|
+
import crypto from "crypto";
|
|
5
|
+
import jwt from "jsonwebtoken";
|
|
6
|
+
import { ObjectId } from "mongodb";
|
|
7
|
+
import { ApiController } from "../controllers/api.controller.js";
|
|
8
|
+
import { MongoDBDatabase } from "../databases/mongo-db/mongo-db.database.js";
|
|
9
|
+
import { LeftJoin } from "../databases/operations/left-join.operation.js";
|
|
10
|
+
import { PostgresDatabase } from "../databases/postgres/postgres.database.js";
|
|
11
|
+
import { AuthService, GenericApiService } from "../services/index.js";
|
|
12
|
+
import { JwtService } from "../services/jwt.service.js";
|
|
13
|
+
import { MultiTenantApiService } from "../services/multi-tenant-api.service.js";
|
|
14
|
+
import { OrganizationService } from "../services/organization.service.js";
|
|
15
|
+
import * as testObjectsModule from "./test-objects.js";
|
|
16
|
+
const { getTestMetaOrg, getTestOrg, getTestMetaOrgUser, getTestMetaOrgUserContext, getTestOrgUserContext, setTestOrgId, setTestMetaOrgId, setTestMetaOrgUserId, setTestOrgUserId, } = testObjectsModule;
|
|
17
|
+
import { entityUtils } from "@loomcore/common/utils";
|
|
18
|
+
import { config, setBaseApiConfig } from "../config/index.js";
|
|
19
|
+
import { PersonService } from "../services/person.service.js";
|
|
20
|
+
import { apiUtils } from "../utils/index.js";
|
|
21
|
+
import { CategorySpec } from "./models/category.model.js";
|
|
22
|
+
import { ProductSpec } from "./models/product.model.js";
|
|
23
|
+
import { ProductWithCategoryPublicSpec, ProductWithCategorySpec, } from "./models/product-with-category.model.js";
|
|
24
|
+
import { TestEmailClient } from "./test-email-client.js";
|
|
25
|
+
import { getTestMetaOrgUserPerson, getTestOrgUser, getTestOrgUserPerson, setTestMetaOrgUserPersonId, setTestOrgUserPersonId, } from "./test-objects.js";
|
|
26
26
|
let deviceIdCookie;
|
|
27
27
|
let authService;
|
|
28
28
|
let organizationService;
|
|
29
29
|
let personService;
|
|
30
|
-
const JWT_SECRET =
|
|
31
|
-
const newUser1Email =
|
|
32
|
-
const newUser1Password =
|
|
33
|
-
const constDeviceIdCookie = crypto.randomBytes(16).toString(
|
|
30
|
+
const JWT_SECRET = "test-secret";
|
|
31
|
+
const newUser1Email = "one@test.com";
|
|
32
|
+
const newUser1Password = "testone";
|
|
33
|
+
const constDeviceIdCookie = crypto.randomBytes(16).toString("hex");
|
|
34
34
|
function initialize(database) {
|
|
35
35
|
authService = new AuthService(database);
|
|
36
36
|
personService = new PersonService(database);
|
|
@@ -47,14 +47,14 @@ function isPostgresDatabase(database) {
|
|
|
47
47
|
return database instanceof PostgresDatabase;
|
|
48
48
|
}
|
|
49
49
|
function getExpectedIdType(database) {
|
|
50
|
-
return isPostgresDatabase(database) ?
|
|
50
|
+
return isPostgresDatabase(database) ? "number" : "string";
|
|
51
51
|
}
|
|
52
52
|
async function createMetaOrg() {
|
|
53
53
|
if (!config.app.isMultiTenant) {
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
56
|
if (!organizationService) {
|
|
57
|
-
throw new Error(
|
|
57
|
+
throw new Error("OrganizationService not initialized. Call initialize() first.");
|
|
58
58
|
}
|
|
59
59
|
try {
|
|
60
60
|
const existingMetaOrg = await organizationService.getMetaOrg(EmptyUserContext);
|
|
@@ -69,7 +69,7 @@ async function createMetaOrg() {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
catch (error) {
|
|
72
|
-
console.log(
|
|
72
|
+
console.log("Error in createMetaOrg:", error);
|
|
73
73
|
throw error;
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -78,10 +78,12 @@ async function deleteMetaOrg() {
|
|
|
78
78
|
return Promise.resolve();
|
|
79
79
|
}
|
|
80
80
|
try {
|
|
81
|
-
await organizationService.deleteMany(getTestMetaOrgUserContext(), {
|
|
81
|
+
await organizationService.deleteMany(getTestMetaOrgUserContext(), {
|
|
82
|
+
filters: { isMetaOrg: { eq: true } },
|
|
83
|
+
});
|
|
82
84
|
}
|
|
83
85
|
catch (error) {
|
|
84
|
-
console.log(
|
|
86
|
+
console.log("Error deleting meta org:", error);
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
89
|
async function setupTestUsers() {
|
|
@@ -97,19 +99,19 @@ async function setupTestUsers() {
|
|
|
97
99
|
}
|
|
98
100
|
async function createTestUsers() {
|
|
99
101
|
if (!authService || !organizationService || !personService) {
|
|
100
|
-
throw new Error(
|
|
102
|
+
throw new Error("Database not initialized. Call initialize() first.");
|
|
101
103
|
}
|
|
102
104
|
try {
|
|
103
105
|
const existingMetaOrg = await organizationService.getMetaOrg(EmptyUserContext);
|
|
104
106
|
if (!existingMetaOrg) {
|
|
105
|
-
throw new Error(
|
|
107
|
+
throw new Error("Meta organization does not exist. Test setup is incorrect - meta org should be created by migrations or createMetaOrg().");
|
|
106
108
|
}
|
|
107
109
|
setTestMetaOrgId(existingMetaOrg._id);
|
|
108
110
|
const existingTestOrg = await organizationService.findOne(getTestMetaOrgUserContext(), { filters: { _id: { eq: getTestOrg()._id } } });
|
|
109
111
|
if (!existingTestOrg) {
|
|
110
112
|
const createdTestOrg = await organizationService.create(getTestMetaOrgUserContext(), getTestOrg());
|
|
111
113
|
if (!createdTestOrg) {
|
|
112
|
-
throw new Error(
|
|
114
|
+
throw new Error("Failed to create test organization");
|
|
113
115
|
}
|
|
114
116
|
setTestOrgId(createdTestOrg._id);
|
|
115
117
|
}
|
|
@@ -118,25 +120,25 @@ async function createTestUsers() {
|
|
|
118
120
|
}
|
|
119
121
|
const createdTestOrgUserPerson = await personService.create(getTestOrgUserContext(), getTestOrgUserPerson());
|
|
120
122
|
if (!createdTestOrgUserPerson) {
|
|
121
|
-
throw new Error(
|
|
123
|
+
throw new Error("Failed to create test organization user person");
|
|
122
124
|
}
|
|
123
125
|
setTestOrgUserPersonId(createdTestOrgUserPerson._id);
|
|
124
126
|
const createdMetaOrgUserPerson = await personService.create(getTestMetaOrgUserContext(), getTestMetaOrgUserPerson());
|
|
125
127
|
if (!createdMetaOrgUserPerson) {
|
|
126
|
-
throw new Error(
|
|
128
|
+
throw new Error("Failed to create meta organization user person");
|
|
127
129
|
}
|
|
128
130
|
setTestMetaOrgUserPersonId(createdMetaOrgUserPerson._id);
|
|
129
131
|
const createdTestOrgUser = await authService.createUser(getTestOrgUserContext(), getTestOrgUser());
|
|
130
132
|
const createdMetaOrgUser = await authService.createUser(getTestMetaOrgUserContext(), getTestMetaOrgUser());
|
|
131
133
|
if (!createdTestOrgUser || !createdMetaOrgUser) {
|
|
132
|
-
throw new Error(
|
|
134
|
+
throw new Error("Failed to create test user");
|
|
133
135
|
}
|
|
134
136
|
setTestMetaOrgUserId(createdMetaOrgUser._id);
|
|
135
137
|
setTestOrgUserId(createdTestOrgUser._id);
|
|
136
138
|
return { metaOrgUser: createdMetaOrgUser, testOrgUser: createdTestOrgUser };
|
|
137
139
|
}
|
|
138
140
|
catch (error) {
|
|
139
|
-
console.log(
|
|
141
|
+
console.log("Error in createTestUser:", error);
|
|
140
142
|
throw error;
|
|
141
143
|
}
|
|
142
144
|
}
|
|
@@ -144,34 +146,38 @@ async function deleteTestUser() {
|
|
|
144
146
|
if (!authService || !organizationService) {
|
|
145
147
|
return;
|
|
146
148
|
}
|
|
147
|
-
await authService
|
|
149
|
+
await authService
|
|
150
|
+
.deleteById(getTestMetaOrgUserContext(), getTestMetaOrgUser()._id)
|
|
151
|
+
.catch((error) => {
|
|
148
152
|
return null;
|
|
149
153
|
});
|
|
150
|
-
await organizationService
|
|
154
|
+
await organizationService
|
|
155
|
+
.deleteById(getTestMetaOrgUserContext(), getTestOrg()._id)
|
|
156
|
+
.catch((error) => {
|
|
151
157
|
return null;
|
|
152
158
|
});
|
|
153
159
|
}
|
|
154
160
|
async function simulateloginWithTestUser() {
|
|
155
161
|
const req = {
|
|
156
|
-
cookies: {}
|
|
162
|
+
cookies: {},
|
|
157
163
|
};
|
|
158
164
|
if (deviceIdCookie) {
|
|
159
|
-
req.cookies[
|
|
165
|
+
req.cookies["deviceId"] = deviceIdCookie;
|
|
160
166
|
}
|
|
161
167
|
const res = {
|
|
162
168
|
cookie: function (name, value) {
|
|
163
|
-
if (name ===
|
|
169
|
+
if (name === "deviceId") {
|
|
164
170
|
deviceIdCookie = value;
|
|
165
171
|
}
|
|
166
172
|
return res;
|
|
167
|
-
}
|
|
173
|
+
},
|
|
168
174
|
};
|
|
169
175
|
if (!authService) {
|
|
170
|
-
throw new Error(
|
|
176
|
+
throw new Error("AuthService not initialized. Call initialize() first.");
|
|
171
177
|
}
|
|
172
178
|
const loginResponse = await authService.attemptLogin(req, res, getTestMetaOrgUser().email, testObjectsModule.TEST_META_ORG_USER_PASSWORD);
|
|
173
179
|
if (!loginResponse?.tokens?.accessToken) {
|
|
174
|
-
throw new Error(
|
|
180
|
+
throw new Error("Failed to login with test user");
|
|
175
181
|
}
|
|
176
182
|
return `Bearer ${loginResponse.tokens.accessToken}`;
|
|
177
183
|
}
|
|
@@ -185,13 +191,13 @@ function verifyToken(token) {
|
|
|
185
191
|
}
|
|
186
192
|
export class CategoryService extends GenericApiService {
|
|
187
193
|
constructor(database) {
|
|
188
|
-
super(database,
|
|
194
|
+
super(database, "categories", "category", CategorySpec);
|
|
189
195
|
}
|
|
190
196
|
}
|
|
191
197
|
export class CategoryController extends ApiController {
|
|
192
198
|
constructor(app, database) {
|
|
193
199
|
const categoryService = new CategoryService(database);
|
|
194
|
-
super(
|
|
200
|
+
super("categories", app, categoryService, "category", CategorySpec);
|
|
195
201
|
}
|
|
196
202
|
}
|
|
197
203
|
export function setupTestConfig(isMultiTenant = true, dbType) {
|
|
@@ -199,37 +205,37 @@ export function setupTestConfig(isMultiTenant = true, dbType) {
|
|
|
199
205
|
app: {
|
|
200
206
|
isMultiTenant: isMultiTenant,
|
|
201
207
|
isAuthEnabled: true,
|
|
202
|
-
name:
|
|
208
|
+
name: "test-app",
|
|
203
209
|
dbType: dbType,
|
|
204
210
|
},
|
|
205
211
|
auth: {
|
|
206
|
-
clientSecret:
|
|
212
|
+
clientSecret: "test-secret",
|
|
207
213
|
saltWorkFactor: 10,
|
|
208
214
|
jwtExpirationInSeconds: 3600,
|
|
209
215
|
refreshTokenExpirationInDays: 7,
|
|
210
216
|
deviceIdCookieMaxAgeInDays: 730,
|
|
211
|
-
passwordResetTokenExpirationInMinutes: 20
|
|
217
|
+
passwordResetTokenExpirationInMinutes: 20,
|
|
212
218
|
},
|
|
213
219
|
database: {
|
|
214
|
-
name:
|
|
215
|
-
host:
|
|
216
|
-
password:
|
|
220
|
+
name: "test-db",
|
|
221
|
+
host: "localhost",
|
|
222
|
+
password: "test-password",
|
|
217
223
|
port: 27017,
|
|
218
|
-
username:
|
|
224
|
+
username: "test-user",
|
|
219
225
|
},
|
|
220
|
-
env:
|
|
226
|
+
env: "test",
|
|
221
227
|
email: {
|
|
222
|
-
fromAddress:
|
|
223
|
-
systemEmailAddress:
|
|
228
|
+
fromAddress: "test@test.com",
|
|
229
|
+
systemEmailAddress: "system@test.com",
|
|
224
230
|
},
|
|
225
231
|
thirdPartyClients: {
|
|
226
|
-
emailClient: new TestEmailClient()
|
|
232
|
+
emailClient: new TestEmailClient(),
|
|
227
233
|
},
|
|
228
234
|
network: {
|
|
229
|
-
hostName:
|
|
235
|
+
hostName: "localhost",
|
|
230
236
|
internalPort: 8083,
|
|
231
237
|
externalPort: 4000,
|
|
232
|
-
corsAllowedOrigins: [
|
|
238
|
+
corsAllowedOrigins: ["*"],
|
|
233
239
|
},
|
|
234
240
|
});
|
|
235
241
|
}
|
|
@@ -238,54 +244,54 @@ const prepareQueryCustom = (userContext, queryObject, operations) => {
|
|
|
238
244
|
queryObject: queryObject,
|
|
239
245
|
operations: [
|
|
240
246
|
...operations,
|
|
241
|
-
new LeftJoin(
|
|
242
|
-
]
|
|
247
|
+
new LeftJoin("categories", "category_id", "_id", "category"),
|
|
248
|
+
],
|
|
243
249
|
};
|
|
244
250
|
};
|
|
245
251
|
const postProcessEntityCustom = (userContext, entity) => {
|
|
246
252
|
return {
|
|
247
253
|
...entity,
|
|
248
|
-
category: entity._joinData?.category
|
|
254
|
+
category: entity._joinData?.category,
|
|
249
255
|
};
|
|
250
256
|
};
|
|
251
257
|
export class ProductService extends GenericApiService {
|
|
252
258
|
constructor(database) {
|
|
253
|
-
super(database,
|
|
259
|
+
super(database, "products", "product", ProductSpec);
|
|
254
260
|
}
|
|
255
261
|
}
|
|
256
262
|
export class ProductsController extends ApiController {
|
|
257
263
|
constructor(app, database) {
|
|
258
264
|
const productService = new ProductService(database);
|
|
259
|
-
super(
|
|
265
|
+
super("products", app, productService, "product", ProductSpec);
|
|
260
266
|
}
|
|
261
267
|
async get(req, res, next) {
|
|
262
|
-
res.set(
|
|
268
|
+
res.set("Content-Type", "application/json");
|
|
263
269
|
const userContext = req.userContext;
|
|
264
270
|
if (!userContext) {
|
|
265
|
-
throw new Error(
|
|
271
|
+
throw new Error("User context not found");
|
|
266
272
|
}
|
|
267
273
|
const queryOptions = apiUtils.getQueryOptionsFromRequest(req);
|
|
268
274
|
const pagedResult = await this.service.get(userContext, queryOptions, prepareQueryCustom, postProcessEntityCustom);
|
|
269
275
|
apiUtils.apiResponse(res, 200, { data: pagedResult }, ProductWithCategorySpec, ProductWithCategoryPublicSpec);
|
|
270
276
|
}
|
|
271
277
|
async getAll(req, res, next) {
|
|
272
|
-
res.set(
|
|
278
|
+
res.set("Content-Type", "application/json");
|
|
273
279
|
const userContext = req.userContext;
|
|
274
280
|
if (!userContext) {
|
|
275
|
-
throw new Error(
|
|
281
|
+
throw new Error("User context not found");
|
|
276
282
|
}
|
|
277
283
|
const entities = await this.service.getAll(userContext, prepareQueryCustom, postProcessEntityCustom);
|
|
278
284
|
apiUtils.apiResponse(res, 200, { data: entities }, ProductWithCategorySpec, ProductWithCategoryPublicSpec);
|
|
279
285
|
}
|
|
280
286
|
async getById(req, res, next) {
|
|
281
|
-
res.set(
|
|
287
|
+
res.set("Content-Type", "application/json");
|
|
282
288
|
const userContext = req.userContext;
|
|
283
289
|
if (!userContext) {
|
|
284
|
-
throw new Error(
|
|
290
|
+
throw new Error("User context not found");
|
|
285
291
|
}
|
|
286
292
|
const idParam = req.params?.id;
|
|
287
293
|
if (!idParam) {
|
|
288
|
-
throw new Error(
|
|
294
|
+
throw new Error("ID parameter is required");
|
|
289
295
|
}
|
|
290
296
|
try {
|
|
291
297
|
const id = Value.Convert(this.idSchema, idParam);
|
|
@@ -300,13 +306,13 @@ export class ProductsController extends ApiController {
|
|
|
300
306
|
export class MultiTenantProductService extends MultiTenantApiService {
|
|
301
307
|
db;
|
|
302
308
|
constructor(database) {
|
|
303
|
-
super(database,
|
|
309
|
+
super(database, "products", "product", ProductSpec);
|
|
304
310
|
this.db = database;
|
|
305
311
|
}
|
|
306
312
|
prepareQuery(userContext, queryObject, operations) {
|
|
307
313
|
const newOperations = [
|
|
308
314
|
...operations,
|
|
309
|
-
new LeftJoin(
|
|
315
|
+
new LeftJoin("categories", "categoryId", "_id", "category"),
|
|
310
316
|
];
|
|
311
317
|
return super.prepareQuery(userContext, queryObject, newOperations);
|
|
312
318
|
}
|
|
@@ -324,12 +330,14 @@ export class MultiTenantProductsController extends ApiController {
|
|
|
324
330
|
const AggregatedProductSchema = Type.Intersect([
|
|
325
331
|
ProductSpec.fullSchema,
|
|
326
332
|
Type.Partial(Type.Object({
|
|
327
|
-
category: CategorySpec.fullSchema
|
|
328
|
-
}))
|
|
333
|
+
category: CategorySpec.fullSchema,
|
|
334
|
+
})),
|
|
335
|
+
]);
|
|
336
|
+
const PublicAggregatedProductSchema = Type.Omit(AggregatedProductSchema, [
|
|
337
|
+
"internalNumber",
|
|
329
338
|
]);
|
|
330
|
-
const PublicAggregatedProductSchema = Type.Omit(AggregatedProductSchema, ['internalNumber']);
|
|
331
339
|
const PublicAggregatedProductSpec = entityUtils.getModelSpec(PublicAggregatedProductSchema);
|
|
332
|
-
super(
|
|
340
|
+
super("multi-tenant-products", app, productService, "product", ProductSpec, PublicAggregatedProductSpec);
|
|
333
341
|
}
|
|
334
342
|
}
|
|
335
343
|
function configureJwtSecret() {
|
|
@@ -339,17 +347,15 @@ function configureJwtSecret() {
|
|
|
339
347
|
};
|
|
340
348
|
}
|
|
341
349
|
async function loginWithTestUser(agent) {
|
|
342
|
-
agent.set(
|
|
350
|
+
agent.set("Cookie", [`deviceId=${deviceIdCookie}`]);
|
|
343
351
|
const testUser = getTestMetaOrgUser();
|
|
344
|
-
const response = await agent
|
|
345
|
-
.post('/api/auth/login')
|
|
346
|
-
.send({
|
|
352
|
+
const response = await agent.post("/api/auth/login").send({
|
|
347
353
|
email: testUser.email,
|
|
348
354
|
password: testUser.password,
|
|
349
355
|
});
|
|
350
356
|
if (!response.body?.data?.tokens?.accessToken) {
|
|
351
|
-
console.error(
|
|
352
|
-
throw new Error(
|
|
357
|
+
console.error("Login failed:", response.body);
|
|
358
|
+
throw new Error("Failed to login with test user");
|
|
353
359
|
}
|
|
354
360
|
const authorizationHeaderValue = `Bearer ${response.body?.data?.tokens?.accessToken}`;
|
|
355
361
|
return authorizationHeaderValue;
|
|
@@ -360,7 +366,7 @@ async function cleanup() {
|
|
|
360
366
|
await deleteMetaOrg();
|
|
361
367
|
}
|
|
362
368
|
catch (error) {
|
|
363
|
-
console.log(
|
|
369
|
+
console.log("Error during cleanup:", error);
|
|
364
370
|
}
|
|
365
371
|
}
|
|
366
372
|
const testUtils = {
|
|
@@ -382,6 +388,6 @@ const testUtils = {
|
|
|
382
388
|
verifyToken,
|
|
383
389
|
isMongoDatabase,
|
|
384
390
|
isPostgresDatabase,
|
|
385
|
-
getExpectedIdType
|
|
391
|
+
getExpectedIdType,
|
|
386
392
|
};
|
|
387
393
|
export default testUtils;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { IDatabase } from "../databases/models/index.js";
|
|
2
|
+
import type { ITestDatabase } from "./test-database.interface.js";
|
|
3
3
|
export declare class TestPostgresDatabase implements ITestDatabase {
|
|
4
4
|
private database;
|
|
5
5
|
private postgresClient;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Pool } from "pg";
|
|
2
2
|
import { newDb } from "pg-mem";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
const USE_REAL_POSTGRES = process.env.USE_REAL_POSTGRES ===
|
|
3
|
+
import { config } from "../config/base-api-config.js";
|
|
4
|
+
import { runInitialSchemaMigrations, runTestSchemaMigrations, } from "../databases/postgres/migrations/__tests__/test-migration-helper.js";
|
|
5
|
+
import { PostgresDatabase } from "../databases/postgres/postgres.database.js";
|
|
6
|
+
import testUtils from "./common-test.utils.js";
|
|
7
|
+
const USE_REAL_POSTGRES = process.env.USE_REAL_POSTGRES === "true";
|
|
8
8
|
export class TestPostgresDatabase {
|
|
9
9
|
database = null;
|
|
10
10
|
postgresClient = null;
|
|
@@ -18,7 +18,7 @@ export class TestPostgresDatabase {
|
|
|
18
18
|
return this.initPromise;
|
|
19
19
|
}
|
|
20
20
|
getRandomId() {
|
|
21
|
-
throw new Error(
|
|
21
|
+
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.");
|
|
22
22
|
}
|
|
23
23
|
async _performInit() {
|
|
24
24
|
if (!this.database) {
|
|
@@ -32,13 +32,14 @@ export class TestPostgresDatabase {
|
|
|
32
32
|
});
|
|
33
33
|
this.migrationPool = pool;
|
|
34
34
|
try {
|
|
35
|
-
await pool.query(
|
|
35
|
+
await pool.query("SELECT now(), current_database()");
|
|
36
36
|
}
|
|
37
37
|
catch (error) {
|
|
38
38
|
await pool.end();
|
|
39
39
|
this.migrationPool = null;
|
|
40
40
|
const errorMessage = error.message || String(error);
|
|
41
|
-
const isPermissionError = errorMessage.includes(
|
|
41
|
+
const isPermissionError = errorMessage.includes("permission denied") ||
|
|
42
|
+
errorMessage.includes("operation not permitted");
|
|
42
43
|
if (isPermissionError) {
|
|
43
44
|
throw new Error(`Docker permission error. Please ensure:\n` +
|
|
44
45
|
`1. Docker Desktop is running\n` +
|
|
@@ -64,9 +65,9 @@ export class TestPostgresDatabase {
|
|
|
64
65
|
}
|
|
65
66
|
this.database = new PostgresDatabase(connection);
|
|
66
67
|
this.postgresClient = connection;
|
|
67
|
-
const { initializeSystemUserContext, isSystemUserContextInitialized } = await import(
|
|
68
|
+
const { initializeSystemUserContext, isSystemUserContextInitialized } = await import("@loomcore/common/models");
|
|
68
69
|
if (!isSystemUserContextInitialized()) {
|
|
69
|
-
initializeSystemUserContext(config.email?.systemEmailAddress ||
|
|
70
|
+
initializeSystemUserContext(config.email?.systemEmailAddress || "system@test.com", undefined);
|
|
70
71
|
}
|
|
71
72
|
await runInitialSchemaMigrations(pool, config);
|
|
72
73
|
await runTestSchemaMigrations(pool, config);
|
|
@@ -84,12 +85,12 @@ export class TestPostgresDatabase {
|
|
|
84
85
|
`);
|
|
85
86
|
}
|
|
86
87
|
catch (error) {
|
|
87
|
-
console.log(
|
|
88
|
+
console.log("Note: Indexes may be created later when tables are initialized:", error.message);
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
91
|
async clearCollections() {
|
|
91
92
|
if (!this.postgresClient) {
|
|
92
|
-
throw new Error(
|
|
93
|
+
throw new Error("Database not initialized");
|
|
93
94
|
}
|
|
94
95
|
const result = await this.postgresClient.query(`
|
|
95
96
|
SELECT "table_name"
|
|
@@ -98,7 +99,7 @@ export class TestPostgresDatabase {
|
|
|
98
99
|
AND "table_type" = 'BASE TABLE'
|
|
99
100
|
`);
|
|
100
101
|
if (USE_REAL_POSTGRES) {
|
|
101
|
-
await this.postgresClient.query(`TRUNCATE TABLE ${result.rows.map(row => `"${row.table_name}"`).join(
|
|
102
|
+
await this.postgresClient.query(`TRUNCATE TABLE ${result.rows.map((row) => `"${row.table_name}"`).join(", ")} RESTART IDENTITY CASCADE`);
|
|
102
103
|
}
|
|
103
104
|
else {
|
|
104
105
|
result.rows.forEach(async (row) => {
|
|
@@ -120,7 +121,7 @@ export class TestPostgresDatabase {
|
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
123
|
catch (error) {
|
|
123
|
-
console.warn(
|
|
124
|
+
console.warn("Error closing PostgreSQL connection:", error);
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
this.migrationPool = null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { IDatabase } from "../databases/models/index.js";
|
|
2
|
+
import { IBaseApiConfig } from "../models/index.js";
|
|
3
3
|
export declare let config: IBaseApiConfig;
|
|
4
4
|
export declare function setBaseApiConfig(theConfig: IBaseApiConfig): void;
|
|
5
5
|
export declare function initSystemUserContext(database: IDatabase): Promise<void>;
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import { EmptyUserContext, initializeSystemUserContext } from
|
|
1
|
+
import { EmptyUserContext, initializeSystemUserContext, } from "@loomcore/common/models";
|
|
2
2
|
export let config;
|
|
3
3
|
let isConfigSet = false;
|
|
4
4
|
let isSystemUserContextSet = false;
|
|
5
5
|
const BASE_API_CONFIG_KEYS = [
|
|
6
|
-
|
|
6
|
+
"app",
|
|
7
|
+
"auth",
|
|
8
|
+
"database",
|
|
9
|
+
"debug",
|
|
10
|
+
"email",
|
|
11
|
+
"env",
|
|
12
|
+
"network",
|
|
13
|
+
"thirdPartyClients",
|
|
7
14
|
];
|
|
8
15
|
function copyOnlySpecifiedConfigProperties(obj, allowedKeys) {
|
|
9
16
|
const result = {};
|
|
@@ -19,29 +26,29 @@ export function setBaseApiConfig(theConfig) {
|
|
|
19
26
|
config = copyOnlySpecifiedConfigProperties(theConfig, BASE_API_CONFIG_KEYS);
|
|
20
27
|
isConfigSet = true;
|
|
21
28
|
}
|
|
22
|
-
else if (config.env !==
|
|
23
|
-
console.warn(
|
|
29
|
+
else if (config.env !== "test") {
|
|
30
|
+
console.warn("BaseApiConfig data has already been set. Ignoring subsequent calls to setBaseApiConfig.");
|
|
24
31
|
}
|
|
25
32
|
}
|
|
26
33
|
export async function initSystemUserContext(database) {
|
|
27
34
|
if (!isConfigSet) {
|
|
28
|
-
throw new Error(
|
|
35
|
+
throw new Error("BaseApiConfig has not been set. Call setBaseApiConfig first.");
|
|
29
36
|
}
|
|
30
37
|
if (!isSystemUserContextSet) {
|
|
31
|
-
const systemEmail = config.email?.systemEmailAddress ||
|
|
38
|
+
const systemEmail = config.email?.systemEmailAddress || "system@example.com";
|
|
32
39
|
let metaOrg = undefined;
|
|
33
40
|
if (config.app.isMultiTenant) {
|
|
34
|
-
const { OrganizationService } = await import(
|
|
41
|
+
const { OrganizationService } = await import("../services/organization.service.js");
|
|
35
42
|
const organizationService = new OrganizationService(database);
|
|
36
43
|
metaOrg = await organizationService.getMetaOrg(EmptyUserContext);
|
|
37
44
|
if (!metaOrg) {
|
|
38
|
-
throw new Error(
|
|
45
|
+
throw new Error("Meta organization not found. Please create an organization with isMetaOrg=true before starting the API.");
|
|
39
46
|
}
|
|
40
47
|
}
|
|
41
48
|
initializeSystemUserContext(systemEmail, metaOrg);
|
|
42
49
|
isSystemUserContextSet = true;
|
|
43
50
|
}
|
|
44
|
-
else if (config.env !==
|
|
45
|
-
console.warn(
|
|
51
|
+
else if (config.env !== "test") {
|
|
52
|
+
console.warn("SystemUserContext has already been set. Ignoring subsequent calls to initSystemUserContext.");
|
|
46
53
|
}
|
|
47
54
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Db } from
|
|
2
|
-
import { IInitialDbMigrationConfig } from
|
|
1
|
+
import { Db } from "mongodb";
|
|
2
|
+
import { IInitialDbMigrationConfig } from "../../../models/initial-database-config.interface.js";
|
|
3
3
|
export interface ISyntheticMigration {
|
|
4
4
|
name: string;
|
|
5
5
|
up: (context: {
|