@loomcore/api 0.0.34 → 0.0.35
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 +2 -0
- package/dist/__tests__/common-test.utils.js +1 -0
- package/dist/controllers/api.controller.d.ts +1 -0
- package/dist/controllers/api.controller.js +13 -0
- package/dist/services/generic-api-service.interface.d.ts +2 -0
- package/dist/services/generic-api.service.d.ts +9 -7
- package/dist/services/generic-api.service.js +63 -26
- package/package.json +1 -1
|
@@ -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,
|
|
21
|
-
createMany(userContext: IUserContext,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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,
|
|
151
|
+
async create(userContext, preparedEntity) {
|
|
152
152
|
let createdEntity = null;
|
|
153
153
|
try {
|
|
154
|
-
const
|
|
155
|
-
const insertResult = await this.collection.insertOne(
|
|
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(
|
|
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,
|
|
171
|
+
async createMany(userContext, preparedEntities) {
|
|
172
172
|
let createdEntities = [];
|
|
173
|
-
if (
|
|
173
|
+
if (preparedEntities.length) {
|
|
174
174
|
try {
|
|
175
|
-
const
|
|
176
|
-
const insertResult = await this.collection.insertMany(
|
|
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(
|
|
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
|
|
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
|
|
206
|
-
Object.assign(
|
|
207
|
-
const mongoUpdateResult = await this.collection.replaceOne(query,
|
|
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,
|
|
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,
|
|
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
|
|
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:
|
|
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,
|
|
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:
|
|
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,
|
|
254
|
-
const
|
|
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:
|
|
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,
|
|
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
|
|
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);
|