@loomcore/api 0.0.20 → 0.0.22
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 +7 -1
- package/dist/__tests__/common-test.utils.js +53 -4
- package/dist/__tests__/test-express-app.js +4 -1
- package/dist/config/base-api-config.d.ts +2 -0
- package/dist/config/base-api-config.js +22 -0
- package/dist/models/base-api-config.interface.d.ts +1 -0
- package/dist/services/auth.service.js +5 -4
- package/dist/services/generic-api-service.interface.d.ts +3 -0
- package/dist/services/generic-api.service.d.ts +1 -0
- package/dist/services/generic-api.service.js +23 -7
- package/dist/services/organization.service.d.ts +1 -0
- package/dist/services/organization.service.js +4 -0
- package/package.json +2 -2
|
@@ -2,6 +2,8 @@ import { Db, ObjectId } from 'mongodb';
|
|
|
2
2
|
import { IUser, IUserContext } from '@loomcore/common/models';
|
|
3
3
|
declare function initialize(database: Db): void;
|
|
4
4
|
declare function createIndexes(db: Db): Promise<void>;
|
|
5
|
+
declare function createMetaOrg(): Promise<void>;
|
|
6
|
+
declare function deleteMetaOrg(): Promise<void>;
|
|
5
7
|
declare function setupTestUser(): Promise<{
|
|
6
8
|
_id: ObjectId;
|
|
7
9
|
email: string;
|
|
@@ -12,17 +14,21 @@ declare function setupTestUser(): Promise<{
|
|
|
12
14
|
_updated: Date;
|
|
13
15
|
_updatedBy: string;
|
|
14
16
|
}>;
|
|
15
|
-
declare function deleteTestUser(): Promise<
|
|
17
|
+
declare function deleteTestUser(): Promise<any[]>;
|
|
16
18
|
declare function simulateloginWithTestUser(): Promise<string>;
|
|
17
19
|
declare function getAuthToken(): string;
|
|
18
20
|
declare function verifyToken(token: string): any;
|
|
19
21
|
declare function getTestUser(): Partial<IUser>;
|
|
20
22
|
declare function configureJwtSecret(): void;
|
|
21
23
|
declare function loginWithTestUser(agent: any): Promise<string>;
|
|
24
|
+
declare function cleanup(): Promise<void>;
|
|
22
25
|
declare const testUtils: {
|
|
26
|
+
cleanup: typeof cleanup;
|
|
23
27
|
configureJwtSecret: typeof configureJwtSecret;
|
|
24
28
|
constDeviceIdCookie: string;
|
|
25
29
|
createIndexes: typeof createIndexes;
|
|
30
|
+
createMetaOrg: typeof createMetaOrg;
|
|
31
|
+
deleteMetaOrg: typeof deleteMetaOrg;
|
|
26
32
|
deleteTestUser: typeof deleteTestUser;
|
|
27
33
|
getAuthToken: typeof getAuthToken;
|
|
28
34
|
getTestUser: typeof getTestUser;
|
|
@@ -43,9 +43,43 @@ async function createIndexes(db) {
|
|
|
43
43
|
createIndexes: "users", indexes: [{ key: { email: 1 }, name: 'email_index', unique: true, collation: { locale: 'en', strength: 1 } }]
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
|
+
async function createMetaOrg() {
|
|
47
|
+
if (!db || !collections.organizations) {
|
|
48
|
+
throw new Error('Database not initialized. Call initialize() first.');
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const existingMetaOrg = await collections.organizations.findOne({ isMetaOrg: true });
|
|
52
|
+
if (!existingMetaOrg) {
|
|
53
|
+
const metaOrgInsertResult = await collections.organizations.insertOne({
|
|
54
|
+
_id: new ObjectId(),
|
|
55
|
+
name: 'Meta Organization',
|
|
56
|
+
isMetaOrg: true,
|
|
57
|
+
_created: new Date(),
|
|
58
|
+
_createdBy: 'system',
|
|
59
|
+
_updated: new Date(),
|
|
60
|
+
_updatedBy: 'system'
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
console.log('Error in createMetaOrg:', error);
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function deleteMetaOrg() {
|
|
70
|
+
if (!collections.organizations) {
|
|
71
|
+
return Promise.resolve();
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
await collections.organizations.deleteOne({ isMetaOrg: true });
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
console.log('Error deleting meta org:', error);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
46
80
|
async function setupTestUser() {
|
|
47
81
|
try {
|
|
48
|
-
|
|
82
|
+
await deleteTestUser();
|
|
49
83
|
return createTestUser();
|
|
50
84
|
}
|
|
51
85
|
catch (error) {
|
|
@@ -91,11 +125,14 @@ async function createTestUser() {
|
|
|
91
125
|
}
|
|
92
126
|
}
|
|
93
127
|
function deleteTestUser() {
|
|
94
|
-
let
|
|
128
|
+
let promises = [];
|
|
95
129
|
if (testUser) {
|
|
96
|
-
|
|
130
|
+
promises.push(collections.users.deleteOne({ _id: testUser._id }));
|
|
97
131
|
}
|
|
98
|
-
|
|
132
|
+
if (collections.organizations) {
|
|
133
|
+
promises.push(collections.organizations.deleteOne({ _id: new ObjectId(testOrgId) }));
|
|
134
|
+
}
|
|
135
|
+
return Promise.all(promises);
|
|
99
136
|
}
|
|
100
137
|
async function simulateloginWithTestUser() {
|
|
101
138
|
const req = {
|
|
@@ -156,10 +193,22 @@ async function loginWithTestUser(agent) {
|
|
|
156
193
|
const authorizationHeaderValue = `Bearer ${response.body?.data?.tokens?.accessToken}`;
|
|
157
194
|
return authorizationHeaderValue;
|
|
158
195
|
}
|
|
196
|
+
async function cleanup() {
|
|
197
|
+
try {
|
|
198
|
+
await deleteTestUser();
|
|
199
|
+
await deleteMetaOrg();
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
console.log('Error during cleanup:', error);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
159
205
|
const testUtils = {
|
|
206
|
+
cleanup,
|
|
160
207
|
configureJwtSecret,
|
|
161
208
|
constDeviceIdCookie,
|
|
162
209
|
createIndexes,
|
|
210
|
+
createMetaOrg,
|
|
211
|
+
deleteMetaOrg,
|
|
163
212
|
deleteTestUser,
|
|
164
213
|
getAuthToken,
|
|
165
214
|
getTestUser,
|
|
@@ -6,7 +6,7 @@ import { MongoMemoryServer } from 'mongodb-memory-server';
|
|
|
6
6
|
import { MongoClient } from 'mongodb';
|
|
7
7
|
import { initializeTypeBox } from '@loomcore/common/validation';
|
|
8
8
|
import testUtils from './common-test.utils.js';
|
|
9
|
-
import { setBaseApiConfig } from '../config/base-api-config.js';
|
|
9
|
+
import { setBaseApiConfig, initSystemUserContext } from '../config/base-api-config.js';
|
|
10
10
|
import { errorHandler } from '../middleware/error-handler.js';
|
|
11
11
|
import { ensureUserContext } from '../middleware/ensure-user-context.js';
|
|
12
12
|
export class TestExpressApp {
|
|
@@ -66,7 +66,9 @@ export class TestExpressApp {
|
|
|
66
66
|
this.db = this.client.db();
|
|
67
67
|
testUtils.initialize(this.db);
|
|
68
68
|
await testUtils.createIndexes(this.db);
|
|
69
|
+
await testUtils.createMetaOrg();
|
|
69
70
|
}
|
|
71
|
+
await initSystemUserContext(this.db);
|
|
70
72
|
if (!this.app) {
|
|
71
73
|
this.app = express();
|
|
72
74
|
this.app.use(bodyParser.json());
|
|
@@ -87,6 +89,7 @@ export class TestExpressApp {
|
|
|
87
89
|
this.app.use(errorHandler);
|
|
88
90
|
}
|
|
89
91
|
static async cleanup() {
|
|
92
|
+
await testUtils.cleanup();
|
|
90
93
|
if (this.client) {
|
|
91
94
|
await this.client.close();
|
|
92
95
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Db } from 'mongodb';
|
|
1
2
|
import { IBaseApiConfig } from '../models/index.js';
|
|
2
3
|
export declare let config: IBaseApiConfig;
|
|
3
4
|
export declare function setBaseApiConfig(apiConfig: IBaseApiConfig): void;
|
|
5
|
+
export declare function initSystemUserContext(db: Db): Promise<void>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { EmptyUserContext, initializeSystemUserContext } from '@loomcore/common/models';
|
|
1
2
|
export let config;
|
|
2
3
|
let isConfigSet = false;
|
|
4
|
+
let isSystemUserContextSet = false;
|
|
3
5
|
function copyOnlyBaseApiConfigProperties(obj) {
|
|
4
6
|
const baseConfig = {};
|
|
5
7
|
Object.keys(obj).forEach((key) => {
|
|
@@ -16,3 +18,23 @@ export function setBaseApiConfig(apiConfig) {
|
|
|
16
18
|
console.warn('BaseApiConfig data has already been set. Ignoring subsequent calls to setBaseApiConfig.');
|
|
17
19
|
}
|
|
18
20
|
}
|
|
21
|
+
export async function initSystemUserContext(db) {
|
|
22
|
+
if (!isConfigSet) {
|
|
23
|
+
throw new Error('BaseApiConfig has not been set. Call setBaseApiConfig first.');
|
|
24
|
+
}
|
|
25
|
+
if (!isSystemUserContextSet) {
|
|
26
|
+
const systemEmail = config.email.systemEmailAddress || 'system@example.com';
|
|
27
|
+
let metaOrgId = undefined;
|
|
28
|
+
if (config.app.isMultiTenant) {
|
|
29
|
+
const { OrganizationService } = await import('../services/organization.service.js');
|
|
30
|
+
const organizationService = new OrganizationService(db);
|
|
31
|
+
const metaOrg = await organizationService.getMetaOrg(EmptyUserContext);
|
|
32
|
+
metaOrgId = metaOrg._id;
|
|
33
|
+
}
|
|
34
|
+
initializeSystemUserContext(systemEmail, metaOrgId);
|
|
35
|
+
isSystemUserContextSet = true;
|
|
36
|
+
}
|
|
37
|
+
else if (config.env !== 'test') {
|
|
38
|
+
console.warn('SystemUserContext has already been set. Ignoring subsequent calls to initSystemUserContext.');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ObjectId } from 'mongodb';
|
|
2
2
|
import moment from 'moment';
|
|
3
3
|
import crypto from 'crypto';
|
|
4
|
-
import { EmptyUserContext, passwordValidator, UserSpec } from '@loomcore/common/models';
|
|
4
|
+
import { EmptyUserContext, passwordValidator, UserSpec, getSystemUserContext } from '@loomcore/common/models';
|
|
5
5
|
import { entityUtils } from '@loomcore/common/utils';
|
|
6
6
|
import { BadRequestError, ServerError } from '../errors/index.js';
|
|
7
7
|
import { JwtService, EmailService } from './index.js';
|
|
@@ -49,7 +49,7 @@ export class AuthService extends GenericApiService {
|
|
|
49
49
|
refreshToken: refreshTokenObject.token,
|
|
50
50
|
expiresOn: accessTokenExpiresOn
|
|
51
51
|
};
|
|
52
|
-
this.updateLastLoggedIn(userContext.user._id
|
|
52
|
+
this.updateLastLoggedIn(userContext.user._id)
|
|
53
53
|
.catch(err => console.log(`Error updating lastLoggedIn: ${err}`));
|
|
54
54
|
this.transformSingle(userContext.user);
|
|
55
55
|
loginResponse = { tokens: tokenResponse, userContext };
|
|
@@ -266,8 +266,9 @@ export class AuthService extends GenericApiService {
|
|
|
266
266
|
if (!entityUtils.isValidObjectId(userId)) {
|
|
267
267
|
throw new BadRequestError('userId is not a valid ObjectId');
|
|
268
268
|
}
|
|
269
|
-
const updates = { _lastLoggedIn: moment().utc().
|
|
270
|
-
|
|
269
|
+
const updates = { _lastLoggedIn: moment().utc().toISOString() };
|
|
270
|
+
const systemUserContext = getSystemUserContext();
|
|
271
|
+
await this.partialUpdateById(systemUserContext, userId, updates);
|
|
271
272
|
}
|
|
272
273
|
catch (error) {
|
|
273
274
|
console.log(`Failed to update lastLoggedIn for user ${userId}: ${error}`);
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { DeleteResult, Document, FindOptions } from 'mongodb';
|
|
2
|
+
import { ValueError } from '@sinclair/typebox/errors';
|
|
2
3
|
import { IUserContext, IEntity, IPagedResult, QueryOptions } from '@loomcore/common/models';
|
|
3
4
|
export interface IGenericApiService<T extends IEntity> {
|
|
5
|
+
validate(doc: any, isPartial?: boolean): ValueError[] | null;
|
|
6
|
+
validateMany(docs: any[], isPartial?: boolean): ValueError[] | null;
|
|
4
7
|
getAll(userContext: IUserContext): Promise<T[]>;
|
|
5
8
|
get(userContext: IUserContext, queryOptions: QueryOptions): Promise<IPagedResult<T>>;
|
|
6
9
|
getById(userContext: IUserContext, id: string): Promise<T>;
|
|
@@ -10,6 +10,7 @@ export declare class GenericApiService<T extends IEntity> implements IGenericApi
|
|
|
10
10
|
protected modelSpec?: IModelSpec;
|
|
11
11
|
constructor(db: Db, pluralResourceName: string, singularResourceName: string, modelSpec?: IModelSpec);
|
|
12
12
|
validate(doc: any, isPartial?: boolean): ValueError[] | null;
|
|
13
|
+
validateMany(docs: any[], isPartial?: boolean): ValueError[] | null;
|
|
13
14
|
protected getAdditionalPipelineStages(): any[];
|
|
14
15
|
protected createAggregationPipeline(userContext: IUserContext, query: any, queryOptions?: QueryOptions): any[];
|
|
15
16
|
getAll(userContext: IUserContext): Promise<T[]>;
|
|
@@ -25,6 +25,20 @@ export class GenericApiService {
|
|
|
25
25
|
const validator = isPartial ? this.modelSpec.partialValidator : this.modelSpec.validator;
|
|
26
26
|
return entityUtils.validate(validator, doc);
|
|
27
27
|
}
|
|
28
|
+
validateMany(docs, isPartial = false) {
|
|
29
|
+
if (!this.modelSpec) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const validator = isPartial ? this.modelSpec.partialValidator : this.modelSpec.validator;
|
|
33
|
+
let allErrors = [];
|
|
34
|
+
for (const doc of docs) {
|
|
35
|
+
const errors = entityUtils.validate(validator, doc);
|
|
36
|
+
if (errors && errors.length > 0) {
|
|
37
|
+
allErrors.push(...errors);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return allErrors.length > 0 ? allErrors : null;
|
|
41
|
+
}
|
|
28
42
|
getAdditionalPipelineStages() {
|
|
29
43
|
return [];
|
|
30
44
|
}
|
|
@@ -146,10 +160,8 @@ export class GenericApiService {
|
|
|
146
160
|
let createdEntities = [];
|
|
147
161
|
if (entities.length) {
|
|
148
162
|
try {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
entityUtils.handleValidationResult(validationErrors, 'GenericApiService.createMany');
|
|
152
|
-
}
|
|
163
|
+
const validationErrors = this.validateMany(entities);
|
|
164
|
+
entityUtils.handleValidationResult(validationErrors, 'GenericApiService.createMany');
|
|
153
165
|
const preparedEntities = await this.onBeforeCreate(userContext, entities);
|
|
154
166
|
const insertResult = await this.collection.insertMany(preparedEntities);
|
|
155
167
|
if (insertResult.insertedIds) {
|
|
@@ -325,9 +337,13 @@ export class GenericApiService {
|
|
|
325
337
|
const transformedEntity = dbUtils.convertObjectIdsToStrings(single, this.modelSpec.fullSchema);
|
|
326
338
|
return transformedEntity;
|
|
327
339
|
}
|
|
328
|
-
stripSenderProvidedSystemProperties(doc) {
|
|
340
|
+
stripSenderProvidedSystemProperties(userContext, doc) {
|
|
341
|
+
const isSystemUser = userContext.user?._id === 'system';
|
|
342
|
+
if (isSystemUser) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
329
345
|
for (const key in doc) {
|
|
330
|
-
if (Object.prototype.hasOwnProperty.call(doc, key) && key.startsWith('_')) {
|
|
346
|
+
if (Object.prototype.hasOwnProperty.call(doc, key) && key.startsWith('_') && key !== '_orgId') {
|
|
331
347
|
delete doc[key];
|
|
332
348
|
}
|
|
333
349
|
}
|
|
@@ -344,7 +360,7 @@ export class GenericApiService {
|
|
|
344
360
|
}
|
|
345
361
|
async prepareEntity(userContext, entity, isCreate) {
|
|
346
362
|
const preparedEntity = _.clone(entity);
|
|
347
|
-
this.stripSenderProvidedSystemProperties(preparedEntity);
|
|
363
|
+
this.stripSenderProvidedSystemProperties(userContext, preparedEntity);
|
|
348
364
|
if (this.modelSpec?.isAuditable) {
|
|
349
365
|
if (isCreate) {
|
|
350
366
|
this.auditForCreate(userContext, preparedEntity);
|
|
@@ -5,4 +5,5 @@ export declare class OrganizationService extends GenericApiService<IOrganization
|
|
|
5
5
|
constructor(db: Db);
|
|
6
6
|
getAuthTokenByRepoCode(userContext: IUserContext, orgId: string): Promise<string | null | undefined>;
|
|
7
7
|
validateRepoAuthToken(userContext: IUserContext, orgCode: string, authToken: string): Promise<string | null>;
|
|
8
|
+
getMetaOrg(userContext: IUserContext): Promise<IOrganization>;
|
|
8
9
|
}
|
|
@@ -13,4 +13,8 @@ export class OrganizationService extends GenericApiService {
|
|
|
13
13
|
const orgId = org.authToken === authToken ? org._id.toString() : null;
|
|
14
14
|
return orgId;
|
|
15
15
|
}
|
|
16
|
+
async getMetaOrg(userContext) {
|
|
17
|
+
const org = await this.findOne(userContext, { isMetaOrg: true });
|
|
18
|
+
return org;
|
|
19
|
+
}
|
|
16
20
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loomcore/api",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.22",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Loom Core Api - An opinionated Node.js api using Typescript, Express, and MongoDb",
|
|
6
6
|
"scripts": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"jsonwebtoken": "^9.0.2"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"@loomcore/common": "^0.0.
|
|
47
|
+
"@loomcore/common": "^0.0.12",
|
|
48
48
|
"@sinclair/typebox": "^0.34.31",
|
|
49
49
|
"cookie-parser": "^1.4.6",
|
|
50
50
|
"cors": "^2.8.5",
|