@loomcore/api 0.0.21 → 0.0.23
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/controllers/api.controller.d.ts +7 -1
- package/dist/controllers/api.controller.js +21 -3
- package/dist/controllers/auth.controller.js +8 -2
- package/dist/models/base-api-config.interface.d.ts +1 -0
- package/dist/services/auth.service.js +5 -6
- package/dist/services/generic-api-service.interface.d.ts +4 -0
- package/dist/services/generic-api.service.d.ts +5 -2
- package/dist/services/generic-api.service.js +21 -32
- 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,6 +1,6 @@
|
|
|
1
1
|
import { Application, NextFunction, Request, Response } from 'express';
|
|
2
2
|
import { TSchema } from '@sinclair/typebox';
|
|
3
|
-
import { IEntity, IModelSpec } from '@loomcore/common/models';
|
|
3
|
+
import { IEntity, IModelSpec, IUserContext } from '@loomcore/common/models';
|
|
4
4
|
import { IGenericApiService } from '../services/index.js';
|
|
5
5
|
export declare abstract class ApiController<T extends IEntity> {
|
|
6
6
|
protected app: Application;
|
|
@@ -11,6 +11,12 @@ export declare abstract class ApiController<T extends IEntity> {
|
|
|
11
11
|
protected publicSchema?: TSchema;
|
|
12
12
|
protected constructor(slug: string, app: Application, service: IGenericApiService<T>, resourceName?: string, modelSpec?: IModelSpec, publicSchema?: TSchema);
|
|
13
13
|
mapRoutes(app: Application): void;
|
|
14
|
+
protected validate(entity: any, isPartial?: boolean): void;
|
|
15
|
+
protected validateMany(entities: any[], isPartial?: boolean): void;
|
|
16
|
+
protected prepareDataForDb(userContext: IUserContext, entity: T, isCreate?: boolean): Promise<T>;
|
|
17
|
+
protected prepareDataForDb(userContext: IUserContext, entity: Partial<T>, isCreate?: boolean): Promise<Partial<T>>;
|
|
18
|
+
protected prepareDataForDb(userContext: IUserContext, entity: T[], isCreate?: boolean): Promise<T[]>;
|
|
19
|
+
protected prepareDataForDb(userContext: IUserContext, entity: Partial<T>[], isCreate?: boolean): Promise<Partial<T>[]>;
|
|
14
20
|
getAll(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
15
21
|
get(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
16
22
|
getById(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { entityUtils } from '@loomcore/common/utils';
|
|
1
2
|
import { isAuthenticated } from '../middleware/index.js';
|
|
2
3
|
import { apiUtils } from '../utils/index.js';
|
|
3
4
|
export class ApiController {
|
|
@@ -26,6 +27,17 @@ export class ApiController {
|
|
|
26
27
|
app.patch(`/api/${this.slug}/:id`, isAuthenticated, this.partialUpdateById.bind(this));
|
|
27
28
|
app.delete(`/api/${this.slug}/:id`, isAuthenticated, this.deleteById.bind(this));
|
|
28
29
|
}
|
|
30
|
+
validate(entity, isPartial = false) {
|
|
31
|
+
const validationErrors = this.service.validate(entity, isPartial);
|
|
32
|
+
entityUtils.handleValidationResult(validationErrors, `ApiController.validate for ${this.slug}`);
|
|
33
|
+
}
|
|
34
|
+
validateMany(entities, isPartial = false) {
|
|
35
|
+
const validationErrors = this.service.validateMany(entities, isPartial);
|
|
36
|
+
entityUtils.handleValidationResult(validationErrors, `ApiController.validateMany for ${this.slug}`);
|
|
37
|
+
}
|
|
38
|
+
async prepareDataForDb(userContext, entity, isCreate = false) {
|
|
39
|
+
return await this.service.prepareDataForDb(userContext, entity, isCreate);
|
|
40
|
+
}
|
|
29
41
|
async getAll(req, res, next) {
|
|
30
42
|
res.set('Content-Type', 'application/json');
|
|
31
43
|
const entities = await this.service.getAll(req.userContext);
|
|
@@ -50,17 +62,23 @@ export class ApiController {
|
|
|
50
62
|
}
|
|
51
63
|
async create(req, res, next) {
|
|
52
64
|
res.set('Content-Type', 'application/json');
|
|
53
|
-
|
|
65
|
+
this.validate(req.body);
|
|
66
|
+
const preparedEntity = await this.prepareDataForDb(req.userContext, req.body, true);
|
|
67
|
+
const entity = await this.service.create(req.userContext, preparedEntity);
|
|
54
68
|
apiUtils.apiResponse(res, 201, { data: entity || undefined }, this.modelSpec, this.publicSchema);
|
|
55
69
|
}
|
|
56
70
|
async fullUpdateById(req, res, next) {
|
|
57
71
|
res.set('Content-Type', 'application/json');
|
|
58
|
-
|
|
72
|
+
this.validate(req.body);
|
|
73
|
+
const preparedEntity = await this.prepareDataForDb(req.userContext, req.body, false);
|
|
74
|
+
const updateResult = await this.service.fullUpdateById(req.userContext, req.params.id, preparedEntity);
|
|
59
75
|
apiUtils.apiResponse(res, 200, { data: updateResult }, this.modelSpec, this.publicSchema);
|
|
60
76
|
}
|
|
61
77
|
async partialUpdateById(req, res, next) {
|
|
62
78
|
res.set('Content-Type', 'application/json');
|
|
63
|
-
|
|
79
|
+
this.validate(req.body, true);
|
|
80
|
+
const preparedEntity = await this.prepareDataForDb(req.userContext, req.body, false);
|
|
81
|
+
const updateResult = await this.service.partialUpdateById(req.userContext, req.params.id, preparedEntity);
|
|
64
82
|
apiUtils.apiResponse(res, 200, { data: updateResult }, this.modelSpec, this.publicSchema);
|
|
65
83
|
}
|
|
66
84
|
async deleteById(req, res, next) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { LoginResponseSpec, TokenResponseSpec, UserSpec, PublicUserSchema, UserContextSpec } from '@loomcore/common/models';
|
|
1
|
+
import { LoginResponseSpec, TokenResponseSpec, UserSpec, PublicUserSchema, UserContextSpec, passwordValidator } from '@loomcore/common/models';
|
|
2
|
+
import { entityUtils } from '@loomcore/common/utils';
|
|
2
3
|
import { BadRequestError, UnauthenticatedError } from '../errors/index.js';
|
|
3
4
|
import { isAuthenticated } from '../middleware/index.js';
|
|
4
5
|
import { apiUtils } from '../utils/index.js';
|
|
@@ -28,7 +29,10 @@ export class AuthController {
|
|
|
28
29
|
async registerUser(req, res) {
|
|
29
30
|
const userContext = req.userContext;
|
|
30
31
|
const body = req.body;
|
|
31
|
-
const
|
|
32
|
+
const validationErrors = this.authService.validate(body);
|
|
33
|
+
entityUtils.handleValidationResult(validationErrors, 'AuthController.registerUser');
|
|
34
|
+
const preparedUser = await this.authService.prepareDataForDb(userContext, body, true);
|
|
35
|
+
const user = await this.authService.createUser(userContext, preparedUser);
|
|
32
36
|
apiUtils.apiResponse(res, 201, { data: user || undefined }, UserSpec, PublicUserSchema);
|
|
33
37
|
}
|
|
34
38
|
async requestTokenUsingRefreshToken(req, res, next) {
|
|
@@ -51,6 +55,8 @@ export class AuthController {
|
|
|
51
55
|
async changePassword(req, res) {
|
|
52
56
|
const userContext = req.userContext;
|
|
53
57
|
const body = req.body;
|
|
58
|
+
const validationErrors = entityUtils.validate(passwordValidator, { password: body.password });
|
|
59
|
+
entityUtils.handleValidationResult(validationErrors, 'AuthController.changePassword');
|
|
54
60
|
const updateResult = await this.authService.changeLoggedInUsersPassword(userContext, body);
|
|
55
61
|
apiUtils.apiResponse(res, 200, { data: updateResult });
|
|
56
62
|
}
|
|
@@ -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,
|
|
4
|
+
import { EmptyUserContext, 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 };
|
|
@@ -88,8 +88,6 @@ export class AuthService extends GenericApiService {
|
|
|
88
88
|
return tokens;
|
|
89
89
|
}
|
|
90
90
|
async changeLoggedInUsersPassword(userContext, body) {
|
|
91
|
-
const validationResult = entityUtils.validate(passwordValidator, { password: body.password });
|
|
92
|
-
entityUtils.handleValidationResult(validationResult, 'AuthService.changePassword');
|
|
93
91
|
const queryObject = { _id: new ObjectId(userContext.user._id) };
|
|
94
92
|
const result = await this.changePassword(userContext, queryObject, body.password);
|
|
95
93
|
return result;
|
|
@@ -266,8 +264,9 @@ export class AuthService extends GenericApiService {
|
|
|
266
264
|
if (!entityUtils.isValidObjectId(userId)) {
|
|
267
265
|
throw new BadRequestError('userId is not a valid ObjectId');
|
|
268
266
|
}
|
|
269
|
-
const updates = { _lastLoggedIn: moment().utc().
|
|
270
|
-
|
|
267
|
+
const updates = { _lastLoggedIn: moment().utc().toDate() };
|
|
268
|
+
const systemUserContext = getSystemUserContext();
|
|
269
|
+
await this.partialUpdateById(systemUserContext, userId, updates);
|
|
271
270
|
}
|
|
272
271
|
catch (error) {
|
|
273
272
|
console.log(`Failed to update lastLoggedIn for user ${userId}: ${error}`);
|
|
@@ -4,6 +4,10 @@ import { IUserContext, IEntity, IPagedResult, QueryOptions } from '@loomcore/com
|
|
|
4
4
|
export interface IGenericApiService<T extends IEntity> {
|
|
5
5
|
validate(doc: any, isPartial?: boolean): ValueError[] | null;
|
|
6
6
|
validateMany(docs: any[], isPartial?: boolean): ValueError[] | null;
|
|
7
|
+
prepareDataForDb(userContext: IUserContext, entity: T, isCreate?: boolean): Promise<T>;
|
|
8
|
+
prepareDataForDb(userContext: IUserContext, entity: Partial<T>, isCreate?: boolean): Promise<Partial<T>>;
|
|
9
|
+
prepareDataForDb(userContext: IUserContext, entity: T[], isCreate?: boolean): Promise<T[]>;
|
|
10
|
+
prepareDataForDb(userContext: IUserContext, entity: Partial<T>[], isCreate?: boolean): Promise<Partial<T>[]>;
|
|
7
11
|
getAll(userContext: IUserContext): Promise<T[]>;
|
|
8
12
|
get(userContext: IUserContext, queryOptions: QueryOptions): Promise<IPagedResult<T>>;
|
|
9
13
|
getById(userContext: IUserContext, id: string): Promise<T>;
|
|
@@ -30,7 +30,7 @@ export declare class GenericApiService<T extends IEntity> implements IGenericApi
|
|
|
30
30
|
auditForCreate(userContext: IUserContext, doc: any): void;
|
|
31
31
|
auditForUpdate(userContext: IUserContext, doc: any): void;
|
|
32
32
|
onBeforeCreate<E extends T | T[] | Partial<T> | Partial<T>[]>(userContext: IUserContext, entities: E): Promise<E | E[]>;
|
|
33
|
-
onAfterCreate<E extends T | T[]>(userContext: IUserContext
|
|
33
|
+
onAfterCreate<E extends T | T[]>(userContext: IUserContext, entities: E): Promise<E | E[]>;
|
|
34
34
|
onBeforeUpdate<E extends T | T[] | Partial<T> | Partial<T>[]>(userContext: IUserContext, entities: E): Promise<E | E[]>;
|
|
35
35
|
onAfterUpdate<E extends T | T[] | Partial<T> | Partial<T>[]>(userContext: IUserContext | undefined, entities: E): Promise<E>;
|
|
36
36
|
onBeforeDelete(userContext: IUserContext, queryObject: any): Promise<any>;
|
|
@@ -38,7 +38,10 @@ export declare class GenericApiService<T extends IEntity> implements IGenericApi
|
|
|
38
38
|
transformList(list: any[]): T[];
|
|
39
39
|
transformSingle(single: any): T;
|
|
40
40
|
private stripSenderProvidedSystemProperties;
|
|
41
|
-
|
|
41
|
+
prepareDataForDb(userContext: IUserContext, entity: T, isCreate?: boolean): Promise<T>;
|
|
42
|
+
prepareDataForDb(userContext: IUserContext, entity: Partial<T>, isCreate?: boolean): Promise<Partial<T>>;
|
|
43
|
+
prepareDataForDb(userContext: IUserContext, entity: T[], isCreate?: boolean): Promise<T[]>;
|
|
44
|
+
prepareDataForDb(userContext: IUserContext, entity: Partial<T>[], isCreate?: boolean): Promise<Partial<T>[]>;
|
|
42
45
|
protected prepareEntity(userContext: IUserContext, entity: T | Partial<T>, isCreate: boolean): Promise<T | Partial<T>>;
|
|
43
46
|
protected prepareQuery(userContext: IUserContext | undefined, query: any): any;
|
|
44
47
|
protected prepareQueryOptions(userContext: IUserContext | undefined, queryOptions: QueryOptions): QueryOptions;
|
|
@@ -135,8 +135,6 @@ export class GenericApiService {
|
|
|
135
135
|
return count;
|
|
136
136
|
}
|
|
137
137
|
async create(userContext, entity) {
|
|
138
|
-
const validationErrors = this.validate(entity);
|
|
139
|
-
entityUtils.handleValidationResult(validationErrors, 'GenericApiService.create');
|
|
140
138
|
let createdEntity = null;
|
|
141
139
|
try {
|
|
142
140
|
const preparedEntity = await this.onBeforeCreate(userContext, entity);
|
|
@@ -160,8 +158,6 @@ export class GenericApiService {
|
|
|
160
158
|
let createdEntities = [];
|
|
161
159
|
if (entities.length) {
|
|
162
160
|
try {
|
|
163
|
-
const validationErrors = this.validateMany(entities);
|
|
164
|
-
entityUtils.handleValidationResult(validationErrors, 'GenericApiService.createMany');
|
|
165
161
|
const preparedEntities = await this.onBeforeCreate(userContext, entities);
|
|
166
162
|
const insertResult = await this.collection.insertMany(preparedEntities);
|
|
167
163
|
if (insertResult.insertedIds) {
|
|
@@ -182,8 +178,6 @@ export class GenericApiService {
|
|
|
182
178
|
if (!entityUtils.isValidObjectId(id)) {
|
|
183
179
|
throw new BadRequestError('id is not a valid ObjectId');
|
|
184
180
|
}
|
|
185
|
-
const validationErrors = this.validate(entity);
|
|
186
|
-
entityUtils.handleValidationResult(validationErrors, 'GenericApiService.fullUpdateById');
|
|
187
181
|
const baseQuery = { _id: new ObjectId(id) };
|
|
188
182
|
const query = this.prepareQuery(userContext, baseQuery);
|
|
189
183
|
const existingEntity = await this.collection.findOne(query);
|
|
@@ -194,13 +188,13 @@ export class GenericApiService {
|
|
|
194
188
|
_created: existingEntity._created,
|
|
195
189
|
_createdBy: existingEntity._createdBy,
|
|
196
190
|
};
|
|
197
|
-
const
|
|
198
|
-
Object.assign(
|
|
199
|
-
const mongoUpdateResult = await this.collection.replaceOne(query,
|
|
191
|
+
const preparedEntity = await this.onBeforeUpdate(userContext, entity);
|
|
192
|
+
Object.assign(preparedEntity, auditProperties);
|
|
193
|
+
const mongoUpdateResult = await this.collection.replaceOne(query, preparedEntity);
|
|
200
194
|
if (mongoUpdateResult?.matchedCount <= 0) {
|
|
201
195
|
throw new IdNotFoundError();
|
|
202
196
|
}
|
|
203
|
-
await this.onAfterUpdate(userContext,
|
|
197
|
+
await this.onAfterUpdate(userContext, preparedEntity);
|
|
204
198
|
const updatedEntity = await this.collection.findOne(query);
|
|
205
199
|
return this.transformSingle(updatedEntity);
|
|
206
200
|
}
|
|
@@ -208,12 +202,10 @@ export class GenericApiService {
|
|
|
208
202
|
if (!entityUtils.isValidObjectId(id)) {
|
|
209
203
|
throw new BadRequestError('id is not a valid ObjectId');
|
|
210
204
|
}
|
|
211
|
-
const
|
|
212
|
-
entityUtils.handleValidationResult(validationErrors, 'GenericApiService.partialUpdateById');
|
|
213
|
-
const clone = await this.onBeforeUpdate(userContext, entity);
|
|
205
|
+
const preparedEntity = await this.onBeforeUpdate(userContext, entity);
|
|
214
206
|
const baseQuery = { _id: new ObjectId(id) };
|
|
215
207
|
const query = this.prepareQuery(userContext, baseQuery);
|
|
216
|
-
const updatedEntity = await this.collection.findOneAndUpdate(query, { $set:
|
|
208
|
+
const updatedEntity = await this.collection.findOneAndUpdate(query, { $set: preparedEntity }, { returnDocument: 'after' });
|
|
217
209
|
if (!updatedEntity) {
|
|
218
210
|
throw new IdNotFoundError();
|
|
219
211
|
}
|
|
@@ -227,12 +219,9 @@ export class GenericApiService {
|
|
|
227
219
|
if (!entityUtils.isValidObjectId(id)) {
|
|
228
220
|
throw new BadRequestError('id is not a valid ObjectId');
|
|
229
221
|
}
|
|
230
|
-
const validationErrors = this.validate(entity, true);
|
|
231
|
-
entityUtils.handleValidationResult(validationErrors, 'GenericApiService.partialUpdateByIdWithoutBeforeAndAfter');
|
|
232
|
-
const preparedEntity = this.preparePayload(userContext, entity, false);
|
|
233
222
|
const baseQuery = { _id: new ObjectId(id) };
|
|
234
223
|
const query = this.prepareQuery(userContext, baseQuery);
|
|
235
|
-
const modifyResult = await this.collection.findOneAndUpdate(query, { $set:
|
|
224
|
+
const modifyResult = await this.collection.findOneAndUpdate(query, { $set: entity }, { returnDocument: 'after' });
|
|
236
225
|
let updatedEntity = null;
|
|
237
226
|
if (modifyResult?.ok === 1) {
|
|
238
227
|
updatedEntity = modifyResult.value;
|
|
@@ -248,13 +237,13 @@ export class GenericApiService {
|
|
|
248
237
|
return this.transformSingle(updatedEntity);
|
|
249
238
|
}
|
|
250
239
|
async update(userContext, queryObject, entity) {
|
|
251
|
-
const
|
|
240
|
+
const preparedEntity = await this.onBeforeUpdate(userContext, entity);
|
|
252
241
|
const query = this.prepareQuery(userContext, queryObject);
|
|
253
|
-
const mongoUpdateResult = await this.collection.updateMany(query, { $set:
|
|
242
|
+
const mongoUpdateResult = await this.collection.updateMany(query, { $set: preparedEntity });
|
|
254
243
|
if (mongoUpdateResult?.matchedCount <= 0) {
|
|
255
244
|
throw new NotFoundError('No records found matching update query');
|
|
256
245
|
}
|
|
257
|
-
await this.onAfterUpdate(userContext,
|
|
246
|
+
await this.onAfterUpdate(userContext, preparedEntity);
|
|
258
247
|
const updatedEntities = await this.collection.find(query).toArray();
|
|
259
248
|
return this.transformList(updatedEntities);
|
|
260
249
|
}
|
|
@@ -304,15 +293,13 @@ export class GenericApiService {
|
|
|
304
293
|
doc._updatedBy = userId;
|
|
305
294
|
}
|
|
306
295
|
async onBeforeCreate(userContext, entities) {
|
|
307
|
-
|
|
308
|
-
return Promise.resolve(preparedEntities);
|
|
296
|
+
return Promise.resolve(entities);
|
|
309
297
|
}
|
|
310
298
|
async onAfterCreate(userContext, entities) {
|
|
311
299
|
return Promise.resolve(entities);
|
|
312
300
|
}
|
|
313
301
|
async onBeforeUpdate(userContext, entities) {
|
|
314
|
-
|
|
315
|
-
return Promise.resolve(preparedEntities);
|
|
302
|
+
return Promise.resolve(entities);
|
|
316
303
|
}
|
|
317
304
|
onAfterUpdate(userContext, entities) {
|
|
318
305
|
return Promise.resolve(entities);
|
|
@@ -337,26 +324,28 @@ export class GenericApiService {
|
|
|
337
324
|
const transformedEntity = dbUtils.convertObjectIdsToStrings(single, this.modelSpec.fullSchema);
|
|
338
325
|
return transformedEntity;
|
|
339
326
|
}
|
|
340
|
-
stripSenderProvidedSystemProperties(doc) {
|
|
327
|
+
stripSenderProvidedSystemProperties(userContext, doc) {
|
|
328
|
+
const isSystemUser = userContext.user?._id === 'system';
|
|
329
|
+
if (isSystemUser) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
341
332
|
for (const key in doc) {
|
|
342
333
|
if (Object.prototype.hasOwnProperty.call(doc, key) && key.startsWith('_') && key !== '_orgId') {
|
|
343
334
|
delete doc[key];
|
|
344
335
|
}
|
|
345
336
|
}
|
|
346
337
|
}
|
|
347
|
-
async
|
|
348
|
-
let result;
|
|
338
|
+
async prepareDataForDb(userContext, entity, isCreate = false) {
|
|
349
339
|
if (Array.isArray(entity)) {
|
|
350
|
-
|
|
340
|
+
return await Promise.all(entity.map(item => this.prepareEntity(userContext, item, isCreate)));
|
|
351
341
|
}
|
|
352
342
|
else {
|
|
353
|
-
|
|
343
|
+
return await this.prepareEntity(userContext, entity, isCreate);
|
|
354
344
|
}
|
|
355
|
-
return result;
|
|
356
345
|
}
|
|
357
346
|
async prepareEntity(userContext, entity, isCreate) {
|
|
358
347
|
const preparedEntity = _.clone(entity);
|
|
359
|
-
this.stripSenderProvidedSystemProperties(preparedEntity);
|
|
348
|
+
this.stripSenderProvidedSystemProperties(userContext, preparedEntity);
|
|
360
349
|
if (this.modelSpec?.isAuditable) {
|
|
361
350
|
if (isCreate) {
|
|
362
351
|
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.23",
|
|
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",
|