@loomcore/api 0.0.22 → 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/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/services/auth.service.js +2 -4
- 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 +15 -30
- package/package.json +1 -1
|
@@ -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';
|
|
@@ -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,7 +264,7 @@ 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().
|
|
267
|
+
const updates = { _lastLoggedIn: moment().utc().toDate() };
|
|
270
268
|
const systemUserContext = getSystemUserContext();
|
|
271
269
|
await this.partialUpdateById(systemUserContext, userId, updates);
|
|
272
270
|
}
|
|
@@ -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);
|
|
@@ -348,15 +335,13 @@ export class GenericApiService {
|
|
|
348
335
|
}
|
|
349
336
|
}
|
|
350
337
|
}
|
|
351
|
-
async
|
|
352
|
-
let result;
|
|
338
|
+
async prepareDataForDb(userContext, entity, isCreate = false) {
|
|
353
339
|
if (Array.isArray(entity)) {
|
|
354
|
-
|
|
340
|
+
return await Promise.all(entity.map(item => this.prepareEntity(userContext, item, isCreate)));
|
|
355
341
|
}
|
|
356
342
|
else {
|
|
357
|
-
|
|
343
|
+
return await this.prepareEntity(userContext, entity, isCreate);
|
|
358
344
|
}
|
|
359
|
-
return result;
|
|
360
345
|
}
|
|
361
346
|
async prepareEntity(userContext, entity, isCreate) {
|
|
362
347
|
const preparedEntity = _.clone(entity);
|