@loomcore/api 0.1.89 → 0.1.91
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/migrations/migration-runner.js +2 -2
- package/dist/databases/mongo-db/utils/convert-operations-to-pipeline.util.js +156 -72
- 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 {};
|
|
@@ -66,7 +66,7 @@ export class MigrationRunner {
|
|
|
66
66
|
this.dbConnection = pool;
|
|
67
67
|
return new Umzug({
|
|
68
68
|
migrations: async () => {
|
|
69
|
-
const initialSchema = getPostgresInitialSchema(this.config).map(m => ({
|
|
69
|
+
const initialSchema = getPostgresInitialSchema(this.config, this.resetConfig).map(m => ({
|
|
70
70
|
name: m.name,
|
|
71
71
|
up: async () => {
|
|
72
72
|
console.log(` Running [LIBRARY] ${m.name}...`);
|
|
@@ -105,7 +105,7 @@ export class MigrationRunner {
|
|
|
105
105
|
console.log(`🔎 Looking for migrations in: ${globPattern}`);
|
|
106
106
|
return new Umzug({
|
|
107
107
|
migrations: async () => {
|
|
108
|
-
const initialSchema = getMongoInitialSchema(this.config).map(m => ({
|
|
108
|
+
const initialSchema = getMongoInitialSchema(this.config, this.resetConfig).map(m => ({
|
|
109
109
|
name: m.name,
|
|
110
110
|
up: async () => {
|
|
111
111
|
console.log(` Running [LIBRARY] ${m.name}...`);
|
|
@@ -2,21 +2,49 @@ 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);
|
|
23
|
+
if (joinAliases.length > 0) {
|
|
24
|
+
pipeline.push({
|
|
25
|
+
$set: {
|
|
26
|
+
_joinData: {}
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
8
30
|
operations.forEach(operation => {
|
|
9
31
|
if (operation instanceof Join) {
|
|
10
32
|
const needsObjectIdConversion = operation.foreignField === '_id';
|
|
33
|
+
const isNestedField = operation.localField.includes('.');
|
|
34
|
+
const resolvedLocalField = resolveLocalFieldPath(operation.localField, processedOperations);
|
|
11
35
|
if (needsObjectIdConversion) {
|
|
12
36
|
pipeline.push({
|
|
13
37
|
$lookup: {
|
|
14
38
|
from: operation.from,
|
|
15
|
-
let: {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
`$${
|
|
19
|
-
|
|
39
|
+
let: {
|
|
40
|
+
localId: {
|
|
41
|
+
$cond: [
|
|
42
|
+
{ $eq: [{ $type: `$${resolvedLocalField}` }, 'string'] },
|
|
43
|
+
{ $toObjectId: `$${resolvedLocalField}` },
|
|
44
|
+
`$${resolvedLocalField}`
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
},
|
|
20
48
|
pipeline: [
|
|
21
49
|
{
|
|
22
50
|
$match: {
|
|
@@ -35,7 +63,7 @@ export function convertOperationsToPipeline(operations) {
|
|
|
35
63
|
}
|
|
36
64
|
}, {
|
|
37
65
|
$addFields: {
|
|
38
|
-
[operation.as]: `$${operation.as}Arr`
|
|
66
|
+
[`_joinData.${operation.as}`]: `$${operation.as}Arr`
|
|
39
67
|
}
|
|
40
68
|
}, {
|
|
41
69
|
$project: {
|
|
@@ -47,7 +75,7 @@ export function convertOperationsToPipeline(operations) {
|
|
|
47
75
|
pipeline.push({
|
|
48
76
|
$lookup: {
|
|
49
77
|
from: operation.from,
|
|
50
|
-
localField:
|
|
78
|
+
localField: resolvedLocalField,
|
|
51
79
|
foreignField: operation.foreignField,
|
|
52
80
|
as: `${operation.as}Arr`
|
|
53
81
|
}
|
|
@@ -58,7 +86,7 @@ export function convertOperationsToPipeline(operations) {
|
|
|
58
86
|
}
|
|
59
87
|
}, {
|
|
60
88
|
$addFields: {
|
|
61
|
-
[operation.as]: `$${operation.as}Arr`
|
|
89
|
+
[`_joinData.${operation.as}`]: `$${operation.as}Arr`
|
|
62
90
|
}
|
|
63
91
|
}, {
|
|
64
92
|
$project: {
|
|
@@ -66,19 +94,43 @@ export function convertOperationsToPipeline(operations) {
|
|
|
66
94
|
}
|
|
67
95
|
});
|
|
68
96
|
}
|
|
97
|
+
if (isNestedField) {
|
|
98
|
+
const [parentAlias] = operation.localField.split('.');
|
|
99
|
+
const parentJoin = processedOperations.find(op => (op instanceof Join || op instanceof JoinMany || op instanceof JoinThrough || op instanceof JoinThroughMany) && op.as === parentAlias);
|
|
100
|
+
if (parentJoin) {
|
|
101
|
+
pipeline.push({
|
|
102
|
+
$set: {
|
|
103
|
+
[`_joinData.${parentAlias}`]: {
|
|
104
|
+
$mergeObjects: [
|
|
105
|
+
{ $ifNull: [`$_joinData.${parentAlias}`, {}] },
|
|
106
|
+
{ [operation.as]: `$_joinData.${operation.as}` }
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
pipeline.push({
|
|
112
|
+
$unset: `_joinData.${operation.as}`
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
69
116
|
}
|
|
70
117
|
else if (operation instanceof JoinMany) {
|
|
71
118
|
const needsObjectIdConversion = operation.foreignField === '_id';
|
|
72
119
|
const isNestedField = operation.localField.includes('.');
|
|
120
|
+
const resolvedLocalField = resolveLocalFieldPath(operation.localField, processedOperations);
|
|
73
121
|
if (needsObjectIdConversion || isNestedField) {
|
|
74
122
|
pipeline.push({
|
|
75
123
|
$lookup: {
|
|
76
124
|
from: operation.from,
|
|
77
|
-
let: {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
`$${
|
|
81
|
-
|
|
125
|
+
let: {
|
|
126
|
+
localId: {
|
|
127
|
+
$cond: [
|
|
128
|
+
{ $eq: [{ $type: `$${resolvedLocalField}` }, 'string'] },
|
|
129
|
+
{ $toObjectId: `$${resolvedLocalField}` },
|
|
130
|
+
`$${resolvedLocalField}`
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
},
|
|
82
134
|
pipeline: [
|
|
83
135
|
{
|
|
84
136
|
$match: {
|
|
@@ -88,27 +140,33 @@ export function convertOperationsToPipeline(operations) {
|
|
|
88
140
|
}
|
|
89
141
|
}
|
|
90
142
|
],
|
|
91
|
-
as: operation.as
|
|
143
|
+
as: `${operation.as}_temp`
|
|
144
|
+
}
|
|
145
|
+
}, {
|
|
146
|
+
$addFields: {
|
|
147
|
+
[`_joinData.${operation.as}`]: `$${operation.as}_temp`
|
|
148
|
+
}
|
|
149
|
+
}, {
|
|
150
|
+
$project: {
|
|
151
|
+
[`${operation.as}_temp`]: 0
|
|
92
152
|
}
|
|
93
153
|
});
|
|
94
154
|
if (isNestedField) {
|
|
95
155
|
const [parentAlias] = operation.localField.split('.');
|
|
96
|
-
const parentJoin = processedOperations.find(op => op instanceof Join && op.as === parentAlias);
|
|
156
|
+
const parentJoin = processedOperations.find(op => (op instanceof Join || op instanceof JoinMany || op instanceof JoinThrough || op instanceof JoinThroughMany) && op.as === parentAlias);
|
|
97
157
|
if (parentJoin) {
|
|
98
158
|
pipeline.push({
|
|
99
|
-
$
|
|
100
|
-
[parentAlias]: {
|
|
159
|
+
$set: {
|
|
160
|
+
[`_joinData.${parentAlias}`]: {
|
|
101
161
|
$mergeObjects: [
|
|
102
|
-
|
|
103
|
-
{ [operation.as]:
|
|
162
|
+
{ $ifNull: [`$_joinData.${parentAlias}`, {}] },
|
|
163
|
+
{ [operation.as]: `$_joinData.${operation.as}` }
|
|
104
164
|
]
|
|
105
165
|
}
|
|
106
166
|
}
|
|
107
167
|
});
|
|
108
168
|
pipeline.push({
|
|
109
|
-
$
|
|
110
|
-
[operation.as]: 0
|
|
111
|
-
}
|
|
169
|
+
$unset: `_joinData.${operation.as}`
|
|
112
170
|
});
|
|
113
171
|
}
|
|
114
172
|
}
|
|
@@ -117,9 +175,17 @@ export function convertOperationsToPipeline(operations) {
|
|
|
117
175
|
pipeline.push({
|
|
118
176
|
$lookup: {
|
|
119
177
|
from: operation.from,
|
|
120
|
-
localField:
|
|
178
|
+
localField: resolvedLocalField,
|
|
121
179
|
foreignField: operation.foreignField,
|
|
122
|
-
as: operation.as
|
|
180
|
+
as: `${operation.as}_temp`
|
|
181
|
+
}
|
|
182
|
+
}, {
|
|
183
|
+
$addFields: {
|
|
184
|
+
[`_joinData.${operation.as}`]: `$${operation.as}_temp`
|
|
185
|
+
}
|
|
186
|
+
}, {
|
|
187
|
+
$project: {
|
|
188
|
+
[`${operation.as}_temp`]: 0
|
|
123
189
|
}
|
|
124
190
|
});
|
|
125
191
|
}
|
|
@@ -127,15 +193,20 @@ export function convertOperationsToPipeline(operations) {
|
|
|
127
193
|
else if (operation instanceof JoinThrough) {
|
|
128
194
|
const needsObjectIdConversion = operation.foreignField === '_id';
|
|
129
195
|
const isNestedField = operation.localField.includes('.');
|
|
196
|
+
const resolvedLocalField = resolveLocalFieldPath(operation.localField, processedOperations);
|
|
130
197
|
if (needsObjectIdConversion || isNestedField) {
|
|
131
198
|
pipeline.push({
|
|
132
199
|
$lookup: {
|
|
133
200
|
from: operation.through,
|
|
134
|
-
let: {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
`$${
|
|
138
|
-
|
|
201
|
+
let: {
|
|
202
|
+
localId: {
|
|
203
|
+
$cond: [
|
|
204
|
+
{ $eq: [{ $type: `$${resolvedLocalField}` }, 'string'] },
|
|
205
|
+
{ $toObjectId: `$${resolvedLocalField}` },
|
|
206
|
+
`$${resolvedLocalField}`
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
},
|
|
139
210
|
pipeline: [
|
|
140
211
|
{
|
|
141
212
|
$match: {
|
|
@@ -165,11 +236,15 @@ export function convertOperationsToPipeline(operations) {
|
|
|
165
236
|
}, {
|
|
166
237
|
$lookup: {
|
|
167
238
|
from: operation.from,
|
|
168
|
-
let: {
|
|
239
|
+
let: {
|
|
240
|
+
foreignId: {
|
|
241
|
+
$cond: [
|
|
169
242
|
{ $eq: [{ $type: `$${operation.as}_through.${operation.throughForeignField}` }, 'string'] },
|
|
170
243
|
{ $toObjectId: `$${operation.as}_through.${operation.throughForeignField}` },
|
|
171
244
|
`$${operation.as}_through.${operation.throughForeignField}`
|
|
172
|
-
]
|
|
245
|
+
]
|
|
246
|
+
}
|
|
247
|
+
},
|
|
173
248
|
pipeline: [
|
|
174
249
|
{
|
|
175
250
|
$match: {
|
|
@@ -197,7 +272,7 @@ export function convertOperationsToPipeline(operations) {
|
|
|
197
272
|
}
|
|
198
273
|
}, {
|
|
199
274
|
$addFields: {
|
|
200
|
-
[operation.as]: `$${operation.as}_temp`
|
|
275
|
+
[`_joinData.${operation.as}`]: `$${operation.as}_temp`
|
|
201
276
|
}
|
|
202
277
|
}, {
|
|
203
278
|
$project: {
|
|
@@ -211,27 +286,28 @@ export function convertOperationsToPipeline(operations) {
|
|
|
211
286
|
if (parentJoin) {
|
|
212
287
|
pipeline.push({
|
|
213
288
|
$set: {
|
|
214
|
-
[parentAlias]: {
|
|
289
|
+
[`_joinData.${parentAlias}`]: {
|
|
215
290
|
$mergeObjects: [
|
|
216
|
-
{ $ifNull: [
|
|
217
|
-
{ [operation.as]:
|
|
291
|
+
{ $ifNull: [`$_joinData.${parentAlias}`, {}] },
|
|
292
|
+
{ [operation.as]: `$_joinData.${operation.as}` }
|
|
218
293
|
]
|
|
219
294
|
}
|
|
220
295
|
}
|
|
221
296
|
});
|
|
222
297
|
pipeline.push({
|
|
223
|
-
$unset: operation.as
|
|
298
|
+
$unset: `_joinData.${operation.as}`
|
|
224
299
|
});
|
|
225
300
|
}
|
|
226
301
|
}
|
|
227
302
|
}
|
|
228
303
|
else {
|
|
229
304
|
const isNestedFieldElse = operation.localField.includes('.');
|
|
305
|
+
const resolvedLocalFieldElse = resolveLocalFieldPath(operation.localField, processedOperations);
|
|
230
306
|
if (isNestedFieldElse) {
|
|
231
307
|
pipeline.push({
|
|
232
308
|
$lookup: {
|
|
233
309
|
from: operation.through,
|
|
234
|
-
let: { localId: `$${
|
|
310
|
+
let: { localId: `$${resolvedLocalFieldElse}` },
|
|
235
311
|
pipeline: [
|
|
236
312
|
{
|
|
237
313
|
$match: {
|
|
@@ -255,7 +331,7 @@ export function convertOperationsToPipeline(operations) {
|
|
|
255
331
|
pipeline.push({
|
|
256
332
|
$lookup: {
|
|
257
333
|
from: operation.through,
|
|
258
|
-
localField:
|
|
334
|
+
localField: resolvedLocalFieldElse,
|
|
259
335
|
foreignField: operation.throughLocalField,
|
|
260
336
|
as: `${operation.as}_through`
|
|
261
337
|
}
|
|
@@ -280,7 +356,7 @@ export function convertOperationsToPipeline(operations) {
|
|
|
280
356
|
}
|
|
281
357
|
}, {
|
|
282
358
|
$addFields: {
|
|
283
|
-
[operation.as]: `$${operation.as}_temp`
|
|
359
|
+
[`_joinData.${operation.as}`]: `$${operation.as}_temp`
|
|
284
360
|
}
|
|
285
361
|
}, {
|
|
286
362
|
$project: {
|
|
@@ -293,19 +369,17 @@ export function convertOperationsToPipeline(operations) {
|
|
|
293
369
|
const parentJoin = processedOperations.find(op => (op instanceof Join || op instanceof JoinThrough) && op.as === parentAlias);
|
|
294
370
|
if (parentJoin) {
|
|
295
371
|
pipeline.push({
|
|
296
|
-
$
|
|
297
|
-
[parentAlias]: {
|
|
372
|
+
$set: {
|
|
373
|
+
[`_joinData.${parentAlias}`]: {
|
|
298
374
|
$mergeObjects: [
|
|
299
|
-
|
|
300
|
-
{ [operation.as]:
|
|
375
|
+
{ $ifNull: [`$_joinData.${parentAlias}`, {}] },
|
|
376
|
+
{ [operation.as]: `$_joinData.${operation.as}` }
|
|
301
377
|
]
|
|
302
378
|
}
|
|
303
379
|
}
|
|
304
380
|
});
|
|
305
381
|
pipeline.push({
|
|
306
|
-
$
|
|
307
|
-
[operation.as]: 0
|
|
308
|
-
}
|
|
382
|
+
$unset: `_joinData.${operation.as}`
|
|
309
383
|
});
|
|
310
384
|
}
|
|
311
385
|
}
|
|
@@ -314,15 +388,20 @@ export function convertOperationsToPipeline(operations) {
|
|
|
314
388
|
else if (operation instanceof JoinThroughMany) {
|
|
315
389
|
const needsObjectIdConversion = operation.foreignField === '_id';
|
|
316
390
|
const isNestedField = operation.localField.includes('.');
|
|
391
|
+
const resolvedLocalField = resolveLocalFieldPath(operation.localField, processedOperations);
|
|
317
392
|
if (needsObjectIdConversion || isNestedField) {
|
|
318
393
|
pipeline.push({
|
|
319
394
|
$lookup: {
|
|
320
395
|
from: operation.through,
|
|
321
|
-
let: {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
`$${
|
|
325
|
-
|
|
396
|
+
let: {
|
|
397
|
+
localId: {
|
|
398
|
+
$cond: [
|
|
399
|
+
{ $eq: [{ $type: `$${resolvedLocalField}` }, 'string'] },
|
|
400
|
+
{ $toObjectId: `$${resolvedLocalField}` },
|
|
401
|
+
`$${resolvedLocalField}`
|
|
402
|
+
]
|
|
403
|
+
}
|
|
404
|
+
},
|
|
326
405
|
pipeline: [
|
|
327
406
|
{
|
|
328
407
|
$match: {
|
|
@@ -351,11 +430,15 @@ export function convertOperationsToPipeline(operations) {
|
|
|
351
430
|
}, {
|
|
352
431
|
$lookup: {
|
|
353
432
|
from: operation.from,
|
|
354
|
-
let: {
|
|
433
|
+
let: {
|
|
434
|
+
foreignId: {
|
|
435
|
+
$cond: [
|
|
355
436
|
{ $eq: [{ $type: `$${operation.as}_through.${operation.throughForeignField}` }, 'string'] },
|
|
356
437
|
{ $toObjectId: `$${operation.as}_through.${operation.throughForeignField}` },
|
|
357
438
|
`$${operation.as}_through.${operation.throughForeignField}`
|
|
358
|
-
]
|
|
439
|
+
]
|
|
440
|
+
}
|
|
441
|
+
},
|
|
359
442
|
pipeline: [
|
|
360
443
|
{
|
|
361
444
|
$match: {
|
|
@@ -380,18 +463,19 @@ export function convertOperationsToPipeline(operations) {
|
|
|
380
463
|
$group: {
|
|
381
464
|
_id: '$_id',
|
|
382
465
|
root: { $first: '$$ROOT' },
|
|
383
|
-
[operation.as]: { $push: { $arrayElemAt: [`$${operation.as}_temp`, 0] } }
|
|
466
|
+
[`${operation.as}_temp_grouped`]: { $push: { $arrayElemAt: [`$${operation.as}_temp`, 0] } }
|
|
384
467
|
}
|
|
385
468
|
}, {
|
|
386
469
|
$replaceRoot: {
|
|
387
470
|
newRoot: {
|
|
388
|
-
$mergeObjects: ['$root', { [operation.as]: `$${operation.as}` }]
|
|
471
|
+
$mergeObjects: ['$root', { [`_joinData.${operation.as}`]: `$${operation.as}_temp_grouped` }]
|
|
389
472
|
}
|
|
390
473
|
}
|
|
391
474
|
}, {
|
|
392
475
|
$project: {
|
|
393
476
|
[`${operation.as}_through`]: 0,
|
|
394
|
-
[`${operation.as}_temp`]: 0
|
|
477
|
+
[`${operation.as}_temp`]: 0,
|
|
478
|
+
[`${operation.as}_temp_grouped`]: 0
|
|
395
479
|
}
|
|
396
480
|
});
|
|
397
481
|
if (isNestedField) {
|
|
@@ -400,27 +484,28 @@ export function convertOperationsToPipeline(operations) {
|
|
|
400
484
|
if (parentJoin) {
|
|
401
485
|
pipeline.push({
|
|
402
486
|
$set: {
|
|
403
|
-
[parentAlias]: {
|
|
487
|
+
[`_joinData.${parentAlias}`]: {
|
|
404
488
|
$mergeObjects: [
|
|
405
|
-
{ $ifNull: [
|
|
406
|
-
{ [operation.as]:
|
|
489
|
+
{ $ifNull: [`$_joinData.${parentAlias}`, {}] },
|
|
490
|
+
{ [operation.as]: `$_joinData.${operation.as}` }
|
|
407
491
|
]
|
|
408
492
|
}
|
|
409
493
|
}
|
|
410
494
|
});
|
|
411
495
|
pipeline.push({
|
|
412
|
-
$unset: operation.as
|
|
496
|
+
$unset: `_joinData.${operation.as}`
|
|
413
497
|
});
|
|
414
498
|
}
|
|
415
499
|
}
|
|
416
500
|
}
|
|
417
501
|
else {
|
|
418
502
|
const isNestedFieldElse = operation.localField.includes('.');
|
|
503
|
+
const resolvedLocalFieldElse = resolveLocalFieldPath(operation.localField, processedOperations);
|
|
419
504
|
if (isNestedFieldElse) {
|
|
420
505
|
pipeline.push({
|
|
421
506
|
$lookup: {
|
|
422
507
|
from: operation.through,
|
|
423
|
-
let: { localId: `$${
|
|
508
|
+
let: { localId: `$${resolvedLocalFieldElse}` },
|
|
424
509
|
pipeline: [
|
|
425
510
|
{
|
|
426
511
|
$match: {
|
|
@@ -443,7 +528,7 @@ export function convertOperationsToPipeline(operations) {
|
|
|
443
528
|
pipeline.push({
|
|
444
529
|
$lookup: {
|
|
445
530
|
from: operation.through,
|
|
446
|
-
localField:
|
|
531
|
+
localField: resolvedLocalFieldElse,
|
|
447
532
|
foreignField: operation.throughLocalField,
|
|
448
533
|
as: `${operation.as}_through`
|
|
449
534
|
}
|
|
@@ -465,18 +550,19 @@ export function convertOperationsToPipeline(operations) {
|
|
|
465
550
|
$group: {
|
|
466
551
|
_id: '$_id',
|
|
467
552
|
root: { $first: '$$ROOT' },
|
|
468
|
-
[operation.as]: { $push: { $arrayElemAt: [`$${operation.as}_temp`, 0] } }
|
|
553
|
+
[`${operation.as}_temp_grouped`]: { $push: { $arrayElemAt: [`$${operation.as}_temp`, 0] } }
|
|
469
554
|
}
|
|
470
555
|
}, {
|
|
471
556
|
$replaceRoot: {
|
|
472
557
|
newRoot: {
|
|
473
|
-
$mergeObjects: ['$root', { [operation.as]: `$${operation.as}` }]
|
|
558
|
+
$mergeObjects: ['$root', { [`_joinData.${operation.as}`]: `$${operation.as}_temp_grouped` }]
|
|
474
559
|
}
|
|
475
560
|
}
|
|
476
561
|
}, {
|
|
477
562
|
$project: {
|
|
478
563
|
[`${operation.as}_through`]: 0,
|
|
479
|
-
[`${operation.as}_temp`]: 0
|
|
564
|
+
[`${operation.as}_temp`]: 0,
|
|
565
|
+
[`${operation.as}_temp_grouped`]: 0
|
|
480
566
|
}
|
|
481
567
|
});
|
|
482
568
|
if (isNestedFieldElse) {
|
|
@@ -484,19 +570,17 @@ export function convertOperationsToPipeline(operations) {
|
|
|
484
570
|
const parentJoin = processedOperations.find(op => (op instanceof Join || op instanceof JoinThrough) && op.as === parentAlias);
|
|
485
571
|
if (parentJoin) {
|
|
486
572
|
pipeline.push({
|
|
487
|
-
$
|
|
488
|
-
[parentAlias]: {
|
|
573
|
+
$set: {
|
|
574
|
+
[`_joinData.${parentAlias}`]: {
|
|
489
575
|
$mergeObjects: [
|
|
490
|
-
|
|
491
|
-
{ [operation.as]:
|
|
576
|
+
{ $ifNull: [`$_joinData.${parentAlias}`, {}] },
|
|
577
|
+
{ [operation.as]: `$_joinData.${operation.as}` }
|
|
492
578
|
]
|
|
493
579
|
}
|
|
494
580
|
}
|
|
495
581
|
});
|
|
496
582
|
pipeline.push({
|
|
497
|
-
$
|
|
498
|
-
[operation.as]: 0
|
|
499
|
-
}
|
|
583
|
+
$unset: `_joinData.${operation.as}`
|
|
500
584
|
});
|
|
501
585
|
}
|
|
502
586
|
}
|
|
@@ -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.91",
|
|
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",
|