masterrecord 0.2.36 → 0.3.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.
Files changed (38) hide show
  1. package/.claude/settings.local.json +20 -1
  2. package/Entity/entityModel.js +6 -0
  3. package/Entity/entityTrackerModel.js +20 -3
  4. package/Entity/fieldTransformer.js +266 -0
  5. package/Migrations/migrationMySQLQuery.js +145 -1
  6. package/Migrations/migrationPostgresQuery.js +402 -0
  7. package/Migrations/migrationSQLiteQuery.js +145 -1
  8. package/Migrations/schema.js +131 -28
  9. package/QueryLanguage/queryMethods.js +193 -15
  10. package/QueryLanguage/queryParameters.js +136 -0
  11. package/QueryLanguage/queryScript.js +13 -4
  12. package/SQLLiteEngine.js +331 -20
  13. package/context.js +91 -14
  14. package/docs/INCLUDES_CLARIFICATION.md +202 -0
  15. package/docs/METHODS_REFERENCE.md +184 -0
  16. package/docs/MIGRATIONS_GUIDE.md +699 -0
  17. package/docs/POSTGRESQL_SETUP.md +415 -0
  18. package/examples/jsonArrayTransformer.js +215 -0
  19. package/mySQLEngine.js +273 -17
  20. package/package.json +3 -3
  21. package/postgresEngine.js +600 -483
  22. package/postgresSyncConnect.js +209 -0
  23. package/readme.md +1046 -416
  24. package/test/anyCommaStringTest.js +237 -0
  25. package/test/anyMethodTest.js +176 -0
  26. package/test/findByIdTest.js +227 -0
  27. package/test/includesFeatureTest.js +183 -0
  28. package/test/includesTransformTest.js +110 -0
  29. package/test/newMethodTest.js +330 -0
  30. package/test/newMethodUnitTest.js +320 -0
  31. package/test/parameterizedPlaceholderTest.js +159 -0
  32. package/test/postgresEngineTest.js +463 -0
  33. package/test/postgresIntegrationTest.js +381 -0
  34. package/test/securityTest.js +268 -0
  35. package/test/singleDollarPlaceholderTest.js +238 -0
  36. package/test/transformerTest.js +287 -0
  37. package/test/verifyFindById.js +169 -0
  38. package/test/verifyNewMethod.js +191 -0
@@ -9,7 +9,26 @@
9
9
  "Bash(npm whoami:*)",
10
10
  "Bash(npm pkg fix:*)",
11
11
  "Bash(~/.npmrc)",
12
- "Bash(cat:*)"
12
+ "Bash(cat:*)",
13
+ "Bash(node test/includesTransformTest.js:*)",
14
+ "Bash(node test/securityTest.js:*)",
15
+ "Bash(node test/transformerTest.js:*)",
16
+ "Bash(node:*)",
17
+ "Bash(npm publish)",
18
+ "Bash(npm owner:*)",
19
+ "Bash(npm profile get:*)",
20
+ "Bash(npm cache clean:*)",
21
+ "Bash(npm config:*)",
22
+ "Bash(npm publish:*)",
23
+ "Bash(npm logout)",
24
+ "Bash(npm access:*)",
25
+ "Bash(npm view:*)",
26
+ "Bash(git add:*)",
27
+ "Bash(git commit:*)",
28
+ "Bash(git push)",
29
+ "Bash(find:*)",
30
+ "Bash(npm install)",
31
+ "Bash(ls:*)"
13
32
  ],
14
33
  "deny": [],
15
34
  "ask": []
@@ -137,6 +137,12 @@ class EntityModel {
137
137
  return this;
138
138
  }
139
139
 
140
+ // allows you to add custom field transformers for serialization/deserialization
141
+ transform(transformObj){
142
+ this.obj.transform = transformObj;
143
+ return this;
144
+ }
145
+
140
146
  // allows you to add a virtual object that will skipped from being used as sql objects
141
147
  virtual(){
142
148
  this.obj.virtual = true;
@@ -1,6 +1,8 @@
1
1
 
2
2
  // version : 0.0.9
3
3
  var tools = require('../Tools');
4
+ var FieldTransformer = require('./fieldTransformer');
5
+
4
6
  class EntityTrackerModel {
5
7
 
6
8
 
@@ -18,12 +20,27 @@ class EntityTrackerModel {
18
20
  this.buildRelationshipModels(modelClass, currentEntity, dataModel);
19
21
 
20
22
  // loop through data model fields
21
- for (const [modelField, modelFieldValue] of modelFields) {
22
-
23
+ for (const [modelField, modelFieldValue] of modelFields) {
24
+
23
25
  // set the value dynamiclly
24
26
  if(!$that._isRelationship(currentEntity[modelField])){
27
+ // 🔥 Apply fromDatabase transformer when building entity from DB row
28
+ let transformedValue = modelFieldValue;
29
+ try {
30
+ transformedValue = FieldTransformer.fromDatabase(
31
+ modelFieldValue,
32
+ currentEntity[modelField],
33
+ currentEntity.__name,
34
+ modelField
35
+ );
36
+ } catch(transformError) {
37
+ console.error(`Warning: Failed to transform ${currentEntity.__name}.${modelField} from database: ${transformError.message}`);
38
+ // Use original value if transformation fails (non-fatal)
39
+ transformedValue = modelFieldValue;
40
+ }
41
+
25
42
  // current entity has a value then add
26
- modelClass["__proto__"]["_" + modelField] = modelFieldValue;
43
+ modelClass["__proto__"]["_" + modelField] = transformedValue;
27
44
 
28
45
  Object.defineProperty(modelClass,modelField, {
29
46
  set: function(value) {
@@ -0,0 +1,266 @@
1
+ /**
2
+ * FieldTransformer - Production-grade field transformation system
3
+ *
4
+ * Allows entity fields to define custom serialization/deserialization logic
5
+ * for transforming values between JavaScript types and database storage formats.
6
+ *
7
+ * @example
8
+ * class User {
9
+ * constructor() {
10
+ * this.certified_models = {
11
+ * type: "string",
12
+ * transform: {
13
+ * toDatabase: (value) => Array.isArray(value) ? JSON.stringify(value) : value,
14
+ * fromDatabase: (value) => {
15
+ * try { return JSON.parse(value); }
16
+ * catch { return []; }
17
+ * }
18
+ * }
19
+ * };
20
+ * }
21
+ * }
22
+ *
23
+ * @author MasterRecord Team
24
+ * @version 1.0.0
25
+ */
26
+
27
+ class FieldTransformer {
28
+
29
+ /**
30
+ * Check if a field definition has a transformer
31
+ * @param {Object} fieldDef - Field definition from entity
32
+ * @returns {boolean}
33
+ */
34
+ static hasTransformer(fieldDef) {
35
+ return fieldDef
36
+ && typeof fieldDef === 'object'
37
+ && fieldDef.transform
38
+ && typeof fieldDef.transform === 'object'
39
+ && (typeof fieldDef.transform.toDatabase === 'function'
40
+ || typeof fieldDef.transform.fromDatabase === 'function');
41
+ }
42
+
43
+ /**
44
+ * Validate transformer definition structure
45
+ * @param {Object} transformer - The transform object
46
+ * @param {string} entityName - Entity name for error messages
47
+ * @param {string} fieldName - Field name for error messages
48
+ * @throws {Error} If transformer is invalid
49
+ */
50
+ static validateTransformer(transformer, entityName, fieldName) {
51
+ if (!transformer || typeof transformer !== 'object') {
52
+ throw new Error(
53
+ `Invalid transformer for ${entityName}.${fieldName}: ` +
54
+ `transform must be an object with toDatabase and/or fromDatabase functions`
55
+ );
56
+ }
57
+
58
+ const { toDatabase, fromDatabase } = transformer;
59
+
60
+ // At least one direction must be provided
61
+ if (!toDatabase && !fromDatabase) {
62
+ throw new Error(
63
+ `Invalid transformer for ${entityName}.${fieldName}: ` +
64
+ `must provide at least one of: toDatabase, fromDatabase`
65
+ );
66
+ }
67
+
68
+ // Validate toDatabase if present
69
+ if (toDatabase !== undefined && typeof toDatabase !== 'function') {
70
+ throw new Error(
71
+ `Invalid transformer for ${entityName}.${fieldName}: ` +
72
+ `toDatabase must be a function, got ${typeof toDatabase}`
73
+ );
74
+ }
75
+
76
+ // Validate fromDatabase if present
77
+ if (fromDatabase !== undefined && typeof fromDatabase !== 'function') {
78
+ throw new Error(
79
+ `Invalid transformer for ${entityName}.${fieldName}: ` +
80
+ `fromDatabase must be a function, got ${typeof fromDatabase}`
81
+ );
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Transform a value for database storage
87
+ * Executes the toDatabase transformer if defined
88
+ *
89
+ * @param {*} value - The value to transform
90
+ * @param {Object} fieldDef - Field definition with transformer
91
+ * @param {string} entityName - Entity name for error messages
92
+ * @param {string} fieldName - Field name for error messages
93
+ * @returns {*} Transformed value
94
+ * @throws {Error} If transformation fails
95
+ */
96
+ static toDatabase(value, fieldDef, entityName, fieldName) {
97
+ // No transformer - return original value
98
+ if (!this.hasTransformer(fieldDef)) {
99
+ return value;
100
+ }
101
+
102
+ const transformer = fieldDef.transform;
103
+
104
+ // No toDatabase function - return original value
105
+ if (!transformer.toDatabase) {
106
+ return value;
107
+ }
108
+
109
+ // Execute transformation with comprehensive error handling
110
+ try {
111
+ const transformed = transformer.toDatabase(value);
112
+
113
+ // Validate transformation returned a value
114
+ if (transformed === undefined) {
115
+ throw new Error(
116
+ `Transformer for ${entityName}.${fieldName} returned undefined. ` +
117
+ `Transform functions must return a value.`
118
+ );
119
+ }
120
+
121
+ return transformed;
122
+ } catch (err) {
123
+ // Re-throw with context if it's already our error
124
+ if (err.message.includes(entityName)) {
125
+ throw err;
126
+ }
127
+
128
+ // Wrap external errors with context
129
+ throw new Error(
130
+ `Transform error for ${entityName}.${fieldName}: ${err.message}\n` +
131
+ `Original value: ${JSON.stringify(value)}\n` +
132
+ `Stack: ${err.stack}`
133
+ );
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Transform a value from database storage to application format
139
+ * Executes the fromDatabase transformer if defined
140
+ *
141
+ * @param {*} value - The value from database
142
+ * @param {Object} fieldDef - Field definition with transformer
143
+ * @param {string} entityName - Entity name for error messages
144
+ * @param {string} fieldName - Field name for error messages
145
+ * @returns {*} Transformed value
146
+ * @throws {Error} If transformation fails
147
+ */
148
+ static fromDatabase(value, fieldDef, entityName, fieldName) {
149
+ // No transformer - return original value
150
+ if (!this.hasTransformer(fieldDef)) {
151
+ return value;
152
+ }
153
+
154
+ const transformer = fieldDef.transform;
155
+
156
+ // No fromDatabase function - return original value
157
+ if (!transformer.fromDatabase) {
158
+ return value;
159
+ }
160
+
161
+ // Execute transformation with comprehensive error handling
162
+ try {
163
+ const transformed = transformer.fromDatabase(value);
164
+
165
+ // Allow undefined return for optional transformations
166
+ // (e.g., parsing may return undefined for null/empty strings)
167
+ return transformed;
168
+ } catch (err) {
169
+ // Re-throw with context if it's already our error
170
+ if (err.message.includes(entityName)) {
171
+ throw err;
172
+ }
173
+
174
+ // Wrap external errors with context
175
+ throw new Error(
176
+ `Transform error for ${entityName}.${fieldName}: ${err.message}\n` +
177
+ `Original value: ${JSON.stringify(value)}\n` +
178
+ `Stack: ${err.stack}`
179
+ );
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Apply toDatabase transformers to all fields in an entity
185
+ * Used during INSERT/UPDATE operations
186
+ *
187
+ * @param {Object} entityData - The entity data to transform
188
+ * @param {Object} entityDef - Entity definition with field definitions
189
+ * @returns {Object} New object with transformed values
190
+ */
191
+ static applyToDatabaseTransforms(entityData, entityDef) {
192
+ const transformed = {};
193
+ const entityName = entityDef.__name || 'Entity';
194
+
195
+ for (const fieldName in entityData) {
196
+ // Skip internal fields
197
+ if (fieldName.startsWith('__')) {
198
+ continue;
199
+ }
200
+
201
+ const value = entityData[fieldName];
202
+ const fieldDef = entityDef[fieldName];
203
+
204
+ // If no field definition, pass through
205
+ if (!fieldDef) {
206
+ transformed[fieldName] = value;
207
+ continue;
208
+ }
209
+
210
+ // Apply transformer if present
211
+ try {
212
+ transformed[fieldName] = this.toDatabase(value, fieldDef, entityName, fieldName);
213
+ } catch (err) {
214
+ // Add operation context
215
+ throw new Error(
216
+ `Failed to transform field for database write: ${err.message}`
217
+ );
218
+ }
219
+ }
220
+
221
+ return transformed;
222
+ }
223
+
224
+ /**
225
+ * Apply fromDatabase transformers to all fields in an entity
226
+ * Used during SELECT operations when building entities from database rows
227
+ *
228
+ * @param {Object} dbRow - Raw database row
229
+ * @param {Object} entityDef - Entity definition with field definitions
230
+ * @returns {Object} New object with transformed values
231
+ */
232
+ static applyFromDatabaseTransforms(dbRow, entityDef) {
233
+ const transformed = {};
234
+ const entityName = entityDef.__name || 'Entity';
235
+
236
+ for (const fieldName in dbRow) {
237
+ // Skip internal fields
238
+ if (fieldName.startsWith('__')) {
239
+ continue;
240
+ }
241
+
242
+ const value = dbRow[fieldName];
243
+ const fieldDef = entityDef[fieldName];
244
+
245
+ // If no field definition, pass through
246
+ if (!fieldDef) {
247
+ transformed[fieldName] = value;
248
+ continue;
249
+ }
250
+
251
+ // Apply transformer if present
252
+ try {
253
+ transformed[fieldName] = this.fromDatabase(value, fieldDef, entityName, fieldName);
254
+ } catch (err) {
255
+ // Add operation context
256
+ throw new Error(
257
+ `Failed to transform field from database: ${err.message}`
258
+ );
259
+ }
260
+ }
261
+
262
+ return transformed;
263
+ }
264
+ }
265
+
266
+ module.exports = FieldTransformer;
@@ -224,7 +224,151 @@ class migrationMySQLQuery {
224
224
  return `ALTER TABLE \`${table.tableName}\` RENAME COLUMN \`${table.name}\` TO \`${table.newName}\``
225
225
  }
226
226
 
227
-
227
+ /**
228
+ * SEED DATA METHODS
229
+ * Support for inserting seed data during migrations
230
+ */
231
+
232
+ /**
233
+ * Insert seed data into a table
234
+ * @param {string} tableName - Name of the table
235
+ * @param {Object} data - Data object with column names as keys
236
+ * @returns {string} INSERT query
237
+ */
238
+ insertSeedData(tableName, data){
239
+ const columns = Object.keys(data).filter(k => !k.startsWith('__'));
240
+ const values = columns.map(col => {
241
+ const val = data[col];
242
+ if(val === null || val === undefined){
243
+ return 'NULL';
244
+ }
245
+ if(typeof val === 'boolean'){
246
+ return val ? '1' : '0'; // MySQL TINYINT for boolean
247
+ }
248
+ if(typeof val === 'number'){
249
+ return val;
250
+ }
251
+ // Escape strings
252
+ const escaped = String(val).replace(/'/g, "''");
253
+ return `'${escaped}'`;
254
+ });
255
+
256
+ const columnList = columns.map(c => `\`${c}\``).join(', ');
257
+ const valueList = values.join(', ');
258
+
259
+ return `INSERT INTO \`${tableName}\` (${columnList}) VALUES (${valueList})`;
260
+ }
261
+
262
+ /**
263
+ * Insert multiple seed records at once
264
+ * @param {string} tableName - Name of the table
265
+ * @param {Array} dataArray - Array of data objects
266
+ * @returns {string} Bulk INSERT query
267
+ */
268
+ bulkInsertSeedData(tableName, dataArray){
269
+ if(!dataArray || dataArray.length === 0){
270
+ return '';
271
+ }
272
+
273
+ const firstRow = dataArray[0];
274
+ const columns = Object.keys(firstRow).filter(k => !k.startsWith('__'));
275
+ const columnList = columns.map(c => `\`${c}\``).join(', ');
276
+
277
+ const valueRows = dataArray.map(data => {
278
+ const values = columns.map(col => {
279
+ const val = data[col];
280
+ if(val === null || val === undefined){
281
+ return 'NULL';
282
+ }
283
+ if(typeof val === 'boolean'){
284
+ return val ? '1' : '0';
285
+ }
286
+ if(typeof val === 'number'){
287
+ return val;
288
+ }
289
+ const escaped = String(val).replace(/'/g, "''");
290
+ return `'${escaped}'`;
291
+ });
292
+ return `(${values.join(', ')})`;
293
+ });
294
+
295
+ return `INSERT INTO \`${tableName}\` (${columnList}) VALUES ${valueRows.join(', ')}`;
296
+ }
297
+
298
+ /**
299
+ * Update seed data (useful for down migrations)
300
+ * @param {string} tableName - Name of the table
301
+ * @param {Object} data - Data to update
302
+ * @param {Object} where - WHERE conditions
303
+ * @returns {string} UPDATE query
304
+ */
305
+ updateSeedData(tableName, data, where){
306
+ const setClause = Object.keys(data)
307
+ .filter(k => !k.startsWith('__'))
308
+ .map(col => {
309
+ const val = data[col];
310
+ if(val === null || val === undefined){
311
+ return `\`${col}\` = NULL`;
312
+ }
313
+ if(typeof val === 'boolean'){
314
+ return `\`${col}\` = ${val ? '1' : '0'}`;
315
+ }
316
+ if(typeof val === 'number'){
317
+ return `\`${col}\` = ${val}`;
318
+ }
319
+ const escaped = String(val).replace(/'/g, "''");
320
+ return `\`${col}\` = '${escaped}'`;
321
+ })
322
+ .join(', ');
323
+
324
+ const whereClause = Object.keys(where)
325
+ .map(col => {
326
+ const val = where[col];
327
+ if(val === null || val === undefined){
328
+ return `\`${col}\` IS NULL`;
329
+ }
330
+ if(typeof val === 'boolean'){
331
+ return `\`${col}\` = ${val ? '1' : '0'}`;
332
+ }
333
+ if(typeof val === 'number'){
334
+ return `\`${col}\` = ${val}`;
335
+ }
336
+ const escaped = String(val).replace(/'/g, "''");
337
+ return `\`${col}\` = '${escaped}'`;
338
+ })
339
+ .join(' AND ');
340
+
341
+ return `UPDATE \`${tableName}\` SET ${setClause} WHERE ${whereClause}`;
342
+ }
343
+
344
+ /**
345
+ * Delete seed data (useful for down migrations)
346
+ * @param {string} tableName - Name of the table
347
+ * @param {Object} where - WHERE conditions
348
+ * @returns {string} DELETE query
349
+ */
350
+ deleteSeedData(tableName, where){
351
+ const whereClause = Object.keys(where)
352
+ .map(col => {
353
+ const val = where[col];
354
+ if(val === null || val === undefined){
355
+ return `\`${col}\` IS NULL`;
356
+ }
357
+ if(typeof val === 'boolean'){
358
+ return `\`${col}\` = ${val ? '1' : '0'}`;
359
+ }
360
+ if(typeof val === 'number'){
361
+ return `\`${col}\` = ${val}`;
362
+ }
363
+ const escaped = String(val).replace(/'/g, "''");
364
+ return `\`${col}\` = '${escaped}'`;
365
+ })
366
+ .join(' AND ');
367
+
368
+ return `DELETE FROM \`${tableName}\` WHERE ${whereClause}`;
369
+ }
370
+
371
+
228
372
  }
229
373
 
230
374