@rws-framework/db 3.12.4 → 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.
@@ -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 (allowRelations) {
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
- // Process regular fields and time series
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
- return await FindUtils_1.FindUtils.findOneBy(this.constructor, { conditions: where });
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
- const dbData = await opModel.services.dbService.findOneBy(collection, conditions, fields, ordering);
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
- const dbData = await opModel.services.dbService.findOneBy(collection, { id }, fields, ordering);
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
- const dbData = await opModel.services.dbService.findBy(collection, conditions, fields, ordering, paginateParams);
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
- const dbData = await opModel.services.dbService.findBy(collection, conditions, fields, ordering, paginateParams);
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": "3.12.4",
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
- return await FindUtils.findOneBy(this.constructor as OpModelType<any>, { conditions: where });
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
- const dbData = await opModel.services.dbService.findOneBy(collection, conditions, fields, ordering);
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
- const dbData = await opModel.services.dbService.findOneBy(collection, { id }, fields, ordering);
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
- const dbData = await opModel.services.dbService.findBy(collection, conditions, fields, ordering, paginateParams);
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
- const dbData = await opModel.services.dbService.findBy(collection, conditions, fields, ordering, paginateParams);
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): Promise<IModel[]>
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
 
package/tsconfig.json CHANGED
@@ -14,7 +14,8 @@
14
14
  "allowSyntheticDefaultImports": true,
15
15
  "sourceMap": false,
16
16
  "declaration": true,
17
- "outDir": "./dist"
17
+ "outDir": "./dist",
18
+ "types": ["node"]
18
19
  },
19
20
  "include": [
20
21
  "src"