@rws-framework/db 3.12.5 → 4.1.0

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.
@@ -15,6 +15,7 @@ declare class RWSModel<T> implements IModel {
15
15
  static _BANNED_KEYS: string[];
16
16
  static allModels: OpModelType<any>[];
17
17
  static _CUT_KEYS: string[];
18
+ private _relationFields;
18
19
  private postLoadExecuted;
19
20
  constructor(data?: any);
20
21
  isPostLoadExecuted(): boolean;
@@ -72,7 +73,20 @@ declare class RWSModel<T> implements IModel {
72
73
  static count(where?: {
73
74
  [k: string]: any;
74
75
  }): Promise<number>;
76
+ /**
77
+ * Build Prisma include object for relation preloading
78
+ */
79
+ static buildPrismaIncludes<T extends RWSModel<T>>(this: OpModelType<T>, fields?: string[]): Promise<any>;
80
+ /**
81
+ * Check if relations are already populated from Prisma includes
82
+ */
83
+ private checkRelationsPrePopulated;
84
+ /**
85
+ * Hydrate pre-populated relations from Prisma includes (one level only)
86
+ */
87
+ private hydratePrePopulatedRelations;
75
88
  static getDb(): DBService;
76
89
  reload(): Promise<RWSModel<T> | null>;
90
+ getPropertyValue(key: string): any;
77
91
  }
78
92
  export { RWSModel };
@@ -27,6 +27,8 @@ class RWSModel {
27
27
  static _BANNED_KEYS = ['_collection'];
28
28
  static allModels = [];
29
29
  static _CUT_KEYS = [];
30
+ // Store relation foreign key fields for reload() functionality
31
+ _relationFields = {};
30
32
  postLoadExecuted = false;
31
33
  constructor(data = null) {
32
34
  if (!this.getCollection()) {
@@ -107,10 +109,26 @@ class RWSModel {
107
109
  collections_to_models[model.getCollection()] = model;
108
110
  });
109
111
  const seriesHydrationfields = [];
110
- if (allowRelations) {
112
+ // Check if relations are already populated from Prisma includes
113
+ const relationsAlreadyPopulated = this.checkRelationsPrePopulated(data, relOneData, relManyData);
114
+ if (allowRelations && !relationsAlreadyPopulated) {
115
+ // Use traditional relation hydration if not pre-populated
111
116
  await HydrateUtils_1.HydrateUtils.hydrateRelations(this, relManyData, relOneData, seriesHydrationfields, fullDataMode, data);
112
117
  }
113
- // Process regular fields and time series
118
+ else if (allowRelations && relationsAlreadyPopulated) {
119
+ // Relations are already populated from Prisma, just assign them directly
120
+ await this.hydratePrePopulatedRelations(data, relOneData, relManyData);
121
+ // Create a copy of data without relation fields to prevent overwriting hydrated relations
122
+ const dataWithoutRelations = { ...data };
123
+ for (const key in relOneData) {
124
+ delete dataWithoutRelations[key];
125
+ }
126
+ for (const key in relManyData) {
127
+ delete dataWithoutRelations[key];
128
+ }
129
+ data = dataWithoutRelations;
130
+ }
131
+ // Process regular fields and time series (excluding relations when pre-populated)
114
132
  await HydrateUtils_1.HydrateUtils.hydrateDataFields(this, collections_to_models, relOneData, seriesHydrationfields, fullDataMode, data);
115
133
  if (!this.isPostLoadExecuted() && postLoadExecute) {
116
134
  await this.postLoad();
@@ -311,6 +329,187 @@ class RWSModel {
311
329
  static async count(where = {}) {
312
330
  return await this.services.dbService.count(this, where);
313
331
  }
332
+ /**
333
+ * Build Prisma include object for relation preloading
334
+ */
335
+ static async buildPrismaIncludes(fields) {
336
+ const tempInstance = new this();
337
+ const classFields = FieldsHelper_1.FieldsHelper.getAllClassFields(this);
338
+ const [relOneData, relManyData] = await Promise.all([
339
+ this.getRelationOneMeta(tempInstance, classFields),
340
+ this.getRelationManyMeta(tempInstance, classFields)
341
+ ]);
342
+ // Get relations configuration from @RWSCollection decorator
343
+ const allowedRelations = this._RELATIONS || {};
344
+ const hasRelationsConfig = Object.keys(allowedRelations).length > 0;
345
+ const includes = {};
346
+ // Helper function to determine if a relation should be included
347
+ const shouldIncludeRelation = (relationName) => {
348
+ // If relations config exists, only include relations that are explicitly enabled
349
+ if (hasRelationsConfig) {
350
+ if (!allowedRelations[relationName]) {
351
+ return false;
352
+ }
353
+ }
354
+ // If fields are specified, only include if relation is in fields array
355
+ if (fields && fields.length > 0) {
356
+ return fields.includes(relationName);
357
+ }
358
+ // If no fields specified but relations config exists, include enabled relations
359
+ if (hasRelationsConfig) {
360
+ return allowedRelations[relationName] === true;
361
+ }
362
+ // If no relations config and no fields specified, include all relations
363
+ return true;
364
+ };
365
+ // Add one-to-one and many-to-one relations
366
+ for (const key in relOneData) {
367
+ if (shouldIncludeRelation(key)) {
368
+ includes[key] = true;
369
+ }
370
+ }
371
+ // Add one-to-many relations
372
+ for (const key in relManyData) {
373
+ if (shouldIncludeRelation(key)) {
374
+ includes[key] = true;
375
+ }
376
+ }
377
+ return Object.keys(includes).length > 0 ? includes : null;
378
+ }
379
+ /**
380
+ * Check if relations are already populated from Prisma includes
381
+ */
382
+ checkRelationsPrePopulated(data, relOneData, relManyData) {
383
+ // Check if any relation key in data contains object data instead of just ID
384
+ for (const key in relOneData) {
385
+ if (data[key] && typeof data[key] === 'object' && data[key] !== null) {
386
+ // For one-to-one relations, check if it has an id or if it's a full object
387
+ if (data[key].id || Object.keys(data[key]).length > 1) {
388
+ return true;
389
+ }
390
+ }
391
+ }
392
+ for (const key in relManyData) {
393
+ const relationValue = data[key];
394
+ const relMeta = relManyData[key];
395
+ if (relMeta.singular) {
396
+ // Singular inverse relation - should be a single object
397
+ if (relationValue && typeof relationValue === 'object' && relationValue !== null &&
398
+ (relationValue.id || Object.keys(relationValue).length > 1)) {
399
+ return true;
400
+ }
401
+ }
402
+ else if (relationValue && Array.isArray(relationValue) && relationValue.length > 0) {
403
+ // Regular one-to-many relations - should be arrays
404
+ if (typeof relationValue[0] === 'object' && relationValue[0] !== null) {
405
+ return true;
406
+ }
407
+ }
408
+ }
409
+ return false;
410
+ }
411
+ /**
412
+ * Hydrate pre-populated relations from Prisma includes (one level only)
413
+ */
414
+ async hydratePrePopulatedRelations(data, relOneData, relManyData) {
415
+ // Handle one-to-one and many-to-one relations
416
+ for (const key in relOneData) {
417
+ if (data[key] && typeof data[key] === 'object' && data[key] !== null) {
418
+ const relationData = data[key];
419
+ const relMeta = relOneData[key];
420
+ const ModelClass = relMeta.model; // Use the model class directly from metadata
421
+ if (ModelClass) {
422
+ // Check if it's already a full object with data or just an ID reference
423
+ if (relationData.id || Object.keys(relationData).length > 1) {
424
+ // Create new instance and hydrate ONLY basic fields, NO RELATIONS
425
+ const relatedInstance = new ModelClass();
426
+ await relatedInstance._asyncFill(relationData, false, false, true);
427
+ this[key] = relatedInstance;
428
+ }
429
+ }
430
+ }
431
+ }
432
+ // Handle one-to-many relations
433
+ for (const key in relManyData) {
434
+ if (data[key]) {
435
+ const relationData = data[key];
436
+ const relMeta = relManyData[key];
437
+ const ModelClass = relMeta.inversionModel; // Use the model class directly from metadata
438
+ if (ModelClass) {
439
+ // Check if this is a singular inverse relation
440
+ if (relMeta.singular && !Array.isArray(relationData)) {
441
+ // Handle singular inverse relation as a single object
442
+ if (typeof relationData === 'object' && relationData !== null &&
443
+ (relationData.id || Object.keys(relationData).length > 1)) {
444
+ const relatedInstance = new ModelClass();
445
+ // Check relation metadata to identify foreign key fields that need to be preserved
446
+ const tempInstance = new ModelClass();
447
+ const childClassFields = FieldsHelper_1.FieldsHelper.getAllClassFields(tempInstance.constructor);
448
+ const [childRelOneData, childRelManyData] = await Promise.all([
449
+ RelationUtils_1.RelationUtils.getRelationOneMeta(tempInstance, childClassFields),
450
+ RelationUtils_1.RelationUtils.getRelationManyMeta(tempInstance, childClassFields)
451
+ ]);
452
+ // Store foreign key fields from relation metadata
453
+ for (const relKey in childRelOneData) {
454
+ const relMeta = childRelOneData[relKey];
455
+ const foreignKeyField = relMeta.hydrationField; // e.g., 'avatar_id', 'knowledge_id'
456
+ if (foreignKeyField && relationData[foreignKeyField] !== undefined) {
457
+ relatedInstance._relationFields[foreignKeyField] = relationData[foreignKeyField];
458
+ relatedInstance[foreignKeyField] = relationData[foreignKeyField];
459
+ }
460
+ }
461
+ for (const relKey in childRelManyData) {
462
+ const relMeta = childRelManyData[relKey];
463
+ const foreignKeyField = relMeta.foreignKey; // Use foreignKey for inverse relations
464
+ if (foreignKeyField && relationData[foreignKeyField] !== undefined) {
465
+ relatedInstance._relationFields[foreignKeyField] = relationData[foreignKeyField];
466
+ relatedInstance[foreignKeyField] = relationData[foreignKeyField];
467
+ }
468
+ }
469
+ await relatedInstance._asyncFill(relationData, false, false, true);
470
+ this[key] = relatedInstance;
471
+ }
472
+ }
473
+ else if (Array.isArray(relationData) && relationData.length > 0) {
474
+ // Handle regular one-to-many relations as arrays
475
+ const relatedInstances = [];
476
+ for (const itemData of relationData) {
477
+ if (typeof itemData === 'object' && itemData !== null) {
478
+ const relatedInstance = new ModelClass();
479
+ // Check relation metadata to identify foreign key fields that need to be preserved
480
+ const tempInstance = new ModelClass();
481
+ const childClassFields = FieldsHelper_1.FieldsHelper.getAllClassFields(tempInstance.constructor);
482
+ const [childRelOneData, childRelManyData] = await Promise.all([
483
+ RelationUtils_1.RelationUtils.getRelationOneMeta(tempInstance, childClassFields),
484
+ RelationUtils_1.RelationUtils.getRelationManyMeta(tempInstance, childClassFields)
485
+ ]);
486
+ // Store foreign key fields from relation metadata
487
+ for (const relKey in childRelOneData) {
488
+ const relMeta = childRelOneData[relKey];
489
+ const foreignKeyField = relMeta.hydrationField; // e.g., 'avatar_id', 'knowledge_id'
490
+ if (foreignKeyField && itemData[foreignKeyField] !== undefined) {
491
+ relatedInstance._relationFields[foreignKeyField] = itemData[foreignKeyField];
492
+ relatedInstance[foreignKeyField] = itemData[foreignKeyField];
493
+ }
494
+ }
495
+ for (const relKey in childRelManyData) {
496
+ const relMeta = childRelManyData[relKey];
497
+ const foreignKeyField = relMeta.foreignKey; // Use foreignKey for inverse relations
498
+ if (foreignKeyField && itemData[foreignKeyField] !== undefined) {
499
+ relatedInstance._relationFields[foreignKeyField] = itemData[foreignKeyField];
500
+ relatedInstance[foreignKeyField] = itemData[foreignKeyField];
501
+ }
502
+ }
503
+ await relatedInstance._asyncFill(itemData, false, false, true);
504
+ relatedInstances.push(relatedInstance);
505
+ }
506
+ }
507
+ this[key] = relatedInstances;
508
+ }
509
+ }
510
+ }
511
+ }
512
+ }
314
513
  static getDb() {
315
514
  return this.services.dbService;
316
515
  }
@@ -325,7 +524,29 @@ class RWSModel {
325
524
  else {
326
525
  where[pk] = this[pk];
327
526
  }
328
- return await FindUtils_1.FindUtils.findOneBy(this.constructor, { conditions: where });
527
+ // Find the fresh data from database
528
+ const freshData = await FindUtils_1.FindUtils.findOneBy(this.constructor, { conditions: where });
529
+ if (!freshData) {
530
+ return null;
531
+ }
532
+ // Convert the fresh instance back to plain data for hydration
533
+ const plainData = await freshData.toMongo();
534
+ // Preserve foreign key fields from _relationFields to ensure relations can be hydrated
535
+ for (const key in this._relationFields) {
536
+ if (plainData[key] === undefined) {
537
+ plainData[key] = this._relationFields[key];
538
+ }
539
+ }
540
+ // Hydrate current instance with fresh data including relations
541
+ await this._asyncFill(plainData, true, true, true);
542
+ return this;
543
+ }
544
+ // Helper method to get property with fallback to stored relation fields
545
+ getPropertyValue(key) {
546
+ if (this.hasOwnProperty(key) || this[key] !== undefined) {
547
+ return this[key];
548
+ }
549
+ return this._relationFields[key];
329
550
  }
330
551
  }
331
552
  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.5",
4
+ "version": "4.1.0",
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
  },
@@ -27,6 +27,9 @@ class RWSModel<T> implements IModel {
27
27
  static _BANNED_KEYS = ['_collection'];
28
28
  static allModels: OpModelType<any>[] = [];
29
29
  static _CUT_KEYS: string[] = [];
30
+
31
+ // Store relation foreign key fields for reload() functionality
32
+ private _relationFields: Record<string, any> = {};
30
33
 
31
34
  private postLoadExecuted: boolean = false;
32
35
 
@@ -129,12 +132,28 @@ class RWSModel<T> implements IModel {
129
132
 
130
133
  const seriesHydrationfields: string[] = [];
131
134
 
135
+ // Check if relations are already populated from Prisma includes
136
+ const relationsAlreadyPopulated = this.checkRelationsPrePopulated(data, relOneData, relManyData);
132
137
 
133
- if (allowRelations) {
138
+ if (allowRelations && !relationsAlreadyPopulated) {
139
+ // Use traditional relation hydration if not pre-populated
134
140
  await HydrateUtils.hydrateRelations(this, relManyData, relOneData, seriesHydrationfields, fullDataMode, data);
141
+ } else if (allowRelations && relationsAlreadyPopulated) {
142
+ // Relations are already populated from Prisma, just assign them directly
143
+ await this.hydratePrePopulatedRelations(data, relOneData, relManyData);
144
+
145
+ // Create a copy of data without relation fields to prevent overwriting hydrated relations
146
+ const dataWithoutRelations = { ...data };
147
+ for (const key in relOneData) {
148
+ delete dataWithoutRelations[key];
149
+ }
150
+ for (const key in relManyData) {
151
+ delete dataWithoutRelations[key];
152
+ }
153
+ data = dataWithoutRelations;
135
154
  }
136
155
 
137
- // Process regular fields and time series
156
+ // Process regular fields and time series (excluding relations when pre-populated)
138
157
  await HydrateUtils.hydrateDataFields(this, collections_to_models, relOneData, seriesHydrationfields, fullDataMode, data);
139
158
 
140
159
  if(!this.isPostLoadExecuted() && postLoadExecute){
@@ -416,10 +435,216 @@ class RWSModel<T> implements IModel {
416
435
  return this.services.dbService;
417
436
  }
418
437
 
419
- public static async count(where: {[k: string]: any} = {}): Promise<number>{
438
+ public static async count(where: {[k: string]: any} = {}): Promise<number>{
420
439
  return await this.services.dbService.count(this as OpModelType<any>, where);
421
440
  }
422
441
 
442
+ /**
443
+ * Build Prisma include object for relation preloading
444
+ */
445
+ public static async buildPrismaIncludes<T extends RWSModel<T>>(this: OpModelType<T>, fields?: string[]): Promise<any> {
446
+ const tempInstance = new this();
447
+ const classFields = FieldsHelper.getAllClassFields(this);
448
+
449
+ const [relOneData, relManyData] = await Promise.all([
450
+ this.getRelationOneMeta(tempInstance, classFields),
451
+ this.getRelationManyMeta(tempInstance, classFields)
452
+ ]);
453
+
454
+ // Get relations configuration from @RWSCollection decorator
455
+ const allowedRelations = this._RELATIONS || {};
456
+ const hasRelationsConfig = Object.keys(allowedRelations).length > 0;
457
+
458
+ const includes: any = {};
459
+
460
+ // Helper function to determine if a relation should be included
461
+ const shouldIncludeRelation = (relationName: string): boolean => {
462
+ // If relations config exists, only include relations that are explicitly enabled
463
+ if (hasRelationsConfig) {
464
+ if (!allowedRelations[relationName]) {
465
+ return false;
466
+ }
467
+ }
468
+
469
+ // If fields are specified, only include if relation is in fields array
470
+ if (fields && fields.length > 0) {
471
+ return fields.includes(relationName);
472
+ }
473
+
474
+ // If no fields specified but relations config exists, include enabled relations
475
+ if (hasRelationsConfig) {
476
+ return allowedRelations[relationName] === true;
477
+ }
478
+
479
+ // If no relations config and no fields specified, include all relations
480
+ return true;
481
+ };
482
+
483
+ // Add one-to-one and many-to-one relations
484
+ for (const key in relOneData) {
485
+ if (shouldIncludeRelation(key)) {
486
+ includes[key] = true;
487
+ }
488
+ }
489
+
490
+ // Add one-to-many relations
491
+ for (const key in relManyData) {
492
+ if (shouldIncludeRelation(key)) {
493
+ includes[key] = true;
494
+ }
495
+ }
496
+
497
+ return Object.keys(includes).length > 0 ? includes : null;
498
+ }
499
+
500
+ /**
501
+ * Check if relations are already populated from Prisma includes
502
+ */
503
+ private checkRelationsPrePopulated(data: any, relOneData: any, relManyData: any): boolean {
504
+ // Check if any relation key in data contains object data instead of just ID
505
+ for (const key in relOneData) {
506
+ if (data[key] && typeof data[key] === 'object' && data[key] !== null) {
507
+ // For one-to-one relations, check if it has an id or if it's a full object
508
+ if (data[key].id || Object.keys(data[key]).length > 1) {
509
+ return true;
510
+ }
511
+ }
512
+ }
513
+
514
+ for (const key in relManyData) {
515
+ const relationValue = data[key];
516
+ const relMeta = relManyData[key];
517
+
518
+ if (relMeta.singular) {
519
+ // Singular inverse relation - should be a single object
520
+ if (relationValue && typeof relationValue === 'object' && relationValue !== null &&
521
+ (relationValue.id || Object.keys(relationValue).length > 1)) {
522
+ return true;
523
+ }
524
+ } else if (relationValue && Array.isArray(relationValue) && relationValue.length > 0) {
525
+ // Regular one-to-many relations - should be arrays
526
+ if (typeof relationValue[0] === 'object' && relationValue[0] !== null) {
527
+ return true;
528
+ }
529
+ }
530
+ }
531
+
532
+ return false;
533
+ }
534
+
535
+ /**
536
+ * Hydrate pre-populated relations from Prisma includes (one level only)
537
+ */
538
+ private async hydratePrePopulatedRelations(data: any, relOneData: any, relManyData: any): Promise<void> {
539
+ // Handle one-to-one and many-to-one relations
540
+ for (const key in relOneData) {
541
+ if (data[key] && typeof data[key] === 'object' && data[key] !== null) {
542
+ const relationData = data[key];
543
+ const relMeta = relOneData[key];
544
+ const ModelClass = relMeta.model; // Use the model class directly from metadata
545
+
546
+ if (ModelClass) {
547
+ // Check if it's already a full object with data or just an ID reference
548
+ if (relationData.id || Object.keys(relationData).length > 1) {
549
+ // Create new instance and hydrate ONLY basic fields, NO RELATIONS
550
+ const relatedInstance = new ModelClass();
551
+ await relatedInstance._asyncFill(relationData, false, false, true);
552
+ this[key] = relatedInstance;
553
+ }
554
+ }
555
+ }
556
+ }
557
+
558
+ // Handle one-to-many relations
559
+ for (const key in relManyData) {
560
+ if (data[key]) {
561
+ const relationData = data[key];
562
+ const relMeta = relManyData[key];
563
+ const ModelClass = relMeta.inversionModel; // Use the model class directly from metadata
564
+
565
+ if (ModelClass) {
566
+ // Check if this is a singular inverse relation
567
+ if (relMeta.singular && !Array.isArray(relationData)) {
568
+ // Handle singular inverse relation as a single object
569
+ if (typeof relationData === 'object' && relationData !== null &&
570
+ (relationData.id || Object.keys(relationData).length > 1)) {
571
+ const relatedInstance = new ModelClass();
572
+
573
+ // Check relation metadata to identify foreign key fields that need to be preserved
574
+ const tempInstance = new ModelClass();
575
+ const childClassFields = FieldsHelper.getAllClassFields(tempInstance.constructor);
576
+ const [childRelOneData, childRelManyData] = await Promise.all([
577
+ RelationUtils.getRelationOneMeta(tempInstance, childClassFields),
578
+ RelationUtils.getRelationManyMeta(tempInstance, childClassFields)
579
+ ]);
580
+
581
+ // Store foreign key fields from relation metadata
582
+ for (const relKey in childRelOneData) {
583
+ const relMeta = childRelOneData[relKey];
584
+ const foreignKeyField = relMeta.hydrationField; // e.g., 'avatar_id', 'knowledge_id'
585
+ if (foreignKeyField && relationData[foreignKeyField] !== undefined) {
586
+ relatedInstance._relationFields[foreignKeyField] = relationData[foreignKeyField];
587
+ relatedInstance[foreignKeyField] = relationData[foreignKeyField];
588
+ }
589
+ }
590
+
591
+ for (const relKey in childRelManyData) {
592
+ const relMeta = childRelManyData[relKey];
593
+ const foreignKeyField = relMeta.foreignKey; // Use foreignKey for inverse relations
594
+ if (foreignKeyField && relationData[foreignKeyField] !== undefined) {
595
+ relatedInstance._relationFields[foreignKeyField] = relationData[foreignKeyField];
596
+ relatedInstance[foreignKeyField] = relationData[foreignKeyField];
597
+ }
598
+ }
599
+
600
+ await relatedInstance._asyncFill(relationData, false, false, true);
601
+ this[key] = relatedInstance;
602
+ }
603
+ } else if (Array.isArray(relationData) && relationData.length > 0) {
604
+ // Handle regular one-to-many relations as arrays
605
+ const relatedInstances = [];
606
+ for (const itemData of relationData) {
607
+ if (typeof itemData === 'object' && itemData !== null) {
608
+ const relatedInstance = new ModelClass();
609
+
610
+ // Check relation metadata to identify foreign key fields that need to be preserved
611
+ const tempInstance = new ModelClass();
612
+ const childClassFields = FieldsHelper.getAllClassFields(tempInstance.constructor);
613
+ const [childRelOneData, childRelManyData] = await Promise.all([
614
+ RelationUtils.getRelationOneMeta(tempInstance, childClassFields),
615
+ RelationUtils.getRelationManyMeta(tempInstance, childClassFields)
616
+ ]);
617
+
618
+ // Store foreign key fields from relation metadata
619
+ for (const relKey in childRelOneData) {
620
+ const relMeta = childRelOneData[relKey];
621
+ const foreignKeyField = relMeta.hydrationField; // e.g., 'avatar_id', 'knowledge_id'
622
+ if (foreignKeyField && itemData[foreignKeyField] !== undefined) {
623
+ relatedInstance._relationFields[foreignKeyField] = itemData[foreignKeyField];
624
+ relatedInstance[foreignKeyField] = itemData[foreignKeyField];
625
+ }
626
+ }
627
+
628
+ for (const relKey in childRelManyData) {
629
+ const relMeta = childRelManyData[relKey];
630
+ const foreignKeyField = relMeta.foreignKey; // Use foreignKey for inverse relations
631
+ if (foreignKeyField && itemData[foreignKeyField] !== undefined) {
632
+ relatedInstance._relationFields[foreignKeyField] = itemData[foreignKeyField];
633
+ relatedInstance[foreignKeyField] = itemData[foreignKeyField];
634
+ }
635
+ }
636
+
637
+ await relatedInstance._asyncFill(itemData, false, false, true);
638
+ relatedInstances.push(relatedInstance);
639
+ }
640
+ }
641
+ this[key] = relatedInstances;
642
+ }
643
+ }
644
+ }
645
+ }
646
+ }
647
+
423
648
  public static getDb(): DBService
424
649
  {
425
650
  return this.services.dbService;
@@ -438,7 +663,35 @@ class RWSModel<T> implements IModel {
438
663
  where[pk as string] = this[pk as string]
439
664
  }
440
665
 
441
- return await FindUtils.findOneBy(this.constructor as OpModelType<any>, { conditions: where });
666
+ // Find the fresh data from database
667
+ const freshData = await FindUtils.findOneBy(this.constructor as OpModelType<any>, { conditions: where });
668
+
669
+ if (!freshData) {
670
+ return null;
671
+ }
672
+
673
+ // Convert the fresh instance back to plain data for hydration
674
+ const plainData = await freshData.toMongo();
675
+
676
+ // Preserve foreign key fields from _relationFields to ensure relations can be hydrated
677
+ for (const key in this._relationFields) {
678
+ if (plainData[key] === undefined) {
679
+ plainData[key] = this._relationFields[key];
680
+ }
681
+ }
682
+
683
+ // Hydrate current instance with fresh data including relations
684
+ await this._asyncFill(plainData, true, true, true);
685
+
686
+ return this;
687
+ }
688
+
689
+ // Helper method to get property with fallback to stored relation fields
690
+ getPropertyValue(key: string): any {
691
+ if (this.hasOwnProperty(key) || this[key] !== undefined) {
692
+ return this[key];
693
+ }
694
+ return this._relationFields[key];
442
695
  }
443
696
  }
444
697
 
@@ -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"