@rws-framework/db 4.1.5 → 4.1.7

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.
@@ -7,6 +7,9 @@ export interface InverseRelationOpts {
7
7
  singular?: boolean;
8
8
  relationName?: string;
9
9
  mappingName?: string;
10
+ orderBy?: {
11
+ [field: string]: 'asc' | 'desc';
12
+ };
10
13
  }
11
14
  declare function InverseRelation(inversionModel: () => OpModelType<RWSModel<any>>, sourceModel: () => OpModelType<RWSModel<any>>, relationOptions?: Partial<InverseRelationOpts>): (target: any, key: string) => void;
12
15
  export default InverseRelation;
@@ -17,6 +17,10 @@ declare class RWSModel<T> implements IModel {
17
17
  static _CUT_KEYS: string[];
18
18
  private _relationFields;
19
19
  private postLoadExecuted;
20
+ /**
21
+ * Store relation foreign key fields for later hydration
22
+ */
23
+ private storeRelationFields;
20
24
  constructor(data?: any);
21
25
  isPostLoadExecuted(): boolean;
22
26
  setPostLoadExecuted(): void;
@@ -30,6 +30,17 @@ class RWSModel {
30
30
  // Store relation foreign key fields for reload() functionality
31
31
  _relationFields = {};
32
32
  postLoadExecuted = false;
33
+ /**
34
+ * Store relation foreign key fields for later hydration
35
+ */
36
+ storeRelationFields(data, relOneData) {
37
+ for (const relationName in relOneData) {
38
+ const relationMeta = relOneData[relationName];
39
+ if (relationMeta.hydrationField && data[relationMeta.hydrationField]) {
40
+ this._relationFields[relationMeta.hydrationField] = data[relationMeta.hydrationField];
41
+ }
42
+ }
43
+ }
33
44
  constructor(data = null) {
34
45
  if (!this.getCollection()) {
35
46
  throw new Error('Model must have a collection defined');
@@ -109,6 +120,15 @@ class RWSModel {
109
120
  collections_to_models[model.getCollection()] = model;
110
121
  });
111
122
  const seriesHydrationfields = [];
123
+ // Always preprocess foreign keys to relation objects, even if allowRelations is false
124
+ // This is necessary because toMongo() expects relation objects to exist
125
+ try {
126
+ data = await HydrateUtils_1.HydrateUtils.preprocessForeignKeys(data, this, relOneData);
127
+ }
128
+ catch (error) {
129
+ console.error('Error in preprocessForeignKeys:', error);
130
+ throw error;
131
+ }
112
132
  // Check if relations are already populated from Prisma includes
113
133
  const relationsAlreadyPopulated = this.checkRelationsPrePopulated(data, relOneData, relManyData);
114
134
  if (allowRelations && !relationsAlreadyPopulated) {
@@ -118,16 +138,25 @@ class RWSModel {
118
138
  else if (allowRelations && relationsAlreadyPopulated) {
119
139
  // Relations are already populated from Prisma, just assign them directly
120
140
  await this.hydratePrePopulatedRelations(data, relOneData, relManyData);
121
- // Create a copy of data without relation fields to prevent overwriting hydrated relations
141
+ // Create a copy of data without relation fields AND foreign key fields to prevent conflicts
122
142
  const dataWithoutRelations = { ...data };
123
143
  for (const key in relOneData) {
124
- delete dataWithoutRelations[key];
144
+ delete dataWithoutRelations[key]; // Remove relation field (e.g., 'screenFile')
145
+ const relationMeta = relOneData[key];
146
+ if (relationMeta.hydrationField) {
147
+ delete dataWithoutRelations[relationMeta.hydrationField]; // Remove foreign key field (e.g., 'screen_file_id')
148
+ }
125
149
  }
126
150
  for (const key in relManyData) {
127
- delete dataWithoutRelations[key];
151
+ delete dataWithoutRelations[key]; // Remove relation array fields
128
152
  }
129
153
  data = dataWithoutRelations;
130
154
  }
155
+ else {
156
+ // Even if relations are disabled, we still need to store relation foreign key fields
157
+ // for later hydration when reload() is called
158
+ this.storeRelationFields(data, relOneData);
159
+ }
131
160
  // Process regular fields and time series (excluding relations when pre-populated)
132
161
  await HydrateUtils_1.HydrateUtils.hydrateDataFields(this, collections_to_models, relOneData, seriesHydrationfields, fullDataMode, data);
133
162
  if (!this.isPostLoadExecuted() && postLoadExecute) {
@@ -161,33 +190,29 @@ class RWSModel {
161
190
  // Get relation metadata to determine how to handle each relation
162
191
  const classFields = FieldsHelper_1.FieldsHelper.getAllClassFields(this.constructor);
163
192
  const relOneData = await this.getRelationOneMeta(classFields);
193
+ // Check if this is a new model (no ID) to handle relations differently during creation
194
+ const isNewModel = !this.id;
164
195
  for (const key in this) {
165
196
  if (await this.hasRelation(key)) {
166
197
  const relationMeta = relOneData[key];
167
198
  if (relationMeta) {
168
- // Use connect on relations that are either:
169
- // 1. Required (required: true)
170
- // 2. Have explicitly set cascade options (metaOpts.cascade)
171
- const hasExplicitCascade = relationMeta.cascade && Object.keys(relationMeta.cascade).length > 0;
172
- const shouldUseConnect = relationMeta.required || hasExplicitCascade;
173
- if (shouldUseConnect) {
174
- // Relations with required=true or explicit cascade → use connect
175
- if (this[key] === null) {
199
+ // Always use Prisma relation syntax when relation objects exist
200
+ // This ensures compatibility with Prisma's expected format
201
+ if (this[key] === null || this[key] === undefined) {
202
+ // Only add disconnect if this is an update (not a creation)
203
+ if (!isNewModel) {
176
204
  data[key] = { disconnect: true };
177
205
  }
178
- else if (this[key] && this[key].id) {
179
- data[key] = { connect: { id: this[key].id } };
180
- }
206
+ // For new models, we simply don't include the relation field
181
207
  }
182
- else {
183
- // Simple optional relations use foreign key field directly
184
- const foreignKeyField = relationMeta.hydrationField;
185
- if (this[key] === null) {
186
- data[foreignKeyField] = null;
187
- }
188
- else if (this[key] && this[key].id) {
189
- data[foreignKeyField] = this[key].id;
190
- }
208
+ else if (this[key] && this[key].id) {
209
+ data[key] = { connect: { id: this[key].id } };
210
+ }
211
+ else if (this[key] && typeof this[key] === 'object' && !this[key].id) {
212
+ // Handle case where we have a relation object but it might not have an ID yet
213
+ // This could happen if the relation object is also being created
214
+ // In this case, we might want to create the relation first or handle it differently
215
+ console.warn(`Relation ${key} has an object without ID. This might cause issues during creation.`);
191
216
  }
192
217
  }
193
218
  continue;
@@ -375,7 +400,16 @@ class RWSModel {
375
400
  // Add one-to-many relations (without nesting)
376
401
  for (const key in relManyData) {
377
402
  if (shouldIncludeRelation(key)) {
378
- includes[key] = true; // Only load this level, no nested relations
403
+ const relationMeta = relManyData[key];
404
+ if (relationMeta.orderBy) {
405
+ // Add Prisma orderBy to the relation include
406
+ includes[key] = {
407
+ orderBy: relationMeta.orderBy
408
+ };
409
+ }
410
+ else {
411
+ includes[key] = true; // Only load this level, no nested relations
412
+ }
379
413
  }
380
414
  }
381
415
  return Object.keys(includes).length > 0 ? includes : null;
@@ -24,6 +24,9 @@ export type RelManyMetaType<T extends IRWSModel> = {
24
24
  inversionModel: OpModelType<T>;
25
25
  foreignKey: string;
26
26
  singular: boolean;
27
+ orderBy?: {
28
+ [field: string]: 'asc' | 'desc';
29
+ };
27
30
  };
28
31
  };
29
32
  export {};
@@ -2,6 +2,10 @@ import { RWSModel } from "../core/RWSModel";
2
2
  import { RelManyMetaType, RelOneMetaType } from "../types/RelationTypes";
3
3
  import { IRWSModel } from "../../types/IRWSModel";
4
4
  export declare class HydrateUtils {
5
+ /**
6
+ * Preprocess database data to convert foreign keys to relation objects when relations are not already populated
7
+ */
8
+ static preprocessForeignKeys(data: any, model: RWSModel<any>, relOneData: RelOneMetaType<IRWSModel>): Promise<any>;
5
9
  static hydrateDataFields(model: RWSModel<any>, collections_to_models: {
6
10
  [key: string]: any;
7
11
  }, relOneData: RelOneMetaType<IRWSModel>, seriesHydrationfields: string[], fullDataMode: boolean, data: {
@@ -9,6 +9,41 @@ const RelationUtils_1 = require("./RelationUtils");
9
9
  const ModelUtils_1 = require("./ModelUtils");
10
10
  const chalk_1 = __importDefault(require("chalk"));
11
11
  class HydrateUtils {
12
+ /**
13
+ * Preprocess database data to convert foreign keys to relation objects when relations are not already populated
14
+ */
15
+ static async preprocessForeignKeys(data, model, relOneData) {
16
+ const processedData = { ...data };
17
+ // For each relation, handle different scenarios during creation and updates
18
+ for (const relationName in relOneData) {
19
+ const relationMeta = relOneData[relationName];
20
+ const foreignKeyField = relationMeta.hydrationField; // e.g., "tutorial_id"
21
+ const relationField = relationMeta.key; // e.g., "tutorial"
22
+ // Scenario 1: We have a foreign key value but no relation object (or the relation is just an ID)
23
+ if (foreignKeyField in data && data[foreignKeyField] !== null && data[foreignKeyField] !== undefined &&
24
+ (!data[relationField] || typeof data[relationField] !== 'object')) {
25
+ // Create a minimal relation object with just the ID
26
+ // This allows toMongo() to work properly without requiring full relation loading
27
+ processedData[relationField] = { id: data[foreignKeyField] };
28
+ }
29
+ // Scenario 2: We have a relation object but no foreign key field (common during creation)
30
+ // Ensure the foreign key field exists when we have a valid relation object
31
+ else if (data[relationField] && typeof data[relationField] === 'object' &&
32
+ data[relationField].id &&
33
+ (!(foreignKeyField in data) || data[foreignKeyField] === null || data[foreignKeyField] === undefined)) {
34
+ // Set the foreign key field from the relation object's ID
35
+ processedData[foreignKeyField] = data[relationField].id;
36
+ }
37
+ // Scenario 3: Both relation object and foreign key exist, ensure they're consistent
38
+ else if (data[relationField] && typeof data[relationField] === 'object' &&
39
+ data[relationField].id && foreignKeyField in data &&
40
+ data[foreignKeyField] !== data[relationField].id) {
41
+ // Prioritize the relation object's ID
42
+ processedData[foreignKeyField] = data[relationField].id;
43
+ }
44
+ }
45
+ return processedData;
46
+ }
12
47
  static async hydrateDataFields(model, collections_to_models, relOneData, seriesHydrationfields, fullDataMode, data) {
13
48
  const timeSeriesIds = TimeSeriesUtils_1.TimeSeriesUtils.getTimeSeriesModelFields(model);
14
49
  // Build a set of foreign key field names to skip
@@ -97,9 +132,14 @@ class HydrateUtils {
97
132
  const relMeta = relOneData[key];
98
133
  const relationEnabled = !RelationUtils_1.RelationUtils.checkRelDisabled(model, relMeta.key);
99
134
  if (!data[relMeta.hydrationField] && relMeta.required) {
100
- // Only throw error if this is a fresh load, not a reload of existing model
101
- if (!model.id) {
102
- throw new Error(`Relation field "${relMeta.hydrationField}" is required in model ${model.constructor.name}.`);
135
+ // Only throw error if this is a fresh load AND we're not in creation mode
136
+ // During creation, required relations might not have their foreign key set yet
137
+ if (!model.id && data[relMeta.key]) {
138
+ // We have the relation object but not the foreign key - this is okay during creation
139
+ continue;
140
+ }
141
+ else if (!model.id && !data[relMeta.key]) {
142
+ throw new Error(`Required relation "${relMeta.key}" is missing in model ${model.constructor.name}.`);
103
143
  }
104
144
  // For existing models (reloads), skip loading this relation if the field is missing
105
145
  continue;
@@ -42,7 +42,8 @@ class RelationUtils {
42
42
  key: resolvedMetadata.key,
43
43
  inversionModel: resolvedMetadata.inversionModel,
44
44
  foreignKey: resolvedMetadata.foreignKey,
45
- singular: resolvedMetadata?.singular || false
45
+ singular: resolvedMetadata?.singular || false,
46
+ orderBy: resolvedMetadata?.orderBy || null
46
47
  };
47
48
  }
48
49
  }
@@ -35,11 +35,6 @@ declare class DBService {
35
35
  count<T = any>(opModel: OpModelType<T>, where?: {
36
36
  [k: string]: any;
37
37
  }): Promise<number>;
38
- /**
39
- * Convert foreign key fields to Prisma relation syntax
40
- * Handles common patterns like user_id -> creator, avatar_id -> avatar, etc.
41
- */
42
- private convertForeignKeysToRelations;
43
38
  getPrismaClient(): PrismaClient;
44
39
  }
45
40
  export { DBService, IDBClientCreate };
@@ -120,11 +120,9 @@ class DBService {
120
120
  delete data[cKey];
121
121
  }
122
122
  }
123
- // Convert foreign key fields to Prisma relation syntax
124
- const processedData = this.convertForeignKeysToRelations(data);
125
123
  await prismaCollection.update({
126
124
  where,
127
- data: processedData,
125
+ data,
128
126
  });
129
127
  return await this.findOneBy(collection, where);
130
128
  }
@@ -246,57 +244,6 @@ class DBService {
246
244
  async count(opModel, where = {}) {
247
245
  return await this.getCollectionHandler(opModel._collection).count({ where });
248
246
  }
249
- /**
250
- * Convert foreign key fields to Prisma relation syntax
251
- * Handles common patterns like user_id -> creator, avatar_id -> avatar, etc.
252
- */
253
- convertForeignKeysToRelations(data) {
254
- const processedData = { ...data };
255
- const relationMappings = {
256
- // Common relation mappings for foreign keys to relation names
257
- 'user_id': 'creator',
258
- 'avatar_id': 'avatar',
259
- 'file_id': 'logo',
260
- 'company_id': 'company',
261
- 'user_group_id': 'userGroup',
262
- 'accountGrade_id': 'accountGrade',
263
- 'account_balance_id': 'accountBalance',
264
- 'knowledgeGroup_id': 'project',
265
- 'conversation_id': 'conversation',
266
- 'message_id': 'message',
267
- 'knowledge_id': 'knowledge',
268
- 'profession_id': 'profession',
269
- 'step_id': 'step',
270
- 'question_id': 'question',
271
- 'tutorial_id': 'tutorial',
272
- 'tutorial_step_id': 'tutorialStep',
273
- 'tutorial_section_id': 'tutorialSection',
274
- 'todo_id': 'todo',
275
- 'bot_test_id': 'botTest',
276
- 'bot_test_tester_id': 'tester',
277
- 'bot_test_target_id': 'target',
278
- 'branch_id': 'branch',
279
- 'acl_policy_id': 'policy',
280
- 'instruction_file_id': 'instructionFile'
281
- };
282
- // Convert foreign key fields to relation syntax
283
- Object.keys(processedData).forEach(key => {
284
- if (key.endsWith('_id') && relationMappings[key]) {
285
- const relationField = relationMappings[key];
286
- const value = processedData[key];
287
- // Remove the foreign key field
288
- delete processedData[key];
289
- // Add the relation field with proper Prisma syntax
290
- if (value === null || value === undefined) {
291
- processedData[relationField] = { disconnect: true };
292
- }
293
- else {
294
- processedData[relationField] = { connect: { id: value } };
295
- }
296
- }
297
- });
298
- return processedData;
299
- }
300
247
  getPrismaClient() {
301
248
  if (!this.client || !this.connected) {
302
249
  this.connectToDB();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rws-framework/db",
3
3
  "private": false,
4
- "version": "4.1.5",
4
+ "version": "4.1.7",
5
5
  "description": "",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -9,6 +9,7 @@ export interface InverseRelationOpts {
9
9
  singular?: boolean
10
10
  relationName?: string
11
11
  mappingName?: string
12
+ orderBy?: { [field: string]: 'asc' | 'desc' }
12
13
  }
13
14
 
14
15
  function guessForeignKey(inversionModel: OpModelType<RWSModel<any>>, bindingModel: OpModelType<RWSModel<any>>, decoratorsData: any)
@@ -33,6 +33,18 @@ class RWSModel<T> implements IModel {
33
33
 
34
34
  private postLoadExecuted: boolean = false;
35
35
 
36
+ /**
37
+ * Store relation foreign key fields for later hydration
38
+ */
39
+ private storeRelationFields(data: any, relOneData: any): void {
40
+ for (const relationName in relOneData) {
41
+ const relationMeta = relOneData[relationName];
42
+ if (relationMeta.hydrationField && data[relationMeta.hydrationField]) {
43
+ this._relationFields[relationMeta.hydrationField] = data[relationMeta.hydrationField];
44
+ }
45
+ }
46
+ }
47
+
36
48
  constructor(data: any = null) {
37
49
  if(!this.getCollection()){
38
50
  throw new Error('Model must have a collection defined');
@@ -130,10 +142,20 @@ class RWSModel<T> implements IModel {
130
142
  collections_to_models[model.getCollection()] = model;
131
143
  });
132
144
 
133
- const seriesHydrationfields: string[] = [];
145
+ const seriesHydrationfields: string[] = [];
146
+
147
+ // Always preprocess foreign keys to relation objects, even if allowRelations is false
148
+ // This is necessary because toMongo() expects relation objects to exist
149
+ try {
150
+ data = await HydrateUtils.preprocessForeignKeys(data, this, relOneData);
151
+ } catch (error) {
152
+ console.error('Error in preprocessForeignKeys:', error);
153
+ throw error;
154
+ }
134
155
 
135
156
  // Check if relations are already populated from Prisma includes
136
157
  const relationsAlreadyPopulated = this.checkRelationsPrePopulated(data, relOneData, relManyData);
158
+
137
159
 
138
160
  if (allowRelations && !relationsAlreadyPopulated) {
139
161
  // Use traditional relation hydration if not pre-populated
@@ -142,15 +164,24 @@ class RWSModel<T> implements IModel {
142
164
  // Relations are already populated from Prisma, just assign them directly
143
165
  await this.hydratePrePopulatedRelations(data, relOneData, relManyData);
144
166
 
145
- // Create a copy of data without relation fields to prevent overwriting hydrated relations
167
+ // Create a copy of data without relation fields AND foreign key fields to prevent conflicts
146
168
  const dataWithoutRelations = { ...data };
147
169
  for (const key in relOneData) {
148
- delete dataWithoutRelations[key];
170
+ delete dataWithoutRelations[key]; // Remove relation field (e.g., 'screenFile')
171
+
172
+ const relationMeta = relOneData[key];
173
+ if (relationMeta.hydrationField) {
174
+ delete dataWithoutRelations[relationMeta.hydrationField]; // Remove foreign key field (e.g., 'screen_file_id')
175
+ }
149
176
  }
150
177
  for (const key in relManyData) {
151
- delete dataWithoutRelations[key];
178
+ delete dataWithoutRelations[key]; // Remove relation array fields
152
179
  }
153
180
  data = dataWithoutRelations;
181
+ } else {
182
+ // Even if relations are disabled, we still need to store relation foreign key fields
183
+ // for later hydration when reload() is called
184
+ this.storeRelationFields(data, relOneData);
154
185
  }
155
186
 
156
187
  // Process regular fields and time series (excluding relations when pre-populated)
@@ -195,6 +226,7 @@ class RWSModel<T> implements IModel {
195
226
  }
196
227
 
197
228
  public async toMongo(): Promise<any> {
229
+
198
230
  const data: any = {};
199
231
  const timeSeriesIds = TimeSeriesUtils.getTimeSeriesModelFields(this);
200
232
  const timeSeriesHydrationFields: string[] = [];
@@ -202,33 +234,33 @@ class RWSModel<T> implements IModel {
202
234
  // Get relation metadata to determine how to handle each relation
203
235
  const classFields = FieldsHelper.getAllClassFields(this.constructor);
204
236
  const relOneData = await this.getRelationOneMeta(classFields);
205
-
237
+
238
+ // Check if this is a new model (no ID) to handle relations differently during creation
239
+ const isNewModel = !this.id;
240
+
206
241
  for (const key in (this as any)) {
242
+
207
243
  if (await this.hasRelation(key)) {
208
244
  const relationMeta = relOneData[key];
209
245
 
210
246
  if (relationMeta) {
211
- // Use connect on relations that are either:
212
- // 1. Required (required: true)
213
- // 2. Have explicitly set cascade options (metaOpts.cascade)
214
- const hasExplicitCascade = relationMeta.cascade && Object.keys(relationMeta.cascade).length > 0;
215
- const shouldUseConnect = relationMeta.required || hasExplicitCascade;
216
247
 
217
- if (shouldUseConnect) {
218
- // Relations with required=true or explicit cascade → use connect
219
- if (this[key] === null) {
248
+ // Always use Prisma relation syntax when relation objects exist
249
+ // This ensures compatibility with Prisma's expected format
250
+
251
+ if (this[key] === null || this[key] === undefined) {
252
+ // Only add disconnect if this is an update (not a creation)
253
+ if (!isNewModel) {
220
254
  data[key] = { disconnect: true };
221
- } else if (this[key] && this[key].id) {
222
- data[key] = { connect: { id: this[key].id } };
223
- }
224
- } else {
225
- // Simple optional relations → use foreign key field directly
226
- const foreignKeyField = relationMeta.hydrationField;
227
- if (this[key] === null) {
228
- data[foreignKeyField] = null;
229
- } else if (this[key] && this[key].id) {
230
- data[foreignKeyField] = this[key].id;
231
255
  }
256
+ // For new models, we simply don't include the relation field
257
+ } else if (this[key] && this[key].id) {
258
+ data[key] = { connect: { id: this[key].id } };
259
+ } else if (this[key] && typeof this[key] === 'object' && !this[key].id) {
260
+ // Handle case where we have a relation object but it might not have an ID yet
261
+ // This could happen if the relation object is also being created
262
+ // In this case, we might want to create the relation first or handle it differently
263
+ console.warn(`Relation ${key} has an object without ID. This might cause issues during creation.`);
232
264
  }
233
265
  }
234
266
 
@@ -256,7 +288,7 @@ class RWSModel<T> implements IModel {
256
288
  timeSeriesHydrationFields.push(timeSeriesIds[key].hydrationField);
257
289
  }
258
290
  }
259
-
291
+
260
292
  return data;
261
293
  }
262
294
 
@@ -268,8 +300,9 @@ class RWSModel<T> implements IModel {
268
300
  return (this as any).constructor._collection || this._collection;
269
301
  }
270
302
 
271
- async save(): Promise<this> {
303
+ async save(): Promise<this> {
272
304
  const data = await this.toMongo();
305
+
273
306
  let updatedModelData = data;
274
307
 
275
308
  const entryExists = await ModelUtils.entryExists(this);
@@ -495,7 +528,15 @@ class RWSModel<T> implements IModel {
495
528
  // Add one-to-many relations (without nesting)
496
529
  for (const key in relManyData) {
497
530
  if (shouldIncludeRelation(key)) {
498
- includes[key] = true; // Only load this level, no nested relations
531
+ const relationMeta = relManyData[key];
532
+ if (relationMeta.orderBy) {
533
+ // Add Prisma orderBy to the relation include
534
+ includes[key] = {
535
+ orderBy: relationMeta.orderBy
536
+ };
537
+ } else {
538
+ includes[key] = true; // Only load this level, no nested relations
539
+ }
499
540
  }
500
541
  }
501
542
 
@@ -25,6 +25,7 @@ export type RelManyMetaType<T extends IRWSModel> = {
25
25
  key: string,
26
26
  inversionModel: OpModelType<T>,
27
27
  foreignKey: string,
28
- singular: boolean
28
+ singular: boolean,
29
+ orderBy?: { [field: string]: 'asc' | 'desc' }
29
30
  }
30
31
  };
@@ -9,7 +9,52 @@ import { FieldsHelper } from "../../helper/FieldsHelper";
9
9
  import chalk from 'chalk';
10
10
 
11
11
  export class HydrateUtils {
12
+ /**
13
+ * Preprocess database data to convert foreign keys to relation objects when relations are not already populated
14
+ */
15
+ static async preprocessForeignKeys(data: any, model: RWSModel<any>, relOneData: RelOneMetaType<IRWSModel>): Promise<any> {
16
+ const processedData = { ...data };
17
+
18
+ // For each relation, handle different scenarios during creation and updates
19
+ for (const relationName in relOneData) {
20
+ const relationMeta = relOneData[relationName];
21
+ const foreignKeyField = relationMeta.hydrationField; // e.g., "tutorial_id"
22
+ const relationField = relationMeta.key; // e.g., "tutorial"
23
+
24
+ // Scenario 1: We have a foreign key value but no relation object (or the relation is just an ID)
25
+ if (foreignKeyField in data && data[foreignKeyField] !== null && data[foreignKeyField] !== undefined &&
26
+ (!data[relationField] || typeof data[relationField] !== 'object')) {
27
+
28
+ // Create a minimal relation object with just the ID
29
+ // This allows toMongo() to work properly without requiring full relation loading
30
+ processedData[relationField] = { id: data[foreignKeyField] };
31
+ }
32
+
33
+ // Scenario 2: We have a relation object but no foreign key field (common during creation)
34
+ // Ensure the foreign key field exists when we have a valid relation object
35
+ else if (data[relationField] && typeof data[relationField] === 'object' &&
36
+ data[relationField].id &&
37
+ (!(foreignKeyField in data) || data[foreignKeyField] === null || data[foreignKeyField] === undefined)) {
38
+
39
+ // Set the foreign key field from the relation object's ID
40
+ processedData[foreignKeyField] = data[relationField].id;
41
+ }
42
+
43
+ // Scenario 3: Both relation object and foreign key exist, ensure they're consistent
44
+ else if (data[relationField] && typeof data[relationField] === 'object' &&
45
+ data[relationField].id && foreignKeyField in data &&
46
+ data[foreignKeyField] !== data[relationField].id) {
47
+
48
+ // Prioritize the relation object's ID
49
+ processedData[foreignKeyField] = data[relationField].id;
50
+ }
51
+ }
52
+
53
+ return processedData;
54
+ }
55
+
12
56
  static async hydrateDataFields(model: RWSModel<any>, collections_to_models: { [key: string]: any }, relOneData: RelOneMetaType<IRWSModel>, seriesHydrationfields: string[], fullDataMode: boolean, data: { [key: string]: any }) {
57
+
13
58
  const timeSeriesIds = TimeSeriesUtils.getTimeSeriesModelFields(model);
14
59
 
15
60
  // Build a set of foreign key field names to skip
@@ -26,6 +71,7 @@ export class HydrateUtils {
26
71
 
27
72
  for (const key in data) {
28
73
  if (data.hasOwnProperty(key)) {
74
+
29
75
  if (!fullDataMode && ignoredKeys.includes(key)) {
30
76
  continue;
31
77
  }
@@ -117,9 +163,13 @@ export class HydrateUtils {
117
163
  const relationEnabled = !RelationUtils.checkRelDisabled(model, relMeta.key);
118
164
 
119
165
  if (!data[relMeta.hydrationField] && relMeta.required) {
120
- // Only throw error if this is a fresh load, not a reload of existing model
121
- if (!model.id) {
122
- throw new Error(`Relation field "${relMeta.hydrationField}" is required in model ${model.constructor.name}.`);
166
+ // Only throw error if this is a fresh load AND we're not in creation mode
167
+ // During creation, required relations might not have their foreign key set yet
168
+ if (!model.id && data[relMeta.key]) {
169
+ // We have the relation object but not the foreign key - this is okay during creation
170
+ continue;
171
+ } else if (!model.id && !data[relMeta.key]) {
172
+ throw new Error(`Required relation "${relMeta.key}" is missing in model ${model.constructor.name}.`);
123
173
  }
124
174
  // For existing models (reloads), skip loading this relation if the field is missing
125
175
  continue;
@@ -51,7 +51,8 @@ export class RelationUtils {
51
51
  key: resolvedMetadata.key,
52
52
  inversionModel: resolvedMetadata.inversionModel,
53
53
  foreignKey: resolvedMetadata.foreignKey,
54
- singular: resolvedMetadata?.singular || false
54
+ singular: resolvedMetadata?.singular || false,
55
+ orderBy: resolvedMetadata?.orderBy || null
55
56
  };
56
57
  }
57
58
  }
@@ -1,15 +1,16 @@
1
1
  import { PrismaClient } from '@prisma/client';
2
2
  import { Collection, Db, MongoClient } from 'mongodb';
3
- import {ITimeSeries} from '../types/ITimeSeries';
3
+ import { ITimeSeries } from '../types/ITimeSeries';
4
4
  import { IModel } from '../models/interfaces/IModel';
5
5
  import chalk from 'chalk';
6
6
  import { IDbConfigHandler } from '../types/DbConfigHandler';
7
7
  import { IPaginationParams, OrderByType, OrderByField, OrderByArray } from '../types/FindParams';
8
8
  import { OpModelType } from '../models/interfaces/OpModelType';
9
+ import { ModelUtils } from '../models/utils/ModelUtils';
9
10
 
10
11
  interface IDBClientCreate {
11
- dbUrl?: string;
12
- dbName?: string;
12
+ dbUrl?: string;
13
+ dbName?: string;
13
14
  }
14
15
 
15
16
  class DBService {
@@ -17,66 +18,62 @@ class DBService {
17
18
  private opts: IDBClientCreate = null;
18
19
  private connected = false;
19
20
 
20
- constructor(private configService: IDbConfigHandler){}
21
+ constructor(private configService: IDbConfigHandler) { }
21
22
 
22
23
  private connectToDB(opts: IDBClientCreate = null) {
23
- if(opts){
24
+ if (opts) {
24
25
  this.opts = opts;
25
- }else{
26
+ } else {
26
27
  this.opts = {
27
- dbUrl: this.configService.get('db_url'),
28
+ dbUrl: this.configService.get('db_url'),
28
29
  dbName: this.configService.get('db_name'),
29
30
  };
30
31
  }
31
32
 
32
- if(!this.opts.dbUrl){
33
+ if (!this.opts.dbUrl) {
33
34
  console.log(chalk.red('No database config set in @rws-framework/db'));
34
35
 
35
36
  return;
36
- }
37
-
38
- try{
39
- this.client = new PrismaClient({
37
+ }
38
+
39
+ try {
40
+ this.client = new PrismaClient({
40
41
  datasources: {
41
42
  db: {
42
43
  url: this.opts.dbUrl
43
44
  },
44
45
  },
45
- });
46
+ });
46
47
 
47
48
  this.connected = true;
48
- } catch (e: Error | any){
49
+ } catch (e: Error | any) {
49
50
  console.error(e);
50
-
51
+
51
52
  throw new Error('PRISMA CONNECTION ERROR');
52
53
  }
53
54
  }
54
55
 
55
- reconnect(opts: IDBClientCreate = null)
56
- {
56
+ reconnect(opts: IDBClientCreate = null) {
57
57
  this.connectToDB(opts);
58
58
  }
59
59
 
60
- static baseClientConstruct(dbUrl: string): MongoClient
61
- {
60
+ static baseClientConstruct(dbUrl: string): MongoClient {
62
61
  const client = new MongoClient(dbUrl);
63
62
 
64
63
  return client;
65
64
  }
66
65
 
67
- public async createBaseMongoClient(): Promise<MongoClient>
68
- {
66
+ public async createBaseMongoClient(): Promise<MongoClient> {
69
67
  const dbUrl = this.opts?.dbUrl || this.configService.get('db_url');
70
68
  const client = DBService.baseClientConstruct(dbUrl);
71
-
69
+
72
70
  await client.connect();
73
71
 
74
72
  return client;
75
73
 
76
74
  }
77
75
 
78
- public async createBaseMongoClientDB(): Promise<[MongoClient, Db]>
79
- {
76
+ public async createBaseMongoClientDB(): Promise<[MongoClient, Db]> {
80
77
  const dbName = this.opts?.dbName || this.configService.get('db_name');
81
78
  const client = await this.createBaseMongoClient();
82
79
  return [client, client.db(dbName)];
@@ -101,104 +98,98 @@ class DBService {
101
98
  await client.close();
102
99
  }
103
100
 
104
- async watchCollection(collectionName: string, preRun: () => void): Promise<any>
105
- {
101
+ async watchCollection(collectionName: string, preRun: () => void): Promise<any> {
106
102
  const [client, db] = await this.createBaseMongoClientDB();
107
103
  const collection = db.collection(collectionName);
108
104
 
109
- const changeStream = collection.watch();
110
- return new Promise((resolve) => {
111
- changeStream.on('change', (change) => {
105
+ const changeStream = collection.watch();
106
+ return new Promise((resolve) => {
107
+ changeStream.on('change', (change) => {
112
108
  resolve(change);
113
109
  });
114
110
 
115
111
  preRun();
116
- });
112
+ });
117
113
  }
118
114
 
119
115
  async insert(data: any, collection: string, isTimeSeries: boolean = false) {
120
-
116
+
121
117
  let result: any = data;
122
118
  // Insert time-series data outside of the transaction
123
119
 
124
- if(isTimeSeries){
120
+ if (isTimeSeries) {
125
121
  const [client, db] = await this.createBaseMongoClientDB();
126
122
  const collectionHandler = db.collection(collection);
127
-
123
+
128
124
  const insert = await collectionHandler.insertOne(data);
129
125
 
130
- result = await this.findOneBy(collection, { id: insert.insertedId.toString() });
126
+ result = await this.findOneBy(collection, { id: insert.insertedId.toString() });
131
127
  return result;
132
128
  }
133
129
 
134
- const prismaCollection = this.getCollectionHandler(collection);
130
+ const prismaCollection = this.getCollectionHandler(collection);
135
131
 
136
132
  result = await prismaCollection.create({ data });
137
133
 
138
134
  return await this.findOneBy(collection, { id: result.id });
139
135
  }
140
136
 
141
- async update(data: any, collection: string, pk: string | string[]): Promise<IModel>
142
- {
137
+ async update(data: any, collection: string, pk: string | string[]): Promise<IModel> {
143
138
 
144
139
  const prismaCollection = this.getCollectionHandler(collection);
145
140
 
146
141
 
147
142
  const where: any = {};
148
-
149
- if(Array.isArray(pk)){
150
- for(const pkElem of pk){
143
+
144
+ if (Array.isArray(pk)) {
145
+ for (const pkElem of pk) {
151
146
  where[pkElem] = data[pkElem];
152
147
  }
153
- }else{
148
+ } else {
154
149
  where[pk as string] = data[pk as string]
155
- }
150
+ }
156
151
 
157
- if(!Array.isArray(pk)){
152
+ if (!Array.isArray(pk)) {
158
153
  delete data[pk];
159
- }else{
160
- for(const cKey in pk){
154
+ } else {
155
+ for (const cKey in pk) {
161
156
  delete data[cKey];
162
157
  }
163
- }
164
-
165
- // Convert foreign key fields to Prisma relation syntax
166
- const processedData = this.convertForeignKeysToRelations(data);
158
+ }
167
159
 
168
160
  await prismaCollection.update({
169
161
  where,
170
- data: processedData,
171
- });
162
+ data,
163
+ });
164
+
172
165
 
173
-
174
166
  return await this.findOneBy(collection, where);
175
167
  }
176
-
177
168
 
178
- async findOneBy(collection: string, conditions: any, fields: string[] | null = null, ordering: OrderByType = null, prismaOptions: any = null): Promise<IModel|null>
179
- {
169
+
170
+ async findOneBy(collection: string, conditions: any, fields: string[] | null = null, ordering: OrderByType = null, prismaOptions: any = null): Promise<IModel | null> {
180
171
  const params: any = { where: conditions };
181
172
 
182
- if(fields){
173
+ if (fields) {
183
174
  params.select = {};
184
- fields.forEach((fieldName: string) => {
175
+ fields.forEach((fieldName: string) => {
185
176
  params.select[fieldName] = true;
186
- });
187
-
177
+ });
178
+
188
179
  // Add relation fields to select instead of using include when fields are specified
189
- if(prismaOptions?.include) {
180
+ if (prismaOptions?.include) {
190
181
  Object.keys(prismaOptions.include).forEach(relationField => {
191
182
  if (fields.includes(relationField)) {
192
183
  params.select[relationField] = true;
193
184
  }
194
185
  });
195
186
  }
196
- } else if(prismaOptions?.include) {
187
+ } else if (prismaOptions?.include) {
197
188
  // Only use include when no fields are specified
198
189
  params.include = prismaOptions.include;
199
190
  }
200
191
 
201
- if(ordering){
192
+ if (ordering) {
202
193
  params.orderBy = this.convertOrderingToPrismaFormat(ordering);
203
194
  }
204
195
 
@@ -207,63 +198,60 @@ class DBService {
207
198
  return retData;
208
199
  }
209
200
 
210
- async delete(collection: string, conditions: any): Promise<void>
211
- {
201
+ async delete(collection: string, conditions: any): Promise<void> {
212
202
  await this.getCollectionHandler(collection).deleteMany({ where: conditions });
213
203
  return;
214
204
  }
215
205
 
216
206
  async findBy(
217
- collection: string,
218
- conditions: any,
219
- fields: string[] | null = null,
220
- ordering: OrderByType = null,
207
+ collection: string,
208
+ conditions: any,
209
+ fields: string[] | null = null,
210
+ ordering: OrderByType = null,
221
211
  pagination: IPaginationParams = null,
222
- prismaOptions: any = null): Promise<IModel[]>
223
- {
224
- const params: any ={ where: conditions };
212
+ prismaOptions: any = null): Promise<IModel[]> {
213
+ const params: any = { where: conditions };
225
214
 
226
- if(fields){
215
+ if (fields) {
227
216
  params.select = {};
228
- fields.forEach((fieldName: string) => {
217
+ fields.forEach((fieldName: string) => {
229
218
  params.select[fieldName] = true;
230
- });
231
-
219
+ });
220
+
232
221
  // Add relation fields to select instead of using include when fields are specified
233
- if(prismaOptions?.include) {
222
+ if (prismaOptions?.include) {
234
223
  Object.keys(prismaOptions.include).forEach(relationField => {
235
224
  if (fields.includes(relationField)) {
236
225
  params.select[relationField] = true;
237
226
  }
238
227
  });
239
228
  }
240
- } else if(prismaOptions?.include) {
229
+ } else if (prismaOptions?.include) {
241
230
  // Only use include when no fields are specified
242
231
  params.include = prismaOptions.include;
243
232
  }
244
233
 
245
- if(ordering){
234
+ if (ordering) {
246
235
  params.orderBy = this.convertOrderingToPrismaFormat(ordering);
247
- }
236
+ }
248
237
 
249
- if(pagination){
238
+ if (pagination) {
250
239
  const perPage = pagination.per_page || 50;
251
240
  params.skip = (pagination.page || 0) * perPage;
252
241
  params.take = perPage;
253
242
  }
254
243
 
255
- const retData = await this.getCollectionHandler(collection).findMany(params);
244
+ const retData = await this.getCollectionHandler(collection).findMany(params);
256
245
 
257
246
  return retData;
258
247
  }
259
248
 
260
- async collectionExists(collection_name: string): Promise<boolean>
261
- {
249
+ async collectionExists(collection_name: string): Promise<boolean> {
262
250
  const dbUrl = this.opts?.dbUrl || this.configService.get('db_url');
263
251
  const client = new MongoClient(dbUrl);
264
252
 
265
253
  try {
266
- await client.connect();
254
+ await client.connect();
267
255
 
268
256
  const db = client.db(this.configService.get('db_name'));
269
257
 
@@ -275,12 +263,11 @@ class DBService {
275
263
  console.error('Error connecting to MongoDB:', error);
276
264
 
277
265
  throw error;
278
- }
266
+ }
279
267
  }
280
268
 
281
- async createTimeSeriesCollection(collection_name: string): Promise<Collection<ITimeSeries>>
282
- {
283
- try {
269
+ async createTimeSeriesCollection(collection_name: string): Promise<Collection<ITimeSeries>> {
270
+ try {
284
271
  const [client, db] = await this.createBaseMongoClientDB();
285
272
 
286
273
  // Create a time series collection
@@ -302,9 +289,8 @@ class DBService {
302
289
  }
303
290
  }
304
291
 
305
- private getCollectionHandler(collection: string): any
306
- {
307
- if(!this.client || !this.connected){
292
+ private getCollectionHandler(collection: string): any {
293
+ if (!this.client || !this.connected) {
308
294
  this.connectToDB();
309
295
  }
310
296
 
@@ -325,74 +311,17 @@ class DBService {
325
311
  return [ordering];
326
312
  }
327
313
 
328
- private setOpts(opts: IDBClientCreate = null): this
329
- {
314
+ private setOpts(opts: IDBClientCreate = null): this {
330
315
  this.opts = opts;
331
316
  return this;
332
317
  }
333
318
 
334
- public async count<T = any>(opModel: OpModelType<T>, where: {[k: string]: any} = {}): Promise<number>{
335
- return await this.getCollectionHandler(opModel._collection).count({where});
336
- }
337
-
338
- /**
339
- * Convert foreign key fields to Prisma relation syntax
340
- * Handles common patterns like user_id -> creator, avatar_id -> avatar, etc.
341
- */
342
- private convertForeignKeysToRelations(data: any): any {
343
- const processedData = { ...data };
344
- const relationMappings: { [key: string]: string } = {
345
- // Common relation mappings for foreign keys to relation names
346
- 'user_id': 'creator',
347
- 'avatar_id': 'avatar',
348
- 'file_id': 'logo',
349
- 'company_id': 'company',
350
- 'user_group_id': 'userGroup',
351
- 'accountGrade_id': 'accountGrade',
352
- 'account_balance_id': 'accountBalance',
353
- 'knowledgeGroup_id': 'project',
354
- 'conversation_id': 'conversation',
355
- 'message_id': 'message',
356
- 'knowledge_id': 'knowledge',
357
- 'profession_id': 'profession',
358
- 'step_id': 'step',
359
- 'question_id': 'question',
360
- 'tutorial_id': 'tutorial',
361
- 'tutorial_step_id': 'tutorialStep',
362
- 'tutorial_section_id': 'tutorialSection',
363
- 'todo_id': 'todo',
364
- 'bot_test_id': 'botTest',
365
- 'bot_test_tester_id': 'tester',
366
- 'bot_test_target_id': 'target',
367
- 'branch_id': 'branch',
368
- 'acl_policy_id': 'policy',
369
- 'instruction_file_id': 'instructionFile'
370
- };
371
-
372
- // Convert foreign key fields to relation syntax
373
- Object.keys(processedData).forEach(key => {
374
- if (key.endsWith('_id') && relationMappings[key]) {
375
- const relationField = relationMappings[key];
376
- const value = processedData[key];
377
-
378
- // Remove the foreign key field
379
- delete processedData[key];
380
-
381
- // Add the relation field with proper Prisma syntax
382
- if (value === null || value === undefined) {
383
- processedData[relationField] = { disconnect: true };
384
- } else {
385
- processedData[relationField] = { connect: { id: value } };
386
- }
387
- }
388
- });
389
-
390
- return processedData;
319
+ public async count<T = any>(opModel: OpModelType<T>, where: { [k: string]: any } = {}): Promise<number> {
320
+ return await this.getCollectionHandler(opModel._collection).count({ where });
391
321
  }
392
322
 
393
- public getPrismaClient(): PrismaClient
394
- {
395
- if(!this.client || !this.connected){
323
+ public getPrismaClient(): PrismaClient {
324
+ if (!this.client || !this.connected) {
396
325
  this.connectToDB();
397
326
  }
398
327