@loomcore/api 0.0.34 → 0.0.36

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.
@@ -26,6 +26,7 @@ export interface ICategory extends IEntity {
26
26
  }
27
27
  export interface IProduct extends IEntity, IAuditable {
28
28
  name: string;
29
+ description?: string;
29
30
  internalNumber?: string;
30
31
  categoryId: string;
31
32
  category?: ICategory;
@@ -36,6 +37,7 @@ export declare const CategorySchema: import("@sinclair/typebox").TObject<{
36
37
  export declare const CategorySpec: import("@loomcore/common/models").IModelSpec<import("@sinclair/typebox").TSchema>;
37
38
  export declare const ProductSchema: import("@sinclair/typebox").TObject<{
38
39
  name: import("@sinclair/typebox").TString;
40
+ description: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
39
41
  internalNumber: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
40
42
  categoryId: import("@sinclair/typebox").TString;
41
43
  }>;
@@ -179,6 +179,7 @@ export const CategorySchema = Type.Object({
179
179
  export const CategorySpec = entityUtils.getModelSpec(CategorySchema);
180
180
  export const ProductSchema = Type.Object({
181
181
  name: Type.String(),
182
+ description: Type.Optional(Type.String()),
182
183
  internalNumber: Type.Optional(Type.String()),
183
184
  categoryId: Type.String({ format: 'objectid' }),
184
185
  });
@@ -22,6 +22,7 @@ export declare abstract class ApiController<T extends IEntity> {
22
22
  getById(req: Request, res: Response, next: NextFunction): Promise<void>;
23
23
  getCount(req: Request, res: Response, next: NextFunction): Promise<void>;
24
24
  create(req: Request, res: Response, next: NextFunction): Promise<void>;
25
+ batchUpdate(req: Request, res: Response, next: NextFunction): Promise<void>;
25
26
  fullUpdateById(req: Request, res: Response, next: NextFunction): Promise<void>;
26
27
  partialUpdateById(req: Request, res: Response, next: NextFunction): Promise<void>;
27
28
  deleteById(req: Request, res: Response, next: NextFunction): Promise<void>;
@@ -1,3 +1,4 @@
1
+ import { BadRequestError } from '../errors/index.js';
1
2
  import { entityUtils } from '@loomcore/common/utils';
2
3
  import { isAuthenticated } from '../middleware/index.js';
3
4
  import { apiUtils } from '../utils/index.js';
@@ -23,6 +24,7 @@ export class ApiController {
23
24
  app.get(`/api/${this.slug}/count`, isAuthenticated, this.getCount.bind(this));
24
25
  app.get(`/api/${this.slug}/:id`, isAuthenticated, this.getById.bind(this));
25
26
  app.post(`/api/${this.slug}`, isAuthenticated, this.create.bind(this));
27
+ app.patch(`/api/${this.slug}/batch`, isAuthenticated, this.batchUpdate.bind(this));
26
28
  app.put(`/api/${this.slug}/:id`, isAuthenticated, this.fullUpdateById.bind(this));
27
29
  app.patch(`/api/${this.slug}/:id`, isAuthenticated, this.partialUpdateById.bind(this));
28
30
  app.delete(`/api/${this.slug}/:id`, isAuthenticated, this.deleteById.bind(this));
@@ -67,6 +69,17 @@ export class ApiController {
67
69
  const entity = await this.service.create(req.userContext, preparedEntity);
68
70
  apiUtils.apiResponse(res, 201, { data: entity || undefined }, this.modelSpec, this.publicSchema);
69
71
  }
72
+ async batchUpdate(req, res, next) {
73
+ res.set('Content-Type', 'application/json');
74
+ const entities = req.body;
75
+ if (!Array.isArray(entities)) {
76
+ throw new BadRequestError('Request body must be an array of entities.');
77
+ }
78
+ this.validateMany(entities, true);
79
+ const preparedEntities = await this.service.prepareDataForBatchUpdate(req.userContext, entities);
80
+ const updatedEntities = await this.service.batchUpdate(req.userContext, preparedEntities);
81
+ apiUtils.apiResponse(res, 200, { data: updatedEntities }, this.modelSpec, this.publicSchema);
82
+ }
70
83
  async fullUpdateById(req, res, next) {
71
84
  res.set('Content-Type', 'application/json');
72
85
  this.validate(req.body);
@@ -8,12 +8,14 @@ export interface IGenericApiService<T extends IEntity> {
8
8
  prepareDataForDb(userContext: IUserContext, entity: Partial<T>, isCreate?: boolean): Promise<Partial<T>>;
9
9
  prepareDataForDb(userContext: IUserContext, entity: T[], isCreate?: boolean): Promise<T[]>;
10
10
  prepareDataForDb(userContext: IUserContext, entity: Partial<T>[], isCreate?: boolean): Promise<Partial<T>[]>;
11
+ prepareDataForBatchUpdate(userContext: IUserContext, entities: Partial<T>[]): Promise<Partial<T>[]>;
11
12
  getAll(userContext: IUserContext): Promise<T[]>;
12
13
  get(userContext: IUserContext, queryOptions: IQueryOptions): Promise<IPagedResult<T>>;
13
14
  getById(userContext: IUserContext, id: string): Promise<T>;
14
15
  getCount(userContext: IUserContext): Promise<number>;
15
16
  create(userContext: IUserContext, entity: T | Partial<T>): Promise<T | null>;
16
17
  createMany(userContext: IUserContext, entities: T[]): Promise<T[]>;
18
+ batchUpdate(userContext: IUserContext, entities: Partial<T>[]): Promise<T[]>;
17
19
  fullUpdateById(userContext: IUserContext, id: string, entity: T): Promise<T>;
18
20
  partialUpdateById(userContext: IUserContext, id: string, entity: Partial<T>): Promise<T>;
19
21
  partialUpdateByIdWithoutBeforeAndAfter(userContext: IUserContext, id: string, entity: T): Promise<T>;
@@ -17,12 +17,13 @@ export declare class GenericApiService<T extends IEntity> implements IGenericApi
17
17
  get(userContext: IUserContext, queryOptions?: IQueryOptions): Promise<IPagedResult<T>>;
18
18
  getById(userContext: IUserContext, id: string): Promise<T>;
19
19
  getCount(userContext: IUserContext): Promise<number>;
20
- create(userContext: IUserContext, entity: T | Partial<T>): Promise<T | null>;
21
- createMany(userContext: IUserContext, entities: T[]): Promise<T[]>;
22
- fullUpdateById(userContext: IUserContext, id: string, entity: T): Promise<T>;
23
- partialUpdateById(userContext: IUserContext, id: string, entity: Partial<T>): Promise<T>;
24
- partialUpdateByIdWithoutBeforeAndAfter(userContext: IUserContext, id: string, entity: T): Promise<T>;
25
- update(userContext: IUserContext, queryObject: any, entity: Partial<T>): Promise<T[]>;
20
+ create(userContext: IUserContext, preparedEntity: T | Partial<T>): Promise<T | null>;
21
+ createMany(userContext: IUserContext, preparedEntities: T[]): Promise<T[]>;
22
+ batchUpdate(userContext: IUserContext, preparedEntities: Partial<T>[]): Promise<T[]>;
23
+ fullUpdateById(userContext: IUserContext, id: string, preparedEntity: T): Promise<T>;
24
+ partialUpdateById(userContext: IUserContext, id: string, preparedEntity: Partial<T>): Promise<T>;
25
+ partialUpdateByIdWithoutBeforeAndAfter(userContext: IUserContext, id: string, preparedEntity: T): Promise<T>;
26
+ update(userContext: IUserContext, queryObject: any, preparedEntity: Partial<T>): Promise<T[]>;
26
27
  deleteById(userContext: IUserContext, id: string): Promise<DeleteResult>;
27
28
  deleteMany(userContext: IUserContext, queryObject: any): Promise<DeleteResult>;
28
29
  find(userContext: IUserContext, mongoQueryObject: any, options?: FindOptions<Document> | undefined): Promise<T[]>;
@@ -42,7 +43,8 @@ export declare class GenericApiService<T extends IEntity> implements IGenericApi
42
43
  prepareDataForDb(userContext: IUserContext, entity: Partial<T>, isCreate?: boolean): Promise<Partial<T>>;
43
44
  prepareDataForDb(userContext: IUserContext, entity: T[], isCreate?: boolean): Promise<T[]>;
44
45
  prepareDataForDb(userContext: IUserContext, entity: Partial<T>[], isCreate?: boolean): Promise<Partial<T>[]>;
45
- protected prepareEntity(userContext: IUserContext, entity: T | Partial<T>, isCreate: boolean): Promise<T | Partial<T>>;
46
+ prepareDataForBatchUpdate(userContext: IUserContext, entities: Partial<T>[]): Promise<Partial<T>[]>;
47
+ protected prepareEntity(userContext: IUserContext, entity: T | Partial<T>, isCreate: boolean, allowId?: boolean): Promise<T | Partial<T>>;
46
48
  protected prepareQuery(userContext: IUserContext | undefined, query: any): any;
47
49
  protected prepareQueryOptions(userContext: IUserContext | undefined, queryOptions: IQueryOptions): IQueryOptions;
48
50
  }
@@ -148,13 +148,13 @@ export class GenericApiService {
148
148
  const count = await this.collection.countDocuments(query);
149
149
  return count;
150
150
  }
151
- async create(userContext, entity) {
151
+ async create(userContext, preparedEntity) {
152
152
  let createdEntity = null;
153
153
  try {
154
- const preparedEntity = await this.onBeforeCreate(userContext, entity);
155
- const insertResult = await this.collection.insertOne(preparedEntity);
154
+ const entity = await this.onBeforeCreate(userContext, preparedEntity);
155
+ const insertResult = await this.collection.insertOne(entity);
156
156
  if (insertResult.insertedId) {
157
- createdEntity = this.transformSingle(preparedEntity);
157
+ createdEntity = this.transformSingle(entity);
158
158
  }
159
159
  if (createdEntity) {
160
160
  await this.onAfterCreate(userContext, createdEntity);
@@ -168,14 +168,14 @@ export class GenericApiService {
168
168
  }
169
169
  return createdEntity;
170
170
  }
171
- async createMany(userContext, entities) {
171
+ async createMany(userContext, preparedEntities) {
172
172
  let createdEntities = [];
173
- if (entities.length) {
173
+ if (preparedEntities.length) {
174
174
  try {
175
- const preparedEntities = await this.onBeforeCreate(userContext, entities);
176
- const insertResult = await this.collection.insertMany(preparedEntities);
175
+ const entities = await this.onBeforeCreate(userContext, preparedEntities);
176
+ const insertResult = await this.collection.insertMany(entities);
177
177
  if (insertResult.insertedIds) {
178
- createdEntities = this.transformList(preparedEntities);
178
+ createdEntities = this.transformList(entities);
179
179
  }
180
180
  await this.onAfterCreate(userContext, createdEntities);
181
181
  }
@@ -188,7 +188,38 @@ export class GenericApiService {
188
188
  }
189
189
  return createdEntities;
190
190
  }
191
- async fullUpdateById(userContext, id, entity) {
191
+ async batchUpdate(userContext, preparedEntities) {
192
+ if (!preparedEntities || preparedEntities.length === 0) {
193
+ return [];
194
+ }
195
+ const entities = await this.onBeforeUpdate(userContext, preparedEntities);
196
+ const operations = [];
197
+ const entityIds = [];
198
+ for (const entity of entities) {
199
+ const { _id, ...updateData } = entity;
200
+ if (!_id || !(_id instanceof ObjectId)) {
201
+ throw new BadRequestError('Each entity in a batch update must have a valid _id that has been converted to an ObjectId.');
202
+ }
203
+ entityIds.push(_id);
204
+ operations.push({
205
+ updateOne: {
206
+ filter: { _id },
207
+ update: { $set: updateData },
208
+ },
209
+ });
210
+ }
211
+ if (operations.length > 0) {
212
+ await this.collection.bulkWrite(operations);
213
+ }
214
+ const query = this.prepareQuery(userContext, { _id: { $in: entityIds } });
215
+ const rawUpdatedEntities = await this.collection.find(query).toArray();
216
+ const updatedEntities = this.transformList(rawUpdatedEntities);
217
+ if (updatedEntities.length > 0) {
218
+ await this.onAfterUpdate(userContext, updatedEntities);
219
+ }
220
+ return updatedEntities;
221
+ }
222
+ async fullUpdateById(userContext, id, preparedEntity) {
192
223
  if (!entityUtils.isValidObjectId(id)) {
193
224
  throw new BadRequestError('id is not a valid ObjectId');
194
225
  }
@@ -202,24 +233,24 @@ export class GenericApiService {
202
233
  _created: existingEntity._created,
203
234
  _createdBy: existingEntity._createdBy,
204
235
  };
205
- const preparedEntity = await this.onBeforeUpdate(userContext, entity);
206
- Object.assign(preparedEntity, auditProperties);
207
- const mongoUpdateResult = await this.collection.replaceOne(query, preparedEntity);
236
+ const entity = await this.onBeforeUpdate(userContext, preparedEntity);
237
+ Object.assign(entity, auditProperties);
238
+ const mongoUpdateResult = await this.collection.replaceOne(query, entity);
208
239
  if (mongoUpdateResult?.matchedCount <= 0) {
209
240
  throw new IdNotFoundError();
210
241
  }
211
- await this.onAfterUpdate(userContext, preparedEntity);
242
+ await this.onAfterUpdate(userContext, entity);
212
243
  const updatedEntity = await this.collection.findOne(query);
213
244
  return this.transformSingle(updatedEntity);
214
245
  }
215
- async partialUpdateById(userContext, id, entity) {
246
+ async partialUpdateById(userContext, id, preparedEntity) {
216
247
  if (!entityUtils.isValidObjectId(id)) {
217
248
  throw new BadRequestError('id is not a valid ObjectId');
218
249
  }
219
- const preparedEntity = await this.onBeforeUpdate(userContext, entity);
250
+ const entity = await this.onBeforeUpdate(userContext, preparedEntity);
220
251
  const baseQuery = { _id: new ObjectId(id) };
221
252
  const query = this.prepareQuery(userContext, baseQuery);
222
- const updatedEntity = await this.collection.findOneAndUpdate(query, { $set: preparedEntity }, { returnDocument: 'after' });
253
+ const updatedEntity = await this.collection.findOneAndUpdate(query, { $set: entity }, { returnDocument: 'after' });
223
254
  if (!updatedEntity) {
224
255
  throw new IdNotFoundError();
225
256
  }
@@ -229,13 +260,13 @@ export class GenericApiService {
229
260
  }
230
261
  return this.transformSingle(updatedEntity);
231
262
  }
232
- async partialUpdateByIdWithoutBeforeAndAfter(userContext, id, entity) {
263
+ async partialUpdateByIdWithoutBeforeAndAfter(userContext, id, preparedEntity) {
233
264
  if (!entityUtils.isValidObjectId(id)) {
234
265
  throw new BadRequestError('id is not a valid ObjectId');
235
266
  }
236
267
  const baseQuery = { _id: new ObjectId(id) };
237
268
  const query = this.prepareQuery(userContext, baseQuery);
238
- const modifyResult = await this.collection.findOneAndUpdate(query, { $set: entity }, { returnDocument: 'after' });
269
+ const modifyResult = await this.collection.findOneAndUpdate(query, { $set: preparedEntity }, { returnDocument: 'after' });
239
270
  let updatedEntity = null;
240
271
  if (modifyResult?.ok === 1) {
241
272
  updatedEntity = modifyResult.value;
@@ -250,14 +281,14 @@ export class GenericApiService {
250
281
  }
251
282
  return this.transformSingle(updatedEntity);
252
283
  }
253
- async update(userContext, queryObject, entity) {
254
- const preparedEntity = await this.onBeforeUpdate(userContext, entity);
284
+ async update(userContext, queryObject, preparedEntity) {
285
+ const entity = await this.onBeforeUpdate(userContext, preparedEntity);
255
286
  const query = this.prepareQuery(userContext, queryObject);
256
- const mongoUpdateResult = await this.collection.updateMany(query, { $set: preparedEntity });
287
+ const mongoUpdateResult = await this.collection.updateMany(query, { $set: entity });
257
288
  if (mongoUpdateResult?.matchedCount <= 0) {
258
289
  throw new NotFoundError('No records found matching update query');
259
290
  }
260
- await this.onAfterUpdate(userContext, preparedEntity);
291
+ await this.onAfterUpdate(userContext, entity);
261
292
  const updatedEntities = await this.collection.find(query).toArray();
262
293
  return this.transformList(updatedEntities);
263
294
  }
@@ -338,13 +369,16 @@ export class GenericApiService {
338
369
  const transformedEntity = dbUtils.convertObjectIdsToStrings(single, this.modelSpec.fullSchema);
339
370
  return transformedEntity;
340
371
  }
341
- stripSenderProvidedSystemProperties(userContext, doc) {
372
+ stripSenderProvidedSystemProperties(userContext, doc, allowId = false) {
342
373
  const isSystemUser = userContext.user?._id === 'system';
343
374
  if (isSystemUser) {
344
375
  return;
345
376
  }
346
377
  for (const key in doc) {
347
378
  if (Object.prototype.hasOwnProperty.call(doc, key) && key.startsWith('_') && key !== '_orgId') {
379
+ if (allowId && key === '_id') {
380
+ continue;
381
+ }
348
382
  delete doc[key];
349
383
  }
350
384
  }
@@ -357,9 +391,12 @@ export class GenericApiService {
357
391
  return await this.prepareEntity(userContext, entity, isCreate);
358
392
  }
359
393
  }
360
- async prepareEntity(userContext, entity, isCreate) {
394
+ async prepareDataForBatchUpdate(userContext, entities) {
395
+ return Promise.all(entities.map(item => this.prepareEntity(userContext, item, false, true)));
396
+ }
397
+ async prepareEntity(userContext, entity, isCreate, allowId = false) {
361
398
  const preparedEntity = _.clone(entity);
362
- this.stripSenderProvidedSystemProperties(userContext, preparedEntity);
399
+ this.stripSenderProvidedSystemProperties(userContext, preparedEntity, allowId);
363
400
  if (this.modelSpec?.isAuditable) {
364
401
  if (isCreate) {
365
402
  this.auditForCreate(userContext, preparedEntity);
@@ -67,6 +67,9 @@ function convertStringsToObjectIds(entity, schema) {
67
67
  if (!entity)
68
68
  return entity;
69
69
  const clone = _.cloneDeep(entity);
70
+ if (clone._id && typeof clone._id === 'string' && entityUtils.isValidObjectId(clone._id)) {
71
+ clone._id = new ObjectId(clone._id);
72
+ }
70
73
  const processEntity = (obj, subSchema, path = []) => {
71
74
  if (!obj || typeof obj !== 'object')
72
75
  return obj;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loomcore/api",
3
- "version": "0.0.34",
3
+ "version": "0.0.36",
4
4
  "private": false,
5
5
  "description": "Loom Core Api - An opinionated Node.js api using Typescript, Express, and MongoDb",
6
6
  "scripts": {