@loomcore/api 0.0.22 → 0.0.24

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.
@@ -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
- const entity = await this.service.create(req.userContext, req.body);
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
- const updateResult = await this.service.fullUpdateById(req.userContext, req.params.id, req.body);
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
- const updateResult = await this.service.partialUpdateById(req.userContext, req.params.id, req.body);
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 user = await this.authService.createUser(userContext, body);
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) {
@@ -42,8 +46,7 @@ export class AuthController {
42
46
  }
43
47
  async getUserContext(req, res, next) {
44
48
  const userContext = req.userContext;
45
- const clientUserContext = { user: userContext.user };
46
- apiUtils.apiResponse(res, 200, { data: clientUserContext }, UserContextSpec);
49
+ apiUtils.apiResponse(res, 200, { data: userContext }, UserContextSpec);
47
50
  }
48
51
  afterAuth(req, res, loginResponse) {
49
52
  console.log('in afterAuth');
@@ -51,6 +54,8 @@ export class AuthController {
51
54
  async changePassword(req, res) {
52
55
  const userContext = req.userContext;
53
56
  const body = req.body;
57
+ const validationErrors = entityUtils.validate(passwordValidator, { password: body.password });
58
+ entityUtils.handleValidationResult(validationErrors, 'AuthController.changePassword');
54
59
  const updateResult = await this.authService.changeLoggedInUsersPassword(userContext, body);
55
60
  apiUtils.apiResponse(res, 200, { data: updateResult });
56
61
  }
@@ -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, getSystemUserContext } from '@loomcore/common/models';
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().toISOString() };
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 | undefined, entities: E): Promise<E | E[]>;
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
- protected preparePayload<E extends T | T[] | Partial<T> | Partial<T>[]>(userContext: IUserContext, entity: E, isCreate?: boolean): Promise<E | E[]>;
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 clone = await this.onBeforeUpdate(userContext, entity);
198
- Object.assign(clone, auditProperties);
199
- const mongoUpdateResult = await this.collection.replaceOne(query, clone);
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, clone);
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 validationErrors = this.validate(entity, true);
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: clone }, { returnDocument: 'after' });
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: preparedEntity }, { returnDocument: 'after' });
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 clone = await this.onBeforeUpdate(userContext, entity);
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: clone });
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, clone);
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
- const preparedEntities = await this.preparePayload(userContext, entities, true);
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
- const preparedEntities = await this.preparePayload(userContext, entities, false);
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 preparePayload(userContext, entity, isCreate = false) {
352
- let result;
338
+ async prepareDataForDb(userContext, entity, isCreate = false) {
353
339
  if (Array.isArray(entity)) {
354
- result = await Promise.all(entity.map(item => this.prepareEntity(userContext, item, isCreate)));
340
+ return await Promise.all(entity.map(item => this.prepareEntity(userContext, item, isCreate)));
355
341
  }
356
342
  else {
357
- result = await this.prepareEntity(userContext, entity, isCreate);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loomcore/api",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "private": false,
5
5
  "description": "Loom Core Api - An opinionated Node.js api using Typescript, Express, and MongoDb",
6
6
  "scripts": {
@@ -51,7 +51,8 @@
51
51
  "express": "^5.1.0",
52
52
  "lodash": "^4.17.21",
53
53
  "moment": "^2.30.1",
54
- "mongodb": "^6.16.0"
54
+ "mongodb": "^6.16.0",
55
+ "rxjs": "^7.8.0"
55
56
  },
56
57
  "devDependencies": {
57
58
  "@types/cookie-parser": "^1.4.7",
@@ -64,6 +65,7 @@
64
65
  "cross-env": "^7.0.3",
65
66
  "mongodb-memory-server": "^9.3.0",
66
67
  "npm-run-all": "^4.1.5",
68
+ "rxjs": "^7.8.0",
67
69
  "supertest": "^7.1.0",
68
70
  "typescript": "^5.8.3",
69
71
  "vite": "^6.2.5",