@rws-framework/db 3.12.5 → 4.0.1
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/models/core/RWSModel.d.ts +12 -0
- package/dist/models/core/RWSModel.js +161 -3
- package/dist/models/interfaces/OpModelType.d.ts +1 -0
- package/dist/models/utils/FindUtils.js +20 -4
- package/dist/models/utils/HydrateUtils.js +13 -0
- package/dist/services/DBService.d.ts +2 -2
- package/dist/services/DBService.js +26 -2
- package/package.json +2 -1
- package/src/models/core/RWSModel.ts +183 -4
- package/src/models/interfaces/OpModelType.ts +4 -0
- package/src/models/utils/FindUtils.ts +30 -7
- package/src/models/utils/HydrateUtils.ts +16 -0
- package/src/services/DBService.ts +28 -3
- package/tsconfig.json +2 -1
|
@@ -72,6 +72,18 @@ declare class RWSModel<T> implements IModel {
|
|
|
72
72
|
static count(where?: {
|
|
73
73
|
[k: string]: any;
|
|
74
74
|
}): Promise<number>;
|
|
75
|
+
/**
|
|
76
|
+
* Build Prisma include object for relation preloading
|
|
77
|
+
*/
|
|
78
|
+
static buildPrismaIncludes<T extends RWSModel<T>>(this: OpModelType<T>, fields?: string[]): Promise<any>;
|
|
79
|
+
/**
|
|
80
|
+
* Check if relations are already populated from Prisma includes
|
|
81
|
+
*/
|
|
82
|
+
private checkRelationsPrePopulated;
|
|
83
|
+
/**
|
|
84
|
+
* Hydrate pre-populated relations from Prisma includes (one level only)
|
|
85
|
+
*/
|
|
86
|
+
private hydratePrePopulatedRelations;
|
|
75
87
|
static getDb(): DBService;
|
|
76
88
|
reload(): Promise<RWSModel<T> | null>;
|
|
77
89
|
}
|
|
@@ -107,10 +107,26 @@ class RWSModel {
|
|
|
107
107
|
collections_to_models[model.getCollection()] = model;
|
|
108
108
|
});
|
|
109
109
|
const seriesHydrationfields = [];
|
|
110
|
-
if
|
|
110
|
+
// Check if relations are already populated from Prisma includes
|
|
111
|
+
const relationsAlreadyPopulated = this.checkRelationsPrePopulated(data, relOneData, relManyData);
|
|
112
|
+
if (allowRelations && !relationsAlreadyPopulated) {
|
|
113
|
+
// Use traditional relation hydration if not pre-populated
|
|
111
114
|
await HydrateUtils_1.HydrateUtils.hydrateRelations(this, relManyData, relOneData, seriesHydrationfields, fullDataMode, data);
|
|
112
115
|
}
|
|
113
|
-
|
|
116
|
+
else if (allowRelations && relationsAlreadyPopulated) {
|
|
117
|
+
// Relations are already populated from Prisma, just assign them directly
|
|
118
|
+
await this.hydratePrePopulatedRelations(data, relOneData, relManyData);
|
|
119
|
+
// Create a copy of data without relation fields to prevent overwriting hydrated relations
|
|
120
|
+
const dataWithoutRelations = { ...data };
|
|
121
|
+
for (const key in relOneData) {
|
|
122
|
+
delete dataWithoutRelations[key];
|
|
123
|
+
}
|
|
124
|
+
for (const key in relManyData) {
|
|
125
|
+
delete dataWithoutRelations[key];
|
|
126
|
+
}
|
|
127
|
+
data = dataWithoutRelations;
|
|
128
|
+
}
|
|
129
|
+
// Process regular fields and time series (excluding relations when pre-populated)
|
|
114
130
|
await HydrateUtils_1.HydrateUtils.hydrateDataFields(this, collections_to_models, relOneData, seriesHydrationfields, fullDataMode, data);
|
|
115
131
|
if (!this.isPostLoadExecuted() && postLoadExecute) {
|
|
116
132
|
await this.postLoad();
|
|
@@ -311,6 +327,139 @@ class RWSModel {
|
|
|
311
327
|
static async count(where = {}) {
|
|
312
328
|
return await this.services.dbService.count(this, where);
|
|
313
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* Build Prisma include object for relation preloading
|
|
332
|
+
*/
|
|
333
|
+
static async buildPrismaIncludes(fields) {
|
|
334
|
+
const tempInstance = new this();
|
|
335
|
+
const classFields = FieldsHelper_1.FieldsHelper.getAllClassFields(this);
|
|
336
|
+
const [relOneData, relManyData] = await Promise.all([
|
|
337
|
+
this.getRelationOneMeta(tempInstance, classFields),
|
|
338
|
+
this.getRelationManyMeta(tempInstance, classFields)
|
|
339
|
+
]);
|
|
340
|
+
// Get relations configuration from @RWSCollection decorator
|
|
341
|
+
const allowedRelations = this._RELATIONS || {};
|
|
342
|
+
const hasRelationsConfig = Object.keys(allowedRelations).length > 0;
|
|
343
|
+
const includes = {};
|
|
344
|
+
// Helper function to determine if a relation should be included
|
|
345
|
+
const shouldIncludeRelation = (relationName) => {
|
|
346
|
+
// If relations config exists, only include relations that are explicitly enabled
|
|
347
|
+
if (hasRelationsConfig) {
|
|
348
|
+
if (!allowedRelations[relationName]) {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// If fields are specified, only include if relation is in fields array
|
|
353
|
+
if (fields && fields.length > 0) {
|
|
354
|
+
return fields.includes(relationName);
|
|
355
|
+
}
|
|
356
|
+
// If no fields specified but relations config exists, include enabled relations
|
|
357
|
+
if (hasRelationsConfig) {
|
|
358
|
+
return allowedRelations[relationName] === true;
|
|
359
|
+
}
|
|
360
|
+
// If no relations config and no fields specified, include all relations
|
|
361
|
+
return true;
|
|
362
|
+
};
|
|
363
|
+
// Add one-to-one and many-to-one relations
|
|
364
|
+
for (const key in relOneData) {
|
|
365
|
+
if (shouldIncludeRelation(key)) {
|
|
366
|
+
includes[key] = true;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// Add one-to-many relations
|
|
370
|
+
for (const key in relManyData) {
|
|
371
|
+
if (shouldIncludeRelation(key)) {
|
|
372
|
+
includes[key] = true;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return Object.keys(includes).length > 0 ? includes : null;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Check if relations are already populated from Prisma includes
|
|
379
|
+
*/
|
|
380
|
+
checkRelationsPrePopulated(data, relOneData, relManyData) {
|
|
381
|
+
// Check if any relation key in data contains object data instead of just ID
|
|
382
|
+
for (const key in relOneData) {
|
|
383
|
+
if (data[key] && typeof data[key] === 'object' && data[key] !== null) {
|
|
384
|
+
// For one-to-one relations, check if it has an id or if it's a full object
|
|
385
|
+
if (data[key].id || Object.keys(data[key]).length > 1) {
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
for (const key in relManyData) {
|
|
391
|
+
const relationValue = data[key];
|
|
392
|
+
const relMeta = relManyData[key];
|
|
393
|
+
if (relMeta.singular) {
|
|
394
|
+
// Singular inverse relation - should be a single object
|
|
395
|
+
if (relationValue && typeof relationValue === 'object' && relationValue !== null &&
|
|
396
|
+
(relationValue.id || Object.keys(relationValue).length > 1)) {
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
else if (relationValue && Array.isArray(relationValue) && relationValue.length > 0) {
|
|
401
|
+
// Regular one-to-many relations - should be arrays
|
|
402
|
+
if (typeof relationValue[0] === 'object' && relationValue[0] !== null) {
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Hydrate pre-populated relations from Prisma includes (one level only)
|
|
411
|
+
*/
|
|
412
|
+
async hydratePrePopulatedRelations(data, relOneData, relManyData) {
|
|
413
|
+
// Handle one-to-one and many-to-one relations
|
|
414
|
+
for (const key in relOneData) {
|
|
415
|
+
if (data[key] && typeof data[key] === 'object' && data[key] !== null) {
|
|
416
|
+
const relationData = data[key];
|
|
417
|
+
const relMeta = relOneData[key];
|
|
418
|
+
const ModelClass = relMeta.model; // Use the model class directly from metadata
|
|
419
|
+
if (ModelClass) {
|
|
420
|
+
// Check if it's already a full object with data or just an ID reference
|
|
421
|
+
if (relationData.id || Object.keys(relationData).length > 1) {
|
|
422
|
+
// Create new instance and hydrate ONLY basic fields, NO RELATIONS
|
|
423
|
+
const relatedInstance = new ModelClass();
|
|
424
|
+
await relatedInstance._asyncFill(relationData, false, false, true);
|
|
425
|
+
this[key] = relatedInstance;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// Handle one-to-many relations
|
|
431
|
+
for (const key in relManyData) {
|
|
432
|
+
if (data[key]) {
|
|
433
|
+
const relationData = data[key];
|
|
434
|
+
const relMeta = relManyData[key];
|
|
435
|
+
const ModelClass = relMeta.inversionModel; // Use the model class directly from metadata
|
|
436
|
+
if (ModelClass) {
|
|
437
|
+
// Check if this is a singular inverse relation
|
|
438
|
+
if (relMeta.singular && !Array.isArray(relationData)) {
|
|
439
|
+
// Handle singular inverse relation as a single object
|
|
440
|
+
if (typeof relationData === 'object' && relationData !== null &&
|
|
441
|
+
(relationData.id || Object.keys(relationData).length > 1)) {
|
|
442
|
+
const relatedInstance = new ModelClass();
|
|
443
|
+
await relatedInstance._asyncFill(relationData, false, false, true);
|
|
444
|
+
this[key] = relatedInstance;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
else if (Array.isArray(relationData) && relationData.length > 0) {
|
|
448
|
+
// Handle regular one-to-many relations as arrays
|
|
449
|
+
const relatedInstances = [];
|
|
450
|
+
for (const itemData of relationData) {
|
|
451
|
+
if (typeof itemData === 'object' && itemData !== null) {
|
|
452
|
+
const relatedInstance = new ModelClass();
|
|
453
|
+
await relatedInstance._asyncFill(itemData, false, false, true);
|
|
454
|
+
relatedInstances.push(relatedInstance);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
this[key] = relatedInstances;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
314
463
|
static getDb() {
|
|
315
464
|
return this.services.dbService;
|
|
316
465
|
}
|
|
@@ -325,7 +474,16 @@ class RWSModel {
|
|
|
325
474
|
else {
|
|
326
475
|
where[pk] = this[pk];
|
|
327
476
|
}
|
|
328
|
-
|
|
477
|
+
// Find the fresh data from database
|
|
478
|
+
const freshData = await FindUtils_1.FindUtils.findOneBy(this.constructor, { conditions: where });
|
|
479
|
+
if (!freshData) {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
// Convert the fresh instance back to plain data for hydration
|
|
483
|
+
const plainData = await freshData.toMongo();
|
|
484
|
+
// Hydrate current instance with fresh data including relations
|
|
485
|
+
await this._asyncFill(plainData, true, true, true);
|
|
486
|
+
return this;
|
|
329
487
|
}
|
|
330
488
|
}
|
|
331
489
|
exports.RWSModel = RWSModel;
|
|
@@ -35,6 +35,7 @@ export interface OpModelType<T> {
|
|
|
35
35
|
count(where?: {
|
|
36
36
|
[k: string]: any;
|
|
37
37
|
}): Promise<number>;
|
|
38
|
+
buildPrismaIncludes<T extends RWSModel<T>>(this: OpModelType<T>, fields?: string[]): Promise<any>;
|
|
38
39
|
isSubclass<T extends RWSModel<T>, C extends new () => T>(constructor: C, baseClass: new () => T): boolean;
|
|
39
40
|
getModelAnnotations<T extends unknown>(constructor: new () => T): Promise<Record<string, {
|
|
40
41
|
annotationType: string;
|
|
@@ -10,7 +10,11 @@ class FindUtils {
|
|
|
10
10
|
const fullData = findParams?.fullData ?? false;
|
|
11
11
|
opModel.checkForInclusionWithThrow('');
|
|
12
12
|
const collection = Reflect.get(opModel, '_collection');
|
|
13
|
-
|
|
13
|
+
// Build Prisma includes for relation preloading if relations are allowed
|
|
14
|
+
const prismaOptions = allowRelations ? {
|
|
15
|
+
include: await opModel.buildPrismaIncludes(fields)
|
|
16
|
+
} : null;
|
|
17
|
+
const dbData = await opModel.services.dbService.findOneBy(collection, conditions, fields, ordering, prismaOptions);
|
|
14
18
|
if (dbData) {
|
|
15
19
|
const inst = new opModel();
|
|
16
20
|
const loaded = await inst._asyncFill(dbData, fullData, allowRelations, findParams?.cancelPostLoad ? false : true);
|
|
@@ -25,7 +29,11 @@ class FindUtils {
|
|
|
25
29
|
const fullData = findParams?.fullData ?? false;
|
|
26
30
|
const collection = Reflect.get(opModel, '_collection');
|
|
27
31
|
opModel.checkForInclusionWithThrow(opModel.name);
|
|
28
|
-
|
|
32
|
+
// Build Prisma includes for relation preloading if relations are allowed
|
|
33
|
+
const prismaOptions = allowRelations ? {
|
|
34
|
+
include: await opModel.buildPrismaIncludes(fields)
|
|
35
|
+
} : null;
|
|
36
|
+
const dbData = await opModel.services.dbService.findOneBy(collection, { id }, fields, ordering, prismaOptions);
|
|
29
37
|
if (dbData) {
|
|
30
38
|
const inst = new opModel();
|
|
31
39
|
const loaded = await inst._asyncFill(dbData, fullData, allowRelations, findParams?.cancelPostLoad ? false : true);
|
|
@@ -43,7 +51,11 @@ class FindUtils {
|
|
|
43
51
|
opModel.checkForInclusionWithThrow(opModel.name);
|
|
44
52
|
try {
|
|
45
53
|
const paginateParams = findParams?.pagination ? findParams?.pagination : undefined;
|
|
46
|
-
|
|
54
|
+
// Build Prisma includes for relation preloading if relations are allowed
|
|
55
|
+
const prismaOptions = allowRelations ? {
|
|
56
|
+
include: await opModel.buildPrismaIncludes(fields)
|
|
57
|
+
} : null;
|
|
58
|
+
const dbData = await opModel.services.dbService.findBy(collection, conditions, fields, ordering, paginateParams, prismaOptions);
|
|
47
59
|
if (dbData.length) {
|
|
48
60
|
const instanced = [];
|
|
49
61
|
for (const data of dbData) {
|
|
@@ -68,7 +80,11 @@ class FindUtils {
|
|
|
68
80
|
const collection = Reflect.get(opModel, '_collection');
|
|
69
81
|
opModel.checkForInclusionWithThrow(opModel.name);
|
|
70
82
|
try {
|
|
71
|
-
|
|
83
|
+
// Build Prisma includes for relation preloading if relations are allowed
|
|
84
|
+
const prismaOptions = allowRelations ? {
|
|
85
|
+
include: await opModel.buildPrismaIncludes(fields)
|
|
86
|
+
} : null;
|
|
87
|
+
const dbData = await opModel.services.dbService.findBy(collection, conditions, fields, ordering, paginateParams, prismaOptions);
|
|
72
88
|
if (dbData.length) {
|
|
73
89
|
const instanced = [];
|
|
74
90
|
for (const data of dbData) {
|
|
@@ -11,14 +11,27 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
11
11
|
class HydrateUtils {
|
|
12
12
|
static async hydrateDataFields(model, collections_to_models, relOneData, seriesHydrationfields, fullDataMode, data) {
|
|
13
13
|
const timeSeriesIds = TimeSeriesUtils_1.TimeSeriesUtils.getTimeSeriesModelFields(model);
|
|
14
|
+
// Build a set of foreign key field names to skip
|
|
15
|
+
const foreignKeyFields = new Set();
|
|
16
|
+
for (const relationName in relOneData) {
|
|
17
|
+
const relationMeta = relOneData[relationName];
|
|
18
|
+
if (relationMeta.hydrationField) {
|
|
19
|
+
foreignKeyFields.add(relationMeta.hydrationField);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
14
22
|
for (const key in data) {
|
|
15
23
|
if (data.hasOwnProperty(key)) {
|
|
16
24
|
if (!fullDataMode && (model).constructor._CUT_KEYS.includes(key)) {
|
|
17
25
|
continue;
|
|
18
26
|
}
|
|
27
|
+
// Skip relation property names
|
|
19
28
|
if (Object.keys(relOneData).includes(key)) {
|
|
20
29
|
continue;
|
|
21
30
|
}
|
|
31
|
+
// Skip foreign key field names
|
|
32
|
+
if (foreignKeyFields.has(key)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
22
35
|
if (seriesHydrationfields.includes(key)) {
|
|
23
36
|
continue;
|
|
24
37
|
}
|
|
@@ -24,9 +24,9 @@ declare class DBService {
|
|
|
24
24
|
watchCollection(collectionName: string, preRun: () => void): Promise<any>;
|
|
25
25
|
insert(data: any, collection: string, isTimeSeries?: boolean): Promise<any>;
|
|
26
26
|
update(data: any, collection: string, pk: string | string[]): Promise<IModel>;
|
|
27
|
-
findOneBy(collection: string, conditions: any, fields?: string[] | null, ordering?: OrderByType): Promise<IModel | null>;
|
|
27
|
+
findOneBy(collection: string, conditions: any, fields?: string[] | null, ordering?: OrderByType, prismaOptions?: any): Promise<IModel | null>;
|
|
28
28
|
delete(collection: string, conditions: any): Promise<void>;
|
|
29
|
-
findBy(collection: string, conditions: any, fields?: string[] | null, ordering?: OrderByType, pagination?: IPaginationParams): Promise<IModel[]>;
|
|
29
|
+
findBy(collection: string, conditions: any, fields?: string[] | null, ordering?: OrderByType, pagination?: IPaginationParams, prismaOptions?: any): Promise<IModel[]>;
|
|
30
30
|
collectionExists(collection_name: string): Promise<boolean>;
|
|
31
31
|
createTimeSeriesCollection(collection_name: string): Promise<Collection<ITimeSeries>>;
|
|
32
32
|
private getCollectionHandler;
|
|
@@ -126,13 +126,25 @@ class DBService {
|
|
|
126
126
|
});
|
|
127
127
|
return await this.findOneBy(collection, where);
|
|
128
128
|
}
|
|
129
|
-
async findOneBy(collection, conditions, fields = null, ordering = null) {
|
|
129
|
+
async findOneBy(collection, conditions, fields = null, ordering = null, prismaOptions = null) {
|
|
130
130
|
const params = { where: conditions };
|
|
131
131
|
if (fields) {
|
|
132
132
|
params.select = {};
|
|
133
133
|
fields.forEach((fieldName) => {
|
|
134
134
|
params.select[fieldName] = true;
|
|
135
135
|
});
|
|
136
|
+
// Add relation fields to select instead of using include when fields are specified
|
|
137
|
+
if (prismaOptions?.include) {
|
|
138
|
+
Object.keys(prismaOptions.include).forEach(relationField => {
|
|
139
|
+
if (fields.includes(relationField)) {
|
|
140
|
+
params.select[relationField] = true;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else if (prismaOptions?.include) {
|
|
146
|
+
// Only use include when no fields are specified
|
|
147
|
+
params.include = prismaOptions.include;
|
|
136
148
|
}
|
|
137
149
|
if (ordering) {
|
|
138
150
|
params.orderBy = this.convertOrderingToPrismaFormat(ordering);
|
|
@@ -144,13 +156,25 @@ class DBService {
|
|
|
144
156
|
await this.getCollectionHandler(collection).deleteMany({ where: conditions });
|
|
145
157
|
return;
|
|
146
158
|
}
|
|
147
|
-
async findBy(collection, conditions, fields = null, ordering = null, pagination = null) {
|
|
159
|
+
async findBy(collection, conditions, fields = null, ordering = null, pagination = null, prismaOptions = null) {
|
|
148
160
|
const params = { where: conditions };
|
|
149
161
|
if (fields) {
|
|
150
162
|
params.select = {};
|
|
151
163
|
fields.forEach((fieldName) => {
|
|
152
164
|
params.select[fieldName] = true;
|
|
153
165
|
});
|
|
166
|
+
// Add relation fields to select instead of using include when fields are specified
|
|
167
|
+
if (prismaOptions?.include) {
|
|
168
|
+
Object.keys(prismaOptions.include).forEach(relationField => {
|
|
169
|
+
if (fields.includes(relationField)) {
|
|
170
|
+
params.select[relationField] = true;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else if (prismaOptions?.include) {
|
|
176
|
+
// Only use include when no fields are specified
|
|
177
|
+
params.include = prismaOptions.include;
|
|
154
178
|
}
|
|
155
179
|
if (ordering) {
|
|
156
180
|
params.orderBy = this.convertOrderingToPrismaFormat(ordering);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rws-framework/db",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "4.0.1",
|
|
5
5
|
"description": "",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"tsconfig-paths-webpack-plugin": "^4.1.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
+
"@types/node": "^22.0.0",
|
|
27
28
|
"@types/xml2js": "^0.4.14",
|
|
28
29
|
"typescript": "^5.7.2"
|
|
29
30
|
},
|
|
@@ -129,12 +129,28 @@ class RWSModel<T> implements IModel {
|
|
|
129
129
|
|
|
130
130
|
const seriesHydrationfields: string[] = [];
|
|
131
131
|
|
|
132
|
+
// Check if relations are already populated from Prisma includes
|
|
133
|
+
const relationsAlreadyPopulated = this.checkRelationsPrePopulated(data, relOneData, relManyData);
|
|
132
134
|
|
|
133
|
-
if (allowRelations) {
|
|
135
|
+
if (allowRelations && !relationsAlreadyPopulated) {
|
|
136
|
+
// Use traditional relation hydration if not pre-populated
|
|
134
137
|
await HydrateUtils.hydrateRelations(this, relManyData, relOneData, seriesHydrationfields, fullDataMode, data);
|
|
138
|
+
} else if (allowRelations && relationsAlreadyPopulated) {
|
|
139
|
+
// Relations are already populated from Prisma, just assign them directly
|
|
140
|
+
await this.hydratePrePopulatedRelations(data, relOneData, relManyData);
|
|
141
|
+
|
|
142
|
+
// Create a copy of data without relation fields to prevent overwriting hydrated relations
|
|
143
|
+
const dataWithoutRelations = { ...data };
|
|
144
|
+
for (const key in relOneData) {
|
|
145
|
+
delete dataWithoutRelations[key];
|
|
146
|
+
}
|
|
147
|
+
for (const key in relManyData) {
|
|
148
|
+
delete dataWithoutRelations[key];
|
|
149
|
+
}
|
|
150
|
+
data = dataWithoutRelations;
|
|
135
151
|
}
|
|
136
152
|
|
|
137
|
-
// Process regular fields and time series
|
|
153
|
+
// Process regular fields and time series (excluding relations when pre-populated)
|
|
138
154
|
await HydrateUtils.hydrateDataFields(this, collections_to_models, relOneData, seriesHydrationfields, fullDataMode, data);
|
|
139
155
|
|
|
140
156
|
if(!this.isPostLoadExecuted() && postLoadExecute){
|
|
@@ -416,10 +432,160 @@ class RWSModel<T> implements IModel {
|
|
|
416
432
|
return this.services.dbService;
|
|
417
433
|
}
|
|
418
434
|
|
|
419
|
-
public static async count(where: {[k: string]: any} = {}): Promise<number>{
|
|
435
|
+
public static async count(where: {[k: string]: any} = {}): Promise<number>{
|
|
420
436
|
return await this.services.dbService.count(this as OpModelType<any>, where);
|
|
421
437
|
}
|
|
422
438
|
|
|
439
|
+
/**
|
|
440
|
+
* Build Prisma include object for relation preloading
|
|
441
|
+
*/
|
|
442
|
+
public static async buildPrismaIncludes<T extends RWSModel<T>>(this: OpModelType<T>, fields?: string[]): Promise<any> {
|
|
443
|
+
const tempInstance = new this();
|
|
444
|
+
const classFields = FieldsHelper.getAllClassFields(this);
|
|
445
|
+
|
|
446
|
+
const [relOneData, relManyData] = await Promise.all([
|
|
447
|
+
this.getRelationOneMeta(tempInstance, classFields),
|
|
448
|
+
this.getRelationManyMeta(tempInstance, classFields)
|
|
449
|
+
]);
|
|
450
|
+
|
|
451
|
+
// Get relations configuration from @RWSCollection decorator
|
|
452
|
+
const allowedRelations = this._RELATIONS || {};
|
|
453
|
+
const hasRelationsConfig = Object.keys(allowedRelations).length > 0;
|
|
454
|
+
|
|
455
|
+
const includes: any = {};
|
|
456
|
+
|
|
457
|
+
// Helper function to determine if a relation should be included
|
|
458
|
+
const shouldIncludeRelation = (relationName: string): boolean => {
|
|
459
|
+
// If relations config exists, only include relations that are explicitly enabled
|
|
460
|
+
if (hasRelationsConfig) {
|
|
461
|
+
if (!allowedRelations[relationName]) {
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// If fields are specified, only include if relation is in fields array
|
|
467
|
+
if (fields && fields.length > 0) {
|
|
468
|
+
return fields.includes(relationName);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// If no fields specified but relations config exists, include enabled relations
|
|
472
|
+
if (hasRelationsConfig) {
|
|
473
|
+
return allowedRelations[relationName] === true;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// If no relations config and no fields specified, include all relations
|
|
477
|
+
return true;
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
// Add one-to-one and many-to-one relations
|
|
481
|
+
for (const key in relOneData) {
|
|
482
|
+
if (shouldIncludeRelation(key)) {
|
|
483
|
+
includes[key] = true;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Add one-to-many relations
|
|
488
|
+
for (const key in relManyData) {
|
|
489
|
+
if (shouldIncludeRelation(key)) {
|
|
490
|
+
includes[key] = true;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return Object.keys(includes).length > 0 ? includes : null;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Check if relations are already populated from Prisma includes
|
|
499
|
+
*/
|
|
500
|
+
private checkRelationsPrePopulated(data: any, relOneData: any, relManyData: any): boolean {
|
|
501
|
+
// Check if any relation key in data contains object data instead of just ID
|
|
502
|
+
for (const key in relOneData) {
|
|
503
|
+
if (data[key] && typeof data[key] === 'object' && data[key] !== null) {
|
|
504
|
+
// For one-to-one relations, check if it has an id or if it's a full object
|
|
505
|
+
if (data[key].id || Object.keys(data[key]).length > 1) {
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
for (const key in relManyData) {
|
|
512
|
+
const relationValue = data[key];
|
|
513
|
+
const relMeta = relManyData[key];
|
|
514
|
+
|
|
515
|
+
if (relMeta.singular) {
|
|
516
|
+
// Singular inverse relation - should be a single object
|
|
517
|
+
if (relationValue && typeof relationValue === 'object' && relationValue !== null &&
|
|
518
|
+
(relationValue.id || Object.keys(relationValue).length > 1)) {
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
521
|
+
} else if (relationValue && Array.isArray(relationValue) && relationValue.length > 0) {
|
|
522
|
+
// Regular one-to-many relations - should be arrays
|
|
523
|
+
if (typeof relationValue[0] === 'object' && relationValue[0] !== null) {
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Hydrate pre-populated relations from Prisma includes (one level only)
|
|
534
|
+
*/
|
|
535
|
+
private async hydratePrePopulatedRelations(data: any, relOneData: any, relManyData: any): Promise<void> {
|
|
536
|
+
// Handle one-to-one and many-to-one relations
|
|
537
|
+
for (const key in relOneData) {
|
|
538
|
+
if (data[key] && typeof data[key] === 'object' && data[key] !== null) {
|
|
539
|
+
const relationData = data[key];
|
|
540
|
+
const relMeta = relOneData[key];
|
|
541
|
+
const ModelClass = relMeta.model; // Use the model class directly from metadata
|
|
542
|
+
|
|
543
|
+
if (ModelClass) {
|
|
544
|
+
// Check if it's already a full object with data or just an ID reference
|
|
545
|
+
if (relationData.id || Object.keys(relationData).length > 1) {
|
|
546
|
+
// Create new instance and hydrate ONLY basic fields, NO RELATIONS
|
|
547
|
+
const relatedInstance = new ModelClass();
|
|
548
|
+
await relatedInstance._asyncFill(relationData, false, false, true);
|
|
549
|
+
this[key] = relatedInstance;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Handle one-to-many relations
|
|
556
|
+
for (const key in relManyData) {
|
|
557
|
+
if (data[key]) {
|
|
558
|
+
const relationData = data[key];
|
|
559
|
+
const relMeta = relManyData[key];
|
|
560
|
+
const ModelClass = relMeta.inversionModel; // Use the model class directly from metadata
|
|
561
|
+
|
|
562
|
+
if (ModelClass) {
|
|
563
|
+
// Check if this is a singular inverse relation
|
|
564
|
+
if (relMeta.singular && !Array.isArray(relationData)) {
|
|
565
|
+
// Handle singular inverse relation as a single object
|
|
566
|
+
if (typeof relationData === 'object' && relationData !== null &&
|
|
567
|
+
(relationData.id || Object.keys(relationData).length > 1)) {
|
|
568
|
+
const relatedInstance = new ModelClass();
|
|
569
|
+
await relatedInstance._asyncFill(relationData, false, false, true);
|
|
570
|
+
this[key] = relatedInstance;
|
|
571
|
+
}
|
|
572
|
+
} else if (Array.isArray(relationData) && relationData.length > 0) {
|
|
573
|
+
// Handle regular one-to-many relations as arrays
|
|
574
|
+
const relatedInstances = [];
|
|
575
|
+
for (const itemData of relationData) {
|
|
576
|
+
if (typeof itemData === 'object' && itemData !== null) {
|
|
577
|
+
const relatedInstance = new ModelClass();
|
|
578
|
+
await relatedInstance._asyncFill(itemData, false, false, true);
|
|
579
|
+
relatedInstances.push(relatedInstance);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
this[key] = relatedInstances;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
423
589
|
public static getDb(): DBService
|
|
424
590
|
{
|
|
425
591
|
return this.services.dbService;
|
|
@@ -438,7 +604,20 @@ class RWSModel<T> implements IModel {
|
|
|
438
604
|
where[pk as string] = this[pk as string]
|
|
439
605
|
}
|
|
440
606
|
|
|
441
|
-
|
|
607
|
+
// Find the fresh data from database
|
|
608
|
+
const freshData = await FindUtils.findOneBy(this.constructor as OpModelType<any>, { conditions: where });
|
|
609
|
+
|
|
610
|
+
if (!freshData) {
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Convert the fresh instance back to plain data for hydration
|
|
615
|
+
const plainData = await freshData.toMongo();
|
|
616
|
+
|
|
617
|
+
// Hydrate current instance with fresh data including relations
|
|
618
|
+
await this._asyncFill(plainData, true, true, true);
|
|
619
|
+
|
|
620
|
+
return this;
|
|
442
621
|
}
|
|
443
622
|
}
|
|
444
623
|
|
|
@@ -63,6 +63,10 @@ export interface OpModelType<T> {
|
|
|
63
63
|
preRun: () => void
|
|
64
64
|
): Promise<any>;
|
|
65
65
|
count(where?: { [k: string]: any }): Promise<number>;
|
|
66
|
+
buildPrismaIncludes<T extends RWSModel<T>>(
|
|
67
|
+
this: OpModelType<T>,
|
|
68
|
+
fields?: string[]
|
|
69
|
+
): Promise<any>;
|
|
66
70
|
isSubclass<T extends RWSModel<T>, C extends new () => T>(
|
|
67
71
|
constructor: C,
|
|
68
72
|
baseClass: new () => T
|
|
@@ -20,10 +20,14 @@ export class FindUtils {
|
|
|
20
20
|
|
|
21
21
|
opModel.checkForInclusionWithThrow('');
|
|
22
22
|
|
|
23
|
-
|
|
24
23
|
const collection = Reflect.get(opModel, '_collection');
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
|
|
25
|
+
// Build Prisma includes for relation preloading if relations are allowed
|
|
26
|
+
const prismaOptions = allowRelations ? {
|
|
27
|
+
include: await opModel.buildPrismaIncludes(fields)
|
|
28
|
+
} : null;
|
|
29
|
+
|
|
30
|
+
const dbData = await opModel.services.dbService.findOneBy(collection, conditions, fields, ordering, prismaOptions);
|
|
27
31
|
|
|
28
32
|
if (dbData) {
|
|
29
33
|
const inst: T = new (opModel as { new(): T })();
|
|
@@ -47,7 +51,13 @@ export class FindUtils {
|
|
|
47
51
|
const collection = Reflect.get(opModel, '_collection');
|
|
48
52
|
opModel.checkForInclusionWithThrow(opModel.name);
|
|
49
53
|
|
|
50
|
-
|
|
54
|
+
// Build Prisma includes for relation preloading if relations are allowed
|
|
55
|
+
const prismaOptions = allowRelations ? {
|
|
56
|
+
include: await opModel.buildPrismaIncludes(fields)
|
|
57
|
+
} : null;
|
|
58
|
+
|
|
59
|
+
const dbData = await opModel.services.dbService.findOneBy(collection, { id }, fields, ordering, prismaOptions);
|
|
60
|
+
|
|
51
61
|
|
|
52
62
|
if (dbData) {
|
|
53
63
|
const inst: T = new (opModel as { new(): T })();
|
|
@@ -72,12 +82,18 @@ export class FindUtils {
|
|
|
72
82
|
opModel.checkForInclusionWithThrow(opModel.name);
|
|
73
83
|
try {
|
|
74
84
|
const paginateParams = findParams?.pagination ? findParams?.pagination : undefined;
|
|
75
|
-
|
|
85
|
+
|
|
86
|
+
// Build Prisma includes for relation preloading if relations are allowed
|
|
87
|
+
const prismaOptions = allowRelations ? {
|
|
88
|
+
include: await opModel.buildPrismaIncludes(fields)
|
|
89
|
+
} : null;
|
|
90
|
+
|
|
91
|
+
const dbData = await opModel.services.dbService.findBy(collection, conditions, fields, ordering, paginateParams, prismaOptions);
|
|
76
92
|
|
|
77
93
|
if (dbData.length) {
|
|
78
94
|
const instanced: T[] = [];
|
|
79
95
|
|
|
80
|
-
for (const data of dbData) {
|
|
96
|
+
for (const data of dbData) {
|
|
81
97
|
const inst: T = new (opModel as { new(): T })();
|
|
82
98
|
|
|
83
99
|
instanced.push((await inst._asyncFill(data, fullData, allowRelations, findParams?.cancelPostLoad ? false : true)) as T);
|
|
@@ -108,7 +124,13 @@ export class FindUtils {
|
|
|
108
124
|
const collection = Reflect.get(opModel, '_collection');
|
|
109
125
|
opModel.checkForInclusionWithThrow(opModel.name);
|
|
110
126
|
try {
|
|
111
|
-
|
|
127
|
+
// Build Prisma includes for relation preloading if relations are allowed
|
|
128
|
+
const prismaOptions = allowRelations ? {
|
|
129
|
+
include: await opModel.buildPrismaIncludes(fields)
|
|
130
|
+
} : null;
|
|
131
|
+
|
|
132
|
+
const dbData = await opModel.services.dbService.findBy(collection, conditions, fields, ordering, paginateParams, prismaOptions);
|
|
133
|
+
|
|
112
134
|
if (dbData.length) {
|
|
113
135
|
const instanced: T[] = [];
|
|
114
136
|
|
|
@@ -116,6 +138,7 @@ export class FindUtils {
|
|
|
116
138
|
const inst: T = new (opModel as { new(): T })();
|
|
117
139
|
instanced.push((await inst._asyncFill(data, fullData, allowRelations, findParams?.cancelPostLoad ? false : true)) as T);
|
|
118
140
|
}
|
|
141
|
+
|
|
119
142
|
|
|
120
143
|
return instanced;
|
|
121
144
|
}
|
|
@@ -10,16 +10,32 @@ import chalk from 'chalk';
|
|
|
10
10
|
export class HydrateUtils {
|
|
11
11
|
static async hydrateDataFields(model: RWSModel<any>, collections_to_models: { [key: string]: any }, relOneData: RelOneMetaType<IRWSModel>, seriesHydrationfields: string[], fullDataMode: boolean, data: { [key: string]: any }) {
|
|
12
12
|
const timeSeriesIds = TimeSeriesUtils.getTimeSeriesModelFields(model);
|
|
13
|
+
|
|
14
|
+
// Build a set of foreign key field names to skip
|
|
15
|
+
const foreignKeyFields = new Set<string>();
|
|
16
|
+
for (const relationName in relOneData) {
|
|
17
|
+
const relationMeta = relOneData[relationName];
|
|
18
|
+
if (relationMeta.hydrationField) {
|
|
19
|
+
foreignKeyFields.add(relationMeta.hydrationField);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
for (const key in data) {
|
|
14
24
|
if (data.hasOwnProperty(key)) {
|
|
15
25
|
if (!fullDataMode && ((model).constructor as OpModelType<any>)._CUT_KEYS.includes(key)) {
|
|
16
26
|
continue;
|
|
17
27
|
}
|
|
18
28
|
|
|
29
|
+
// Skip relation property names
|
|
19
30
|
if (Object.keys(relOneData).includes(key)) {
|
|
20
31
|
continue;
|
|
21
32
|
}
|
|
22
33
|
|
|
34
|
+
// Skip foreign key field names
|
|
35
|
+
if (foreignKeyFields.has(key)) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
23
39
|
if (seriesHydrationfields.includes(key)) {
|
|
24
40
|
continue;
|
|
25
41
|
}
|
|
@@ -172,7 +172,7 @@ class DBService {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
|
|
175
|
-
async findOneBy(collection: string, conditions: any, fields: string[] | null = null, ordering: OrderByType = null): Promise<IModel|null>
|
|
175
|
+
async findOneBy(collection: string, conditions: any, fields: string[] | null = null, ordering: OrderByType = null, prismaOptions: any = null): Promise<IModel|null>
|
|
176
176
|
{
|
|
177
177
|
const params: any = { where: conditions };
|
|
178
178
|
|
|
@@ -181,6 +181,18 @@ class DBService {
|
|
|
181
181
|
fields.forEach((fieldName: string) => {
|
|
182
182
|
params.select[fieldName] = true;
|
|
183
183
|
});
|
|
184
|
+
|
|
185
|
+
// Add relation fields to select instead of using include when fields are specified
|
|
186
|
+
if(prismaOptions?.include) {
|
|
187
|
+
Object.keys(prismaOptions.include).forEach(relationField => {
|
|
188
|
+
if (fields.includes(relationField)) {
|
|
189
|
+
params.select[relationField] = true;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
} else if(prismaOptions?.include) {
|
|
194
|
+
// Only use include when no fields are specified
|
|
195
|
+
params.include = prismaOptions.include;
|
|
184
196
|
}
|
|
185
197
|
|
|
186
198
|
if(ordering){
|
|
@@ -203,7 +215,8 @@ class DBService {
|
|
|
203
215
|
conditions: any,
|
|
204
216
|
fields: string[] | null = null,
|
|
205
217
|
ordering: OrderByType = null,
|
|
206
|
-
pagination: IPaginationParams = null
|
|
218
|
+
pagination: IPaginationParams = null,
|
|
219
|
+
prismaOptions: any = null): Promise<IModel[]>
|
|
207
220
|
{
|
|
208
221
|
const params: any ={ where: conditions };
|
|
209
222
|
|
|
@@ -212,6 +225,18 @@ class DBService {
|
|
|
212
225
|
fields.forEach((fieldName: string) => {
|
|
213
226
|
params.select[fieldName] = true;
|
|
214
227
|
});
|
|
228
|
+
|
|
229
|
+
// Add relation fields to select instead of using include when fields are specified
|
|
230
|
+
if(prismaOptions?.include) {
|
|
231
|
+
Object.keys(prismaOptions.include).forEach(relationField => {
|
|
232
|
+
if (fields.includes(relationField)) {
|
|
233
|
+
params.select[relationField] = true;
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
} else if(prismaOptions?.include) {
|
|
238
|
+
// Only use include when no fields are specified
|
|
239
|
+
params.include = prismaOptions.include;
|
|
215
240
|
}
|
|
216
241
|
|
|
217
242
|
if(ordering){
|
|
@@ -303,7 +328,7 @@ class DBService {
|
|
|
303
328
|
return this;
|
|
304
329
|
}
|
|
305
330
|
|
|
306
|
-
public async count<T = any>(opModel: OpModelType<T>, where: {[k: string]: any} = {}): Promise<number>{
|
|
331
|
+
public async count<T = any>(opModel: OpModelType<T>, where: {[k: string]: any} = {}): Promise<number>{
|
|
307
332
|
return await this.getCollectionHandler(opModel._collection).count({where});
|
|
308
333
|
}
|
|
309
334
|
|