@loomcore/api 0.1.90 → 0.1.92

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,4 +1,4 @@
1
- import { Application } from 'express';
1
+ import { Request, Response, Application, NextFunction } from 'express';
2
2
  import { IUserContext, IQueryOptions, IUser } from '@loomcore/common/models';
3
3
  import { ApiController } from '../controllers/api.controller.js';
4
4
  import { MultiTenantApiService } from '../services/multi-tenant-api.service.js';
@@ -31,16 +31,13 @@ export declare class CategoryController extends ApiController<ICategory> {
31
31
  }
32
32
  export declare function setupTestConfig(isMultiTenant: boolean | undefined, dbType: DbType): void;
33
33
  export declare class ProductService extends GenericApiService<IProduct> {
34
- private db;
35
34
  constructor(database: IDatabase);
36
- prepareQuery(userContext: IUserContext, queryObject: IQueryOptions, operations: Operation[]): {
37
- queryObject: IQueryOptions;
38
- operations: Operation[];
39
- };
40
- postProcessEntity(userContext: IUserContext, single: any): any;
41
35
  }
42
36
  export declare class ProductsController extends ApiController<IProduct> {
43
37
  constructor(app: Application, database: IDatabase);
38
+ get(req: Request, res: Response, next: NextFunction): Promise<void>;
39
+ getAll(req: Request, res: Response, next: NextFunction): Promise<void>;
40
+ getById(req: Request, res: Response, next: NextFunction): Promise<void>;
44
41
  }
45
42
  export declare class MultiTenantProductService extends MultiTenantApiService<IProduct> {
46
43
  private db;
@@ -2,6 +2,7 @@ import crypto from 'crypto';
2
2
  import jwt from 'jsonwebtoken';
3
3
  import { EmptyUserContext } from '@loomcore/common/models';
4
4
  import { Type } from '@sinclair/typebox';
5
+ import { Value } from '@sinclair/typebox/value';
5
6
  import { JwtService } from '../services/jwt.service.js';
6
7
  import { ApiController } from '../controllers/api.controller.js';
7
8
  import { MultiTenantApiService } from '../services/multi-tenant-api.service.js';
@@ -15,11 +16,13 @@ import * as testObjectsModule from './test-objects.js';
15
16
  const { getTestMetaOrg, getTestOrg, getTestMetaOrgUser, getTestMetaOrgUserContext, getTestOrgUserContext, setTestOrgId, setTestMetaOrgId, setTestMetaOrgUserId, setTestOrgUserId } = testObjectsModule;
16
17
  import { CategorySpec } from './models/category.model.js';
17
18
  import { ProductSpec } from './models/product.model.js';
19
+ import { ProductWithCategoryPublicSpec, ProductWithCategorySpec } from './models/product-with-category.model.js';
18
20
  import { setBaseApiConfig, config } from '../config/index.js';
19
21
  import { entityUtils } from '@loomcore/common/utils';
20
22
  import { getTestMetaOrgUserPerson, getTestOrgUser, getTestOrgUserPerson, setTestMetaOrgUserPersonId, setTestOrgUserPersonId } from './test-objects.js';
21
23
  import { TestEmailClient } from './test-email-client.js';
22
24
  import { PersonService } from '../services/person.service.js';
25
+ import { apiUtils } from '../utils/index.js';
23
26
  let deviceIdCookie;
24
27
  let authService;
25
28
  let organizationService;
@@ -234,39 +237,70 @@ export function setupTestConfig(isMultiTenant = true, dbType) {
234
237
  },
235
238
  });
236
239
  }
240
+ const prepareQueryCustom = (userContext, queryObject, operations) => {
241
+ return {
242
+ queryObject: queryObject,
243
+ operations: [
244
+ ...operations,
245
+ new Join('categories', 'categoryId', '_id', 'category')
246
+ ]
247
+ };
248
+ };
249
+ const postProcessEntityCustom = (userContext, entity) => {
250
+ return {
251
+ ...entity,
252
+ category: entity._joinData?.category
253
+ };
254
+ };
237
255
  export class ProductService extends GenericApiService {
238
- db;
239
256
  constructor(database) {
240
257
  super(database, 'products', 'product', ProductSpec);
241
- this.db = database;
242
- }
243
- prepareQuery(userContext, queryObject, operations) {
244
- const newOperations = [
245
- ...operations,
246
- new Join('categories', 'categoryId', '_id', 'category')
247
- ];
248
- return super.prepareQuery(userContext, queryObject, newOperations);
249
- }
250
- postProcessEntity(userContext, single) {
251
- if (single && single.category) {
252
- const categoryService = new CategoryService(this.db);
253
- single.category = categoryService.postProcessEntity(userContext, single.category);
254
- }
255
- return super.postProcessEntity(userContext, single);
256
258
  }
257
259
  }
258
260
  export class ProductsController extends ApiController {
259
261
  constructor(app, database) {
260
262
  const productService = new ProductService(database);
261
- const AggregatedProductSchema = Type.Intersect([
262
- ProductSpec.fullSchema,
263
- Type.Partial(Type.Object({
264
- category: CategorySpec.fullSchema
265
- }))
266
- ]);
267
- const PublicAggregatedProductSchema = Type.Omit(AggregatedProductSchema, ['internalNumber']);
268
- const PublicAggregatedProductSpec = entityUtils.getModelSpec(PublicAggregatedProductSchema);
269
- super('products', app, productService, 'product', ProductSpec, PublicAggregatedProductSpec);
263
+ super('products', app, productService, 'product', ProductSpec);
264
+ }
265
+ async get(req, res, next) {
266
+ res.set('Content-Type', 'application/json');
267
+ const userContext = req.userContext;
268
+ if (!userContext) {
269
+ throw new Error('User context not found');
270
+ }
271
+ const queryOptions = apiUtils.getQueryOptionsFromRequest(req);
272
+ const pagedResult = await this.service.get(userContext, queryOptions, prepareQueryCustom, postProcessEntityCustom);
273
+ apiUtils.apiResponse(res, 200, { data: pagedResult }, ProductWithCategorySpec, ProductWithCategoryPublicSpec);
274
+ }
275
+ async getAll(req, res, next) {
276
+ res.set('Content-Type', 'application/json');
277
+ const userContext = req.userContext;
278
+ if (!userContext) {
279
+ throw new Error('User context not found');
280
+ }
281
+ const entities = await this.service.getAll(userContext, prepareQueryCustom, postProcessEntityCustom);
282
+ apiUtils.apiResponse(res, 200, { data: entities }, ProductWithCategorySpec, ProductWithCategoryPublicSpec);
283
+ }
284
+ async getById(req, res, next) {
285
+ res.set('Content-Type', 'application/json');
286
+ const userContext = req.userContext;
287
+ if (!userContext) {
288
+ throw new Error('User context not found');
289
+ }
290
+ const idParam = req.params?.id;
291
+ if (!idParam) {
292
+ throw new Error('ID parameter is required');
293
+ }
294
+ try {
295
+ const id = Value.Convert(this.idSchema, idParam);
296
+ console.log('got to Id response');
297
+ const entity = await this.service.getById(userContext, id, prepareQueryCustom, postProcessEntityCustom);
298
+ console.log('got to Id response 2');
299
+ apiUtils.apiResponse(res, 200, { data: entity }, ProductWithCategorySpec, ProductWithCategoryPublicSpec);
300
+ }
301
+ catch (error) {
302
+ throw new Error(`Invalid ID format: ${error.message || error}`);
303
+ }
270
304
  }
271
305
  }
272
306
  export class MultiTenantProductService extends MultiTenantApiService {
@@ -0,0 +1,21 @@
1
+ import { ICategory } from "./category.model.js";
2
+ import { IAuditable, IEntity } from "@loomcore/common/models";
3
+ export interface IProductWithCategory extends IEntity, IAuditable {
4
+ name: string;
5
+ description?: string;
6
+ internalNumber?: string;
7
+ category: ICategory;
8
+ }
9
+ export declare const ProductWithCategorySchema: import("@sinclair/typebox").TObject<{
10
+ name: import("@sinclair/typebox").TString;
11
+ description: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
12
+ internalNumber: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
13
+ category: import("@sinclair/typebox").TSchema;
14
+ }>;
15
+ export declare const ProductWithCategorySpec: import("@loomcore/common/models").IModelSpec<import("@sinclair/typebox").TSchema>;
16
+ export declare const PublicProductWithCategorySchema: import("@sinclair/typebox").TObject<{
17
+ name: import("@sinclair/typebox").TString;
18
+ description: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
19
+ category: import("@sinclair/typebox").TSchema;
20
+ }>;
21
+ export declare const ProductWithCategoryPublicSpec: import("@loomcore/common/models").IModelSpec<import("@sinclair/typebox").TSchema>;
@@ -0,0 +1,12 @@
1
+ import { entityUtils } from "@loomcore/common/utils";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { CategorySpec } from "./category.model.js";
4
+ export const ProductWithCategorySchema = Type.Object({
5
+ name: Type.String(),
6
+ description: Type.Optional(Type.String()),
7
+ internalNumber: Type.Optional(Type.String()),
8
+ category: CategorySpec.fullSchema,
9
+ });
10
+ export const ProductWithCategorySpec = entityUtils.getModelSpec(ProductWithCategorySchema, { isAuditable: true });
11
+ export const PublicProductWithCategorySchema = Type.Omit(ProductWithCategorySchema, ['internalNumber']);
12
+ export const ProductWithCategoryPublicSpec = entityUtils.getModelSpec(PublicProductWithCategorySchema);
@@ -1,12 +1,10 @@
1
1
  import { IAuditable, IEntity } from "@loomcore/common/models";
2
- import { ICategory } from "./category.model.js";
3
2
  import { AppIdType } from "@loomcore/common/types";
4
3
  export interface IProduct extends IEntity, IAuditable {
5
4
  name: string;
6
5
  description?: string;
7
6
  internalNumber?: string;
8
7
  categoryId: AppIdType;
9
- category?: ICategory;
10
8
  }
11
9
  export declare const ProductSchema: import("@sinclair/typebox").TObject<{
12
10
  name: import("@sinclair/typebox").TString;
@@ -8,3 +8,4 @@ export * from './roles.controller.js';
8
8
  export * from './authorizations.controller.js';
9
9
  export * from './features.controller.js';
10
10
  export * from './persons.controller.js';
11
+ export * from './types.js';
@@ -8,3 +8,4 @@ export * from './roles.controller.js';
8
8
  export * from './authorizations.controller.js';
9
9
  export * from './features.controller.js';
10
10
  export * from './persons.controller.js';
11
+ export * from './types.js';
@@ -0,0 +1,7 @@
1
+ import { IEntity, IQueryOptions, IUserContext } from "@loomcore/common/models";
2
+ import { Operation } from "../databases/index.js";
3
+ export type PrepareQueryCustomFunction = (userContext: IUserContext | undefined, queryObject: IQueryOptions, operations: Operation[]) => {
4
+ queryObject: IQueryOptions;
5
+ operations: Operation[];
6
+ };
7
+ export type PostProcessEntityCustomFunction<TIn extends IEntity, TOut extends IEntity> = (userContext: IUserContext, entity: TIn) => TOut;
@@ -0,0 +1 @@
1
+ export {};
@@ -2,21 +2,42 @@ import { Join } from '../../operations/join.operation.js';
2
2
  import { JoinMany } from '../../operations/join-many.operation.js';
3
3
  import { JoinThrough } from '../../operations/join-through.operation.js';
4
4
  import { JoinThroughMany } from '../../operations/join-through-many.operation.js';
5
+ function resolveLocalFieldPath(localField, processedOperations) {
6
+ if (!localField.includes('.')) {
7
+ return localField;
8
+ }
9
+ const [parentAlias] = localField.split('.');
10
+ const parentJoin = processedOperations.find(op => (op instanceof Join || op instanceof JoinMany || op instanceof JoinThrough || op instanceof JoinThroughMany) &&
11
+ op.as === parentAlias);
12
+ if (parentJoin) {
13
+ return `_joinData.${localField}`;
14
+ }
15
+ return localField;
16
+ }
5
17
  export function convertOperationsToPipeline(operations) {
6
18
  let pipeline = [];
7
19
  const processedOperations = [];
20
+ const joinAliases = operations
21
+ .filter(op => op instanceof Join || op instanceof JoinMany || op instanceof JoinThrough || op instanceof JoinThroughMany)
22
+ .map(op => op.as);
8
23
  operations.forEach(operation => {
9
24
  if (operation instanceof Join) {
10
25
  const needsObjectIdConversion = operation.foreignField === '_id';
26
+ const isNestedField = operation.localField.includes('.');
27
+ const resolvedLocalField = resolveLocalFieldPath(operation.localField, processedOperations);
11
28
  if (needsObjectIdConversion) {
12
29
  pipeline.push({
13
30
  $lookup: {
14
31
  from: operation.from,
15
- let: { localId: { $cond: [
16
- { $eq: [{ $type: `$${operation.localField}` }, 'string'] },
17
- { $toObjectId: `$${operation.localField}` },
18
- `$${operation.localField}`
19
- ] } },
32
+ let: {
33
+ localId: {
34
+ $cond: [
35
+ { $eq: [{ $type: `$${resolvedLocalField}` }, 'string'] },
36
+ { $toObjectId: `$${resolvedLocalField}` },
37
+ `$${resolvedLocalField}`
38
+ ]
39
+ }
40
+ },
20
41
  pipeline: [
21
42
  {
22
43
  $match: {
@@ -35,7 +56,12 @@ export function convertOperationsToPipeline(operations) {
35
56
  }
36
57
  }, {
37
58
  $addFields: {
38
- [operation.as]: `$${operation.as}Arr`
59
+ _joinData: {
60
+ $mergeObjects: [
61
+ { $ifNull: ['$_joinData', {}] },
62
+ { [operation.as]: `$${operation.as}Arr` }
63
+ ]
64
+ }
39
65
  }
40
66
  }, {
41
67
  $project: {
@@ -47,7 +73,7 @@ export function convertOperationsToPipeline(operations) {
47
73
  pipeline.push({
48
74
  $lookup: {
49
75
  from: operation.from,
50
- localField: operation.localField,
76
+ localField: resolvedLocalField,
51
77
  foreignField: operation.foreignField,
52
78
  as: `${operation.as}Arr`
53
79
  }
@@ -58,7 +84,12 @@ export function convertOperationsToPipeline(operations) {
58
84
  }
59
85
  }, {
60
86
  $addFields: {
61
- [operation.as]: `$${operation.as}Arr`
87
+ _joinData: {
88
+ $mergeObjects: [
89
+ { $ifNull: ['$_joinData', {}] },
90
+ { [operation.as]: `$${operation.as}Arr` }
91
+ ]
92
+ }
62
93
  }
63
94
  }, {
64
95
  $project: {
@@ -66,19 +97,67 @@ export function convertOperationsToPipeline(operations) {
66
97
  }
67
98
  });
68
99
  }
100
+ if (isNestedField) {
101
+ const [parentAlias] = operation.localField.split('.');
102
+ const parentJoin = processedOperations.find(op => (op instanceof Join || op instanceof JoinMany || op instanceof JoinThrough || op instanceof JoinThroughMany) && op.as === parentAlias);
103
+ if (parentJoin) {
104
+ pipeline.push({
105
+ $addFields: {
106
+ _joinData: {
107
+ $mergeObjects: [
108
+ { $ifNull: ['$_joinData', {}] },
109
+ {
110
+ [parentAlias]: {
111
+ $mergeObjects: [
112
+ {
113
+ $ifNull: [
114
+ {
115
+ $getField: {
116
+ field: parentAlias,
117
+ input: { $ifNull: ['$_joinData', {}] }
118
+ }
119
+ },
120
+ {}
121
+ ]
122
+ },
123
+ {
124
+ [operation.as]: {
125
+ $getField: {
126
+ field: operation.as,
127
+ input: { $ifNull: ['$_joinData', {}] }
128
+ }
129
+ }
130
+ }
131
+ ]
132
+ }
133
+ }
134
+ ]
135
+ }
136
+ }
137
+ });
138
+ pipeline.push({
139
+ $unset: `_joinData.${operation.as}`
140
+ });
141
+ }
142
+ }
69
143
  }
70
144
  else if (operation instanceof JoinMany) {
71
145
  const needsObjectIdConversion = operation.foreignField === '_id';
72
146
  const isNestedField = operation.localField.includes('.');
147
+ const resolvedLocalField = resolveLocalFieldPath(operation.localField, processedOperations);
73
148
  if (needsObjectIdConversion || isNestedField) {
74
149
  pipeline.push({
75
150
  $lookup: {
76
151
  from: operation.from,
77
- let: { localId: { $cond: [
78
- { $eq: [{ $type: `$${operation.localField}` }, 'string'] },
79
- { $toObjectId: `$${operation.localField}` },
80
- `$${operation.localField}`
81
- ] } },
152
+ let: {
153
+ localId: {
154
+ $cond: [
155
+ { $eq: [{ $type: `$${resolvedLocalField}` }, 'string'] },
156
+ { $toObjectId: `$${resolvedLocalField}` },
157
+ `$${resolvedLocalField}`
158
+ ]
159
+ }
160
+ },
82
161
  pipeline: [
83
162
  {
84
163
  $match: {
@@ -88,27 +167,45 @@ export function convertOperationsToPipeline(operations) {
88
167
  }
89
168
  }
90
169
  ],
91
- as: operation.as
170
+ as: `${operation.as}_temp`
171
+ }
172
+ }, {
173
+ $addFields: {
174
+ _joinData: {
175
+ $mergeObjects: [
176
+ { $ifNull: ['$_joinData', {}] },
177
+ { [operation.as]: `$${operation.as}_temp` }
178
+ ]
179
+ }
180
+ }
181
+ }, {
182
+ $project: {
183
+ [`${operation.as}_temp`]: 0
92
184
  }
93
185
  });
94
186
  if (isNestedField) {
95
187
  const [parentAlias] = operation.localField.split('.');
96
- const parentJoin = processedOperations.find(op => op instanceof Join && op.as === parentAlias);
188
+ const parentJoin = processedOperations.find(op => (op instanceof Join || op instanceof JoinMany || op instanceof JoinThrough || op instanceof JoinThroughMany) && op.as === parentAlias);
97
189
  if (parentJoin) {
98
190
  pipeline.push({
99
191
  $addFields: {
100
- [parentAlias]: {
192
+ _joinData: {
101
193
  $mergeObjects: [
102
- `$${parentAlias}`,
103
- { [operation.as]: `$${operation.as}` }
194
+ { $ifNull: ['$_joinData', {}] },
195
+ {
196
+ [parentAlias]: {
197
+ $mergeObjects: [
198
+ { $ifNull: [`$_joinData.${parentAlias}`, {}] },
199
+ { [operation.as]: `$_joinData.${operation.as}` }
200
+ ]
201
+ }
202
+ }
104
203
  ]
105
204
  }
106
205
  }
107
206
  });
108
207
  pipeline.push({
109
- $project: {
110
- [operation.as]: 0
111
- }
208
+ $unset: `_joinData.${operation.as}`
112
209
  });
113
210
  }
114
211
  }
@@ -117,9 +214,22 @@ export function convertOperationsToPipeline(operations) {
117
214
  pipeline.push({
118
215
  $lookup: {
119
216
  from: operation.from,
120
- localField: operation.localField,
217
+ localField: resolvedLocalField,
121
218
  foreignField: operation.foreignField,
122
- as: operation.as
219
+ as: `${operation.as}_temp`
220
+ }
221
+ }, {
222
+ $addFields: {
223
+ _joinData: {
224
+ $mergeObjects: [
225
+ { $ifNull: ['$_joinData', {}] },
226
+ { [operation.as]: `$${operation.as}_temp` }
227
+ ]
228
+ }
229
+ }
230
+ }, {
231
+ $project: {
232
+ [`${operation.as}_temp`]: 0
123
233
  }
124
234
  });
125
235
  }
@@ -127,15 +237,20 @@ export function convertOperationsToPipeline(operations) {
127
237
  else if (operation instanceof JoinThrough) {
128
238
  const needsObjectIdConversion = operation.foreignField === '_id';
129
239
  const isNestedField = operation.localField.includes('.');
240
+ const resolvedLocalField = resolveLocalFieldPath(operation.localField, processedOperations);
130
241
  if (needsObjectIdConversion || isNestedField) {
131
242
  pipeline.push({
132
243
  $lookup: {
133
244
  from: operation.through,
134
- let: { localId: { $cond: [
135
- { $eq: [{ $type: `$${operation.localField}` }, 'string'] },
136
- { $toObjectId: `$${operation.localField}` },
137
- `$${operation.localField}`
138
- ] } },
245
+ let: {
246
+ localId: {
247
+ $cond: [
248
+ { $eq: [{ $type: `$${resolvedLocalField}` }, 'string'] },
249
+ { $toObjectId: `$${resolvedLocalField}` },
250
+ `$${resolvedLocalField}`
251
+ ]
252
+ }
253
+ },
139
254
  pipeline: [
140
255
  {
141
256
  $match: {
@@ -165,11 +280,15 @@ export function convertOperationsToPipeline(operations) {
165
280
  }, {
166
281
  $lookup: {
167
282
  from: operation.from,
168
- let: { foreignId: { $cond: [
283
+ let: {
284
+ foreignId: {
285
+ $cond: [
169
286
  { $eq: [{ $type: `$${operation.as}_through.${operation.throughForeignField}` }, 'string'] },
170
287
  { $toObjectId: `$${operation.as}_through.${operation.throughForeignField}` },
171
288
  `$${operation.as}_through.${operation.throughForeignField}`
172
- ] } },
289
+ ]
290
+ }
291
+ },
173
292
  pipeline: [
174
293
  {
175
294
  $match: {
@@ -197,7 +316,12 @@ export function convertOperationsToPipeline(operations) {
197
316
  }
198
317
  }, {
199
318
  $addFields: {
200
- [operation.as]: `$${operation.as}_temp`
319
+ _joinData: {
320
+ $mergeObjects: [
321
+ { $ifNull: ['$_joinData', {}] },
322
+ { [operation.as]: `$${operation.as}_temp` }
323
+ ]
324
+ }
201
325
  }
202
326
  }, {
203
327
  $project: {
@@ -210,28 +334,36 @@ export function convertOperationsToPipeline(operations) {
210
334
  const parentJoin = processedOperations.find(op => (op instanceof Join || op instanceof JoinThrough) && op.as === parentAlias);
211
335
  if (parentJoin) {
212
336
  pipeline.push({
213
- $set: {
214
- [parentAlias]: {
337
+ $addFields: {
338
+ _joinData: {
215
339
  $mergeObjects: [
216
- { $ifNull: [`$${parentAlias}`, {}] },
217
- { [operation.as]: `$${operation.as}` }
340
+ { $ifNull: ['$_joinData', {}] },
341
+ {
342
+ [parentAlias]: {
343
+ $mergeObjects: [
344
+ { $ifNull: [`$_joinData.${parentAlias}`, {}] },
345
+ { [operation.as]: `$_joinData.${operation.as}` }
346
+ ]
347
+ }
348
+ }
218
349
  ]
219
350
  }
220
351
  }
221
352
  });
222
353
  pipeline.push({
223
- $unset: operation.as
354
+ $unset: `_joinData.${operation.as}`
224
355
  });
225
356
  }
226
357
  }
227
358
  }
228
359
  else {
229
360
  const isNestedFieldElse = operation.localField.includes('.');
361
+ const resolvedLocalFieldElse = resolveLocalFieldPath(operation.localField, processedOperations);
230
362
  if (isNestedFieldElse) {
231
363
  pipeline.push({
232
364
  $lookup: {
233
365
  from: operation.through,
234
- let: { localId: `$${operation.localField}` },
366
+ let: { localId: `$${resolvedLocalFieldElse}` },
235
367
  pipeline: [
236
368
  {
237
369
  $match: {
@@ -255,7 +387,7 @@ export function convertOperationsToPipeline(operations) {
255
387
  pipeline.push({
256
388
  $lookup: {
257
389
  from: operation.through,
258
- localField: operation.localField,
390
+ localField: resolvedLocalFieldElse,
259
391
  foreignField: operation.throughLocalField,
260
392
  as: `${operation.as}_through`
261
393
  }
@@ -280,7 +412,12 @@ export function convertOperationsToPipeline(operations) {
280
412
  }
281
413
  }, {
282
414
  $addFields: {
283
- [operation.as]: `$${operation.as}_temp`
415
+ _joinData: {
416
+ $mergeObjects: [
417
+ { $ifNull: ['$_joinData', {}] },
418
+ { [operation.as]: `$${operation.as}_temp` }
419
+ ]
420
+ }
284
421
  }
285
422
  }, {
286
423
  $project: {
@@ -294,18 +431,23 @@ export function convertOperationsToPipeline(operations) {
294
431
  if (parentJoin) {
295
432
  pipeline.push({
296
433
  $addFields: {
297
- [parentAlias]: {
434
+ _joinData: {
298
435
  $mergeObjects: [
299
- `$${parentAlias}`,
300
- { [operation.as]: `$${operation.as}` }
436
+ { $ifNull: ['$_joinData', {}] },
437
+ {
438
+ [parentAlias]: {
439
+ $mergeObjects: [
440
+ { $ifNull: [`$_joinData.${parentAlias}`, {}] },
441
+ { [operation.as]: `$_joinData.${operation.as}` }
442
+ ]
443
+ }
444
+ }
301
445
  ]
302
446
  }
303
447
  }
304
448
  });
305
449
  pipeline.push({
306
- $project: {
307
- [operation.as]: 0
308
- }
450
+ $unset: `_joinData.${operation.as}`
309
451
  });
310
452
  }
311
453
  }
@@ -314,15 +456,20 @@ export function convertOperationsToPipeline(operations) {
314
456
  else if (operation instanceof JoinThroughMany) {
315
457
  const needsObjectIdConversion = operation.foreignField === '_id';
316
458
  const isNestedField = operation.localField.includes('.');
459
+ const resolvedLocalField = resolveLocalFieldPath(operation.localField, processedOperations);
317
460
  if (needsObjectIdConversion || isNestedField) {
318
461
  pipeline.push({
319
462
  $lookup: {
320
463
  from: operation.through,
321
- let: { localId: { $cond: [
322
- { $eq: [{ $type: `$${operation.localField}` }, 'string'] },
323
- { $toObjectId: `$${operation.localField}` },
324
- `$${operation.localField}`
325
- ] } },
464
+ let: {
465
+ localId: {
466
+ $cond: [
467
+ { $eq: [{ $type: `$${resolvedLocalField}` }, 'string'] },
468
+ { $toObjectId: `$${resolvedLocalField}` },
469
+ `$${resolvedLocalField}`
470
+ ]
471
+ }
472
+ },
326
473
  pipeline: [
327
474
  {
328
475
  $match: {
@@ -351,11 +498,15 @@ export function convertOperationsToPipeline(operations) {
351
498
  }, {
352
499
  $lookup: {
353
500
  from: operation.from,
354
- let: { foreignId: { $cond: [
501
+ let: {
502
+ foreignId: {
503
+ $cond: [
355
504
  { $eq: [{ $type: `$${operation.as}_through.${operation.throughForeignField}` }, 'string'] },
356
505
  { $toObjectId: `$${operation.as}_through.${operation.throughForeignField}` },
357
506
  `$${operation.as}_through.${operation.throughForeignField}`
358
- ] } },
507
+ ]
508
+ }
509
+ },
359
510
  pipeline: [
360
511
  {
361
512
  $match: {
@@ -380,18 +531,29 @@ export function convertOperationsToPipeline(operations) {
380
531
  $group: {
381
532
  _id: '$_id',
382
533
  root: { $first: '$$ROOT' },
383
- [operation.as]: { $push: { $arrayElemAt: [`$${operation.as}_temp`, 0] } }
534
+ [`${operation.as}_temp_grouped`]: { $push: { $arrayElemAt: [`$${operation.as}_temp`, 0] } }
384
535
  }
385
536
  }, {
386
537
  $replaceRoot: {
387
538
  newRoot: {
388
- $mergeObjects: ['$root', { [operation.as]: `$${operation.as}` }]
539
+ $mergeObjects: [
540
+ '$root',
541
+ {
542
+ _joinData: {
543
+ $mergeObjects: [
544
+ { $ifNull: ['$root._joinData', {}] },
545
+ { [operation.as]: `$${operation.as}_temp_grouped` }
546
+ ]
547
+ }
548
+ }
549
+ ]
389
550
  }
390
551
  }
391
552
  }, {
392
553
  $project: {
393
554
  [`${operation.as}_through`]: 0,
394
- [`${operation.as}_temp`]: 0
555
+ [`${operation.as}_temp`]: 0,
556
+ [`${operation.as}_temp_grouped`]: 0
395
557
  }
396
558
  });
397
559
  if (isNestedField) {
@@ -399,28 +561,36 @@ export function convertOperationsToPipeline(operations) {
399
561
  const parentJoin = processedOperations.find(op => (op instanceof Join || op instanceof JoinThrough) && op.as === parentAlias);
400
562
  if (parentJoin) {
401
563
  pipeline.push({
402
- $set: {
403
- [parentAlias]: {
564
+ $addFields: {
565
+ _joinData: {
404
566
  $mergeObjects: [
405
- { $ifNull: [`$${parentAlias}`, {}] },
406
- { [operation.as]: `$${operation.as}` }
567
+ { $ifNull: ['$_joinData', {}] },
568
+ {
569
+ [parentAlias]: {
570
+ $mergeObjects: [
571
+ { $ifNull: [`$_joinData.${parentAlias}`, {}] },
572
+ { [operation.as]: `$_joinData.${operation.as}` }
573
+ ]
574
+ }
575
+ }
407
576
  ]
408
577
  }
409
578
  }
410
579
  });
411
580
  pipeline.push({
412
- $unset: operation.as
581
+ $unset: `_joinData.${operation.as}`
413
582
  });
414
583
  }
415
584
  }
416
585
  }
417
586
  else {
418
587
  const isNestedFieldElse = operation.localField.includes('.');
588
+ const resolvedLocalFieldElse = resolveLocalFieldPath(operation.localField, processedOperations);
419
589
  if (isNestedFieldElse) {
420
590
  pipeline.push({
421
591
  $lookup: {
422
592
  from: operation.through,
423
- let: { localId: `$${operation.localField}` },
593
+ let: { localId: `$${resolvedLocalFieldElse}` },
424
594
  pipeline: [
425
595
  {
426
596
  $match: {
@@ -443,7 +613,7 @@ export function convertOperationsToPipeline(operations) {
443
613
  pipeline.push({
444
614
  $lookup: {
445
615
  from: operation.through,
446
- localField: operation.localField,
616
+ localField: resolvedLocalFieldElse,
447
617
  foreignField: operation.throughLocalField,
448
618
  as: `${operation.as}_through`
449
619
  }
@@ -465,18 +635,29 @@ export function convertOperationsToPipeline(operations) {
465
635
  $group: {
466
636
  _id: '$_id',
467
637
  root: { $first: '$$ROOT' },
468
- [operation.as]: { $push: { $arrayElemAt: [`$${operation.as}_temp`, 0] } }
638
+ [`${operation.as}_temp_grouped`]: { $push: { $arrayElemAt: [`$${operation.as}_temp`, 0] } }
469
639
  }
470
640
  }, {
471
641
  $replaceRoot: {
472
642
  newRoot: {
473
- $mergeObjects: ['$root', { [operation.as]: `$${operation.as}` }]
643
+ $mergeObjects: [
644
+ '$root',
645
+ {
646
+ _joinData: {
647
+ $mergeObjects: [
648
+ { $ifNull: ['$root._joinData', {}] },
649
+ { [operation.as]: `$${operation.as}_temp_grouped` }
650
+ ]
651
+ }
652
+ }
653
+ ]
474
654
  }
475
655
  }
476
656
  }, {
477
657
  $project: {
478
658
  [`${operation.as}_through`]: 0,
479
- [`${operation.as}_temp`]: 0
659
+ [`${operation.as}_temp`]: 0,
660
+ [`${operation.as}_temp_grouped`]: 0
480
661
  }
481
662
  });
482
663
  if (isNestedFieldElse) {
@@ -485,18 +666,23 @@ export function convertOperationsToPipeline(operations) {
485
666
  if (parentJoin) {
486
667
  pipeline.push({
487
668
  $addFields: {
488
- [parentAlias]: {
669
+ _joinData: {
489
670
  $mergeObjects: [
490
- `$${parentAlias}`,
491
- { [operation.as]: `$${operation.as}` }
671
+ { $ifNull: ['$_joinData', {}] },
672
+ {
673
+ [parentAlias]: {
674
+ $mergeObjects: [
675
+ { $ifNull: [`$_joinData.${parentAlias}`, {}] },
676
+ { [operation.as]: `$_joinData.${operation.as}` }
677
+ ]
678
+ }
679
+ }
492
680
  ]
493
681
  }
494
682
  }
495
683
  });
496
684
  pipeline.push({
497
- $project: {
498
- [operation.as]: 0
499
- }
685
+ $unset: `_joinData.${operation.as}`
500
686
  });
501
687
  }
502
688
  }
@@ -83,6 +83,7 @@ export function transformJoinResults(rows, operations) {
83
83
  }
84
84
  return rows.map(row => {
85
85
  const transformed = {};
86
+ const joinData = {};
86
87
  for (const key of Object.keys(row)) {
87
88
  const hasJoinPrefix = joinOperations.some(join => key.startsWith(`${join.as}__`));
88
89
  const isJoinAlias = allJoinAliases.includes(key);
@@ -113,14 +114,14 @@ export function transformJoinResults(rows, operations) {
113
114
  const relatedJoin = joinOperations.find(j => j.as === tableAlias);
114
115
  const relatedJoinThrough = joinThroughOperations.find(j => j.as === tableAlias);
115
116
  let targetObject = null;
116
- if (relatedJoin && transformed[relatedJoin.as]) {
117
- targetObject = transformed[relatedJoin.as];
117
+ if (relatedJoin && joinData[relatedJoin.as]) {
118
+ targetObject = joinData[relatedJoin.as];
118
119
  }
119
- else if (relatedJoinThrough && transformed[relatedJoinThrough.as]) {
120
- targetObject = transformed[relatedJoinThrough.as];
120
+ else if (relatedJoinThrough && joinData[relatedJoinThrough.as]) {
121
+ targetObject = joinData[relatedJoinThrough.as];
121
122
  }
122
123
  else {
123
- const found = findNestedObject(transformed, tableAlias);
124
+ const found = findNestedObject(joinData, tableAlias);
124
125
  if (found) {
125
126
  targetObject = found.obj;
126
127
  }
@@ -129,11 +130,11 @@ export function transformJoinResults(rows, operations) {
129
130
  targetObject[operation.as] = hasAnyData ? joinedData : null;
130
131
  }
131
132
  else {
132
- transformed[operation.as] = hasAnyData ? joinedData : null;
133
+ joinData[operation.as] = hasAnyData ? joinedData : null;
133
134
  }
134
135
  }
135
136
  else {
136
- transformed[operation.as] = hasAnyData ? joinedData : null;
137
+ joinData[operation.as] = hasAnyData ? joinedData : null;
137
138
  }
138
139
  }
139
140
  else if (operation instanceof JoinThrough) {
@@ -143,11 +144,11 @@ export function transformJoinResults(rows, operations) {
143
144
  const relatedJoin = joinOperations.find(j => j.as === tableAlias);
144
145
  const relatedJoinThrough = joinThroughOperations.find(j => j.as === tableAlias);
145
146
  let targetObject = null;
146
- if (relatedJoin && transformed[relatedJoin.as]) {
147
- targetObject = transformed[relatedJoin.as];
147
+ if (relatedJoin && joinData[relatedJoin.as]) {
148
+ targetObject = joinData[relatedJoin.as];
148
149
  }
149
150
  else if (relatedJoinThrough) {
150
- const found = findNestedObject(transformed, relatedJoinThrough.as);
151
+ const found = findNestedObject(joinData, relatedJoinThrough.as);
151
152
  if (found) {
152
153
  targetObject = found.obj;
153
154
  }
@@ -156,11 +157,11 @@ export function transformJoinResults(rows, operations) {
156
157
  targetObject[operation.as] = jsonValue;
157
158
  }
158
159
  else {
159
- transformed[operation.as] = jsonValue;
160
+ joinData[operation.as] = jsonValue;
160
161
  }
161
162
  }
162
163
  else {
163
- transformed[operation.as] = jsonValue;
164
+ joinData[operation.as] = jsonValue;
164
165
  }
165
166
  }
166
167
  else if (operation instanceof JoinMany) {
@@ -191,30 +192,30 @@ export function transformJoinResults(rows, operations) {
191
192
  const relatedJoinThrough = joinThroughOperations.find(j => j.as === tableAlias);
192
193
  const relatedJoinThroughMany = joinThroughManyOperations.find(j => j.as === tableAlias);
193
194
  let targetObject = null;
194
- if (relatedJoin && transformed[relatedJoin.as]) {
195
- targetObject = transformed[relatedJoin.as];
195
+ if (relatedJoin && joinData[relatedJoin.as]) {
196
+ targetObject = joinData[relatedJoin.as];
196
197
  }
197
198
  else if (relatedJoinThrough) {
198
- const found = findNestedObject(transformed, relatedJoinThrough.as);
199
+ const found = findNestedObject(joinData, relatedJoinThrough.as);
199
200
  if (found) {
200
201
  targetObject = found.obj;
201
202
  }
202
203
  }
203
- else if (relatedJoinMany && transformed[relatedJoinMany.as]) {
204
- targetObject = transformed[relatedJoinMany.as];
204
+ else if (relatedJoinMany && joinData[relatedJoinMany.as]) {
205
+ targetObject = joinData[relatedJoinMany.as];
205
206
  }
206
- else if (relatedJoinThroughMany && transformed[relatedJoinThroughMany.as]) {
207
- targetObject = transformed[relatedJoinThroughMany.as];
207
+ else if (relatedJoinThroughMany && joinData[relatedJoinThroughMany.as]) {
208
+ targetObject = joinData[relatedJoinThroughMany.as];
208
209
  }
209
210
  if (targetObject) {
210
211
  targetObject[operation.as] = parsedValue;
211
212
  }
212
213
  else {
213
- transformed[operation.as] = parsedValue;
214
+ joinData[operation.as] = parsedValue;
214
215
  }
215
216
  }
216
217
  else {
217
- transformed[operation.as] = parsedValue;
218
+ joinData[operation.as] = parsedValue;
218
219
  }
219
220
  }
220
221
  else if (operation instanceof JoinThroughMany) {
@@ -245,33 +246,36 @@ export function transformJoinResults(rows, operations) {
245
246
  const relatedJoinThrough = joinThroughOperations.find(j => j.as === tableAlias);
246
247
  const relatedJoinThroughMany = joinThroughManyOperations.find(j => j.as === tableAlias);
247
248
  let targetObject = null;
248
- if (relatedJoin && transformed[relatedJoin.as]) {
249
- targetObject = transformed[relatedJoin.as];
249
+ if (relatedJoin && joinData[relatedJoin.as]) {
250
+ targetObject = joinData[relatedJoin.as];
250
251
  }
251
252
  else if (relatedJoinThrough) {
252
- const found = findNestedObject(transformed, relatedJoinThrough.as);
253
+ const found = findNestedObject(joinData, relatedJoinThrough.as);
253
254
  if (found) {
254
255
  targetObject = found.obj;
255
256
  }
256
257
  }
257
- else if (relatedJoinMany && transformed[relatedJoinMany.as]) {
258
- targetObject = transformed[relatedJoinMany.as];
258
+ else if (relatedJoinMany && joinData[relatedJoinMany.as]) {
259
+ targetObject = joinData[relatedJoinMany.as];
259
260
  }
260
- else if (relatedJoinThroughMany && transformed[relatedJoinThroughMany.as]) {
261
- targetObject = transformed[relatedJoinThroughMany.as];
261
+ else if (relatedJoinThroughMany && joinData[relatedJoinThroughMany.as]) {
262
+ targetObject = joinData[relatedJoinThroughMany.as];
262
263
  }
263
264
  if (targetObject) {
264
265
  targetObject[operation.as] = parsedValue;
265
266
  }
266
267
  else {
267
- transformed[operation.as] = parsedValue;
268
+ joinData[operation.as] = parsedValue;
268
269
  }
269
270
  }
270
271
  else {
271
- transformed[operation.as] = parsedValue;
272
+ joinData[operation.as] = parsedValue;
272
273
  }
273
274
  }
274
275
  }
276
+ if (Object.keys(joinData).length > 0) {
277
+ transformed._joinData = joinData;
278
+ }
275
279
  return transformed;
276
280
  });
277
281
  }
@@ -3,6 +3,7 @@ import { IUserContext, IEntity, IPagedResult, IQueryOptions } from '@loomcore/co
3
3
  import type { AppIdType } from '@loomcore/common/types';
4
4
  import { DeleteResult } from '../../databases/models/delete-result.js';
5
5
  import { Operation } from '../../databases/operations/operation.js';
6
+ import { PostProcessEntityCustomFunction, PrepareQueryCustomFunction } from '../../controllers/types.js';
6
7
  export interface IGenericApiService<T extends IEntity> {
7
8
  validate(doc: any, isPartial?: boolean): ValueError[] | null;
8
9
  validateMany(docs: any[], isPartial?: boolean): ValueError[] | null;
@@ -13,8 +14,11 @@ export interface IGenericApiService<T extends IEntity> {
13
14
  preProcessEntity(userContext: IUserContext, entity: Partial<T>, isCreate: boolean, allowId: boolean): Promise<Partial<T>>;
14
15
  postProcessEntity(userContext: IUserContext, entity: T): T;
15
16
  getAll(userContext: IUserContext): Promise<T[]>;
16
- get(userContext: IUserContext, queryOptions: IQueryOptions): Promise<IPagedResult<T>>;
17
+ getAll<TCustom extends IEntity>(userContext: IUserContext, prepareQueryCustom: PrepareQueryCustomFunction, postProcessEntityCustom: PostProcessEntityCustomFunction<T, TCustom>): Promise<TCustom[]>;
18
+ get(userContext: IUserContext, queryOptions?: IQueryOptions): Promise<IPagedResult<T>>;
19
+ get<TCustom extends IEntity>(userContext: IUserContext, queryOptions: IQueryOptions, prepareQueryCustom: PrepareQueryCustomFunction, postProcessEntityCustom: PostProcessEntityCustomFunction<T, TCustom>): Promise<IPagedResult<TCustom>>;
17
20
  getById(userContext: IUserContext, id: AppIdType): Promise<T>;
21
+ getById<TCustom extends IEntity>(userContext: IUserContext, id: AppIdType, prepareQueryCustom: PrepareQueryCustomFunction, postProcessEntityCustom: PostProcessEntityCustomFunction<T, TCustom>): Promise<TCustom>;
18
22
  getCount(userContext: IUserContext): Promise<number>;
19
23
  create(userContext: IUserContext, entity: Partial<T>): Promise<T | null>;
20
24
  createMany(userContext: IUserContext, entities: Partial<T>[]): Promise<T[]>;
@@ -5,6 +5,7 @@ import { IGenericApiService } from './generic-api-service.interface.js';
5
5
  import { Operation } from '../../databases/operations/operation.js';
6
6
  import { DeleteResult } from '../../databases/models/delete-result.js';
7
7
  import { IDatabase } from '../../databases/models/index.js';
8
+ import { PostProcessEntityCustomFunction, PrepareQueryCustomFunction } from '../../controllers/types.js';
8
9
  export declare class GenericApiService<T extends IEntity> implements IGenericApiService<T> {
9
10
  protected database: IDatabase;
10
11
  protected pluralResourceName: string;
@@ -12,6 +13,7 @@ export declare class GenericApiService<T extends IEntity> implements IGenericApi
12
13
  protected modelSpec: IModelSpec;
13
14
  constructor(database: IDatabase, pluralResourceName: string, singularResourceName: string, modelSpec: IModelSpec);
14
15
  getAll(userContext: IUserContext): Promise<T[]>;
16
+ getAll<TCustom extends IEntity>(userContext: IUserContext, prepareQueryCustom: PrepareQueryCustomFunction, postProcessEntityCustom: PostProcessEntityCustomFunction<T, TCustom>): Promise<TCustom[]>;
15
17
  prepareQuery(userContext: IUserContext | undefined, queryObject: IQueryOptions, operations: Operation[]): {
16
18
  queryObject: IQueryOptions;
17
19
  operations: Operation[];
@@ -21,8 +23,10 @@ export declare class GenericApiService<T extends IEntity> implements IGenericApi
21
23
  preProcessEntity(userContext: IUserContext, entity: Partial<T>, isCreate: boolean, allowId?: boolean): Promise<Partial<T>>;
22
24
  postProcessEntity(userContext: IUserContext, entity: T): T;
23
25
  get(userContext: IUserContext, queryOptions?: IQueryOptions): Promise<IPagedResult<T>>;
26
+ get<TCustom extends IEntity>(userContext: IUserContext, queryOptions: IQueryOptions, prepareQueryCustom: PrepareQueryCustomFunction, postProcessEntityCustom: PostProcessEntityCustomFunction<T, TCustom>): Promise<IPagedResult<TCustom>>;
24
27
  protected prepareQueryOptions(userContext: IUserContext | undefined, queryOptions: IQueryOptions): IQueryOptions;
25
28
  getById(userContext: IUserContext, id: AppIdType): Promise<T>;
29
+ getById<TCustom extends IEntity>(userContext: IUserContext, id: AppIdType, prepareQueryCustom: PrepareQueryCustomFunction, postProcessEntityCustom: PostProcessEntityCustomFunction<T, TCustom>): Promise<TCustom>;
26
30
  getCount(userContext: IUserContext): Promise<number>;
27
31
  create(userContext: IUserContext, entity: Partial<T>): Promise<T | null>;
28
32
  createMany(userContext: IUserContext, entities: Partial<T>[]): Promise<T[]>;
@@ -16,10 +16,22 @@ export class GenericApiService {
16
16
  this.modelSpec = modelSpec;
17
17
  this.database = database;
18
18
  }
19
- async getAll(userContext) {
20
- const { operations } = this.prepareQuery(userContext, {}, []);
19
+ async getAll(userContext, prepareQueryCustom, postProcessEntityCustom) {
20
+ let operations = [];
21
+ if (prepareQueryCustom) {
22
+ operations = prepareQueryCustom(userContext, {}, []).operations;
23
+ }
24
+ else {
25
+ operations = this.prepareQuery(userContext, {}, []).operations;
26
+ }
21
27
  const entities = await this.database.getAll(operations, this.pluralResourceName);
22
- return entities.map(entity => this.postProcessEntity(userContext, entity));
28
+ return entities.map(entity => {
29
+ const dbProcessed = this.postProcessEntity(userContext, entity);
30
+ if (postProcessEntityCustom) {
31
+ return postProcessEntityCustom(userContext, dbProcessed);
32
+ }
33
+ return dbProcessed;
34
+ });
23
35
  }
24
36
  prepareQuery(userContext, queryObject, operations) {
25
37
  return { queryObject, operations };
@@ -63,11 +75,23 @@ export class GenericApiService {
63
75
  postProcessEntity(userContext, entity) {
64
76
  return this.database.postProcessEntity(entity, this.modelSpec.fullSchema);
65
77
  }
66
- async get(userContext, queryOptions = { ...DefaultQueryOptions }) {
78
+ async get(userContext, queryOptions = { ...DefaultQueryOptions }, prepareQueryCustom, postProcessEntityCustom) {
67
79
  const preparedOptions = this.prepareQueryOptions(userContext, queryOptions);
68
- const { operations } = this.prepareQuery(userContext, {}, []);
80
+ let operations = [];
81
+ if (prepareQueryCustom) {
82
+ operations = prepareQueryCustom(userContext, {}, []).operations;
83
+ }
84
+ else {
85
+ operations = this.prepareQuery(userContext, {}, []).operations;
86
+ }
69
87
  const pagedResult = await this.database.get(operations, preparedOptions, this.modelSpec, this.pluralResourceName);
70
- const transformedEntities = (pagedResult.entities || []).map(entity => this.postProcessEntity(userContext, entity));
88
+ const transformedEntities = (pagedResult.entities || []).map(entity => {
89
+ const transformedEntity = this.postProcessEntity(userContext, entity);
90
+ if (postProcessEntityCustom) {
91
+ return postProcessEntityCustom(userContext, transformedEntity);
92
+ }
93
+ return transformedEntity;
94
+ });
71
95
  return {
72
96
  ...pagedResult,
73
97
  entities: transformedEntities
@@ -76,13 +100,30 @@ export class GenericApiService {
76
100
  prepareQueryOptions(userContext, queryOptions) {
77
101
  return queryOptions;
78
102
  }
79
- async getById(userContext, id) {
80
- const { operations, queryObject } = this.prepareQuery(userContext, {}, []);
103
+ async getById(userContext, id, prepareQueryCustom, postProcessEntityCustom) {
104
+ let operations = [];
105
+ let queryObject = {};
106
+ if (prepareQueryCustom) {
107
+ const result = prepareQueryCustom(userContext, {}, []);
108
+ operations = result.operations;
109
+ queryObject = result.queryObject;
110
+ }
111
+ else {
112
+ const result = this.prepareQuery(userContext, {}, []);
113
+ operations = result.operations;
114
+ queryObject = result.queryObject;
115
+ }
81
116
  const entity = await this.database.getById(operations, queryObject, id, this.pluralResourceName);
82
117
  if (!entity) {
83
118
  throw new IdNotFoundError();
84
119
  }
85
- return this.postProcessEntity(userContext, entity);
120
+ const transformedEntity = this.postProcessEntity(userContext, entity);
121
+ if (postProcessEntityCustom) {
122
+ return postProcessEntityCustom(userContext, transformedEntity);
123
+ }
124
+ else {
125
+ return transformedEntity;
126
+ }
86
127
  }
87
128
  async getCount(userContext) {
88
129
  this.prepareQuery(userContext, {}, []);
@@ -5,14 +5,21 @@ function apiResponse(response, status, options = {}, modelSpec, publicSpec) {
5
5
  const specForEncoding = publicSpec ?? modelSpec;
6
6
  if (specForEncoding && options.data) {
7
7
  if (Array.isArray(options.data)) {
8
- options.data = options.data.map((item) => specForEncoding.encode(item));
8
+ options.data = options.data.map((item) => {
9
+ delete item._joinData;
10
+ return specForEncoding.encode(item);
11
+ });
9
12
  }
10
13
  else if (typeof options.data === 'object' && options.data !== null && 'entities' in options.data && Array.isArray(options.data.entities)) {
11
14
  const pagedResult = options.data;
12
- pagedResult.entities = pagedResult.entities.map((item) => specForEncoding.encode(item));
15
+ pagedResult.entities = pagedResult.entities.map((item) => {
16
+ delete item._joinData;
17
+ return specForEncoding?.encode(item);
18
+ });
13
19
  options.data = pagedResult;
14
20
  }
15
21
  else {
22
+ delete options.data._joinData;
16
23
  const encodedData = specForEncoding.encode(options.data);
17
24
  options.data = encodedData;
18
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loomcore/api",
3
- "version": "0.1.90",
3
+ "version": "0.1.92",
4
4
  "private": false,
5
5
  "description": "Loom Core Api - An opinionated Node.js api using Typescript, Express, and MongoDb or PostgreSQL",
6
6
  "scripts": {
@@ -57,7 +57,7 @@
57
57
  "qs": "^6.15.0"
58
58
  },
59
59
  "peerDependencies": {
60
- "@loomcore/common": "^0.0.50",
60
+ "@loomcore/common": "^0.0.51",
61
61
  "@sinclair/typebox": "0.34.33",
62
62
  "cookie-parser": "^1.4.6",
63
63
  "cors": "^2.8.5",