@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.
- package/dist/__tests__/common-test.utils.d.ts +4 -7
- package/dist/__tests__/common-test.utils.js +59 -25
- package/dist/__tests__/models/product-with-category.model.d.ts +21 -0
- package/dist/__tests__/models/product-with-category.model.js +12 -0
- package/dist/__tests__/models/product.model.d.ts +0 -2
- package/dist/controllers/index.d.ts +1 -0
- package/dist/controllers/index.js +1 -0
- package/dist/controllers/types.d.ts +7 -0
- package/dist/controllers/types.js +1 -0
- package/dist/databases/mongo-db/utils/convert-operations-to-pipeline.util.js +257 -71
- package/dist/databases/postgres/utils/transform-join-results.js +34 -30
- package/dist/services/generic-api-service/generic-api-service.interface.d.ts +5 -1
- package/dist/services/generic-api-service/generic-api.service.d.ts +4 -0
- package/dist/services/generic-api-service/generic-api.service.js +50 -9
- package/dist/utils/api.utils.js +9 -2
- package/package.json +2 -2
|
@@ -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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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;
|
|
@@ -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: {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
`$${
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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: {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
`$${
|
|
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
|
-
|
|
192
|
+
_joinData: {
|
|
101
193
|
$mergeObjects: [
|
|
102
|
-
|
|
103
|
-
{
|
|
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
|
-
$
|
|
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:
|
|
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: {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
`$${
|
|
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: {
|
|
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
|
-
|
|
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
|
-
$
|
|
214
|
-
|
|
337
|
+
$addFields: {
|
|
338
|
+
_joinData: {
|
|
215
339
|
$mergeObjects: [
|
|
216
|
-
{ $ifNull: [
|
|
217
|
-
{
|
|
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: `$${
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
434
|
+
_joinData: {
|
|
298
435
|
$mergeObjects: [
|
|
299
|
-
|
|
300
|
-
{
|
|
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
|
-
$
|
|
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: {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
`$${
|
|
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: {
|
|
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: [
|
|
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
|
-
$
|
|
403
|
-
|
|
564
|
+
$addFields: {
|
|
565
|
+
_joinData: {
|
|
404
566
|
$mergeObjects: [
|
|
405
|
-
{ $ifNull: [
|
|
406
|
-
{
|
|
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: `$${
|
|
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:
|
|
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: [
|
|
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
|
-
|
|
669
|
+
_joinData: {
|
|
489
670
|
$mergeObjects: [
|
|
490
|
-
|
|
491
|
-
{
|
|
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
|
-
$
|
|
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 &&
|
|
117
|
-
targetObject =
|
|
117
|
+
if (relatedJoin && joinData[relatedJoin.as]) {
|
|
118
|
+
targetObject = joinData[relatedJoin.as];
|
|
118
119
|
}
|
|
119
|
-
else if (relatedJoinThrough &&
|
|
120
|
-
targetObject =
|
|
120
|
+
else if (relatedJoinThrough && joinData[relatedJoinThrough.as]) {
|
|
121
|
+
targetObject = joinData[relatedJoinThrough.as];
|
|
121
122
|
}
|
|
122
123
|
else {
|
|
123
|
-
const found = findNestedObject(
|
|
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
|
-
|
|
133
|
+
joinData[operation.as] = hasAnyData ? joinedData : null;
|
|
133
134
|
}
|
|
134
135
|
}
|
|
135
136
|
else {
|
|
136
|
-
|
|
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 &&
|
|
147
|
-
targetObject =
|
|
147
|
+
if (relatedJoin && joinData[relatedJoin.as]) {
|
|
148
|
+
targetObject = joinData[relatedJoin.as];
|
|
148
149
|
}
|
|
149
150
|
else if (relatedJoinThrough) {
|
|
150
|
-
const found = findNestedObject(
|
|
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
|
-
|
|
160
|
+
joinData[operation.as] = jsonValue;
|
|
160
161
|
}
|
|
161
162
|
}
|
|
162
163
|
else {
|
|
163
|
-
|
|
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 &&
|
|
195
|
-
targetObject =
|
|
195
|
+
if (relatedJoin && joinData[relatedJoin.as]) {
|
|
196
|
+
targetObject = joinData[relatedJoin.as];
|
|
196
197
|
}
|
|
197
198
|
else if (relatedJoinThrough) {
|
|
198
|
-
const found = findNestedObject(
|
|
199
|
+
const found = findNestedObject(joinData, relatedJoinThrough.as);
|
|
199
200
|
if (found) {
|
|
200
201
|
targetObject = found.obj;
|
|
201
202
|
}
|
|
202
203
|
}
|
|
203
|
-
else if (relatedJoinMany &&
|
|
204
|
-
targetObject =
|
|
204
|
+
else if (relatedJoinMany && joinData[relatedJoinMany.as]) {
|
|
205
|
+
targetObject = joinData[relatedJoinMany.as];
|
|
205
206
|
}
|
|
206
|
-
else if (relatedJoinThroughMany &&
|
|
207
|
-
targetObject =
|
|
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
|
-
|
|
214
|
+
joinData[operation.as] = parsedValue;
|
|
214
215
|
}
|
|
215
216
|
}
|
|
216
217
|
else {
|
|
217
|
-
|
|
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 &&
|
|
249
|
-
targetObject =
|
|
249
|
+
if (relatedJoin && joinData[relatedJoin.as]) {
|
|
250
|
+
targetObject = joinData[relatedJoin.as];
|
|
250
251
|
}
|
|
251
252
|
else if (relatedJoinThrough) {
|
|
252
|
-
const found = findNestedObject(
|
|
253
|
+
const found = findNestedObject(joinData, relatedJoinThrough.as);
|
|
253
254
|
if (found) {
|
|
254
255
|
targetObject = found.obj;
|
|
255
256
|
}
|
|
256
257
|
}
|
|
257
|
-
else if (relatedJoinMany &&
|
|
258
|
-
targetObject =
|
|
258
|
+
else if (relatedJoinMany && joinData[relatedJoinMany.as]) {
|
|
259
|
+
targetObject = joinData[relatedJoinMany.as];
|
|
259
260
|
}
|
|
260
|
-
else if (relatedJoinThroughMany &&
|
|
261
|
-
targetObject =
|
|
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
|
-
|
|
268
|
+
joinData[operation.as] = parsedValue;
|
|
268
269
|
}
|
|
269
270
|
}
|
|
270
271
|
else {
|
|
271
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =>
|
|
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
|
-
|
|
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 =>
|
|
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
|
-
|
|
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
|
-
|
|
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, {}, []);
|
package/dist/utils/api.utils.js
CHANGED
|
@@ -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) =>
|
|
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) =>
|
|
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.
|
|
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.
|
|
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",
|