masterrecord 0.3.30 → 0.3.32

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.
@@ -26,7 +26,10 @@ class Migrations{
26
26
  deletedColumns : [],
27
27
  updatedColumns : [],
28
28
  newIndexes : [],
29
- deletedIndexes : []
29
+ deletedIndexes : [],
30
+ newCompositeIndexes : [],
31
+ deletedCompositeIndexes : [],
32
+ newSeedData : []
30
33
  }
31
34
  tables.push(table);
32
35
  });
@@ -42,9 +45,12 @@ class Migrations{
42
45
  deletedColumns : [],
43
46
  updatedColumns : [],
44
47
  newIndexes : [],
45
- deletedIndexes : []
48
+ deletedIndexes : [],
49
+ newCompositeIndexes : [],
50
+ deletedCompositeIndexes : [],
51
+ newSeedData : []
46
52
  }
47
-
53
+
48
54
  oldSchema.forEach(function (oldItem, index) {
49
55
  var oldItemName = oldItem["__name"];
50
56
  if(table.name === oldItemName){
@@ -52,7 +58,7 @@ class Migrations{
52
58
  tables.push(table);
53
59
  }
54
60
  });
55
-
61
+
56
62
  });
57
63
  }
58
64
 
@@ -157,7 +163,7 @@ class Migrations{
157
163
  }
158
164
 
159
165
  // build table to build new migration snapshot
160
- #buildMigrationObject(oldSchema, newSchema){
166
+ #buildMigrationObject(oldSchema, newSchema, newSeedData = {}){
161
167
 
162
168
  var tables = this.#organizeSchemaByTables(oldSchema, newSchema);
163
169
 
@@ -167,6 +173,9 @@ class Migrations{
167
173
  tables = this.#findUpdatedColumns(tables);
168
174
  tables = this.#findNewIndexes(tables);
169
175
  tables = this.#findDeletedIndexes(tables);
176
+ tables = this.#findNewCompositeIndexes(tables);
177
+ tables = this.#findDeletedCompositeIndexes(tables);
178
+ tables = this.#findNewSeedData(tables, newSeedData);
170
179
  return tables;
171
180
  }
172
181
 
@@ -258,6 +267,78 @@ class Migrations{
258
267
  return tables;
259
268
  }
260
269
 
270
+ #findNewCompositeIndexes(tables) {
271
+ tables.forEach(function (item, index) {
272
+ if (item.new && item.old) {
273
+ const newComposite = item.new.__compositeIndexes || [];
274
+ const oldComposite = item.old.__compositeIndexes || [];
275
+
276
+ newComposite.forEach(function(newIdx) {
277
+ const exists = oldComposite.some(oldIdx =>
278
+ oldIdx.name === newIdx.name
279
+ );
280
+
281
+ if (!exists) {
282
+ item.newCompositeIndexes.push({
283
+ tableName: item.name,
284
+ columns: newIdx.columns,
285
+ indexName: newIdx.name,
286
+ unique: newIdx.unique
287
+ });
288
+ }
289
+ });
290
+ } else if (item.new && !item.old) {
291
+ // New table - all composite indexes are new
292
+ const composites = item.new.__compositeIndexes || [];
293
+ composites.forEach(function(idx) {
294
+ item.newCompositeIndexes.push({
295
+ tableName: item.name,
296
+ columns: idx.columns,
297
+ indexName: idx.name,
298
+ unique: idx.unique
299
+ });
300
+ });
301
+ }
302
+ });
303
+ return tables;
304
+ }
305
+
306
+ #findDeletedCompositeIndexes(tables) {
307
+ tables.forEach(function (item, index) {
308
+ if (item.new && item.old) {
309
+ const newComposite = item.new.__compositeIndexes || [];
310
+ const oldComposite = item.old.__compositeIndexes || [];
311
+
312
+ oldComposite.forEach(function(oldIdx) {
313
+ const exists = newComposite.some(newIdx =>
314
+ newIdx.name === oldIdx.name
315
+ );
316
+
317
+ if (!exists) {
318
+ item.deletedCompositeIndexes.push({
319
+ tableName: item.name,
320
+ columns: oldIdx.columns,
321
+ indexName: oldIdx.name,
322
+ unique: oldIdx.unique
323
+ });
324
+ }
325
+ });
326
+ }
327
+ });
328
+ return tables;
329
+ }
330
+
331
+ #findNewSeedData(tables, newSeedData) {
332
+ // newSeedData is from schema snapshot: { tableName: [records] }
333
+ tables.forEach(function(item) {
334
+ const tableSeedData = newSeedData[item.name];
335
+ if (tableSeedData && tableSeedData.length > 0) {
336
+ item.newSeedData = tableSeedData;
337
+ }
338
+ });
339
+ return tables;
340
+ }
341
+
261
342
 
262
343
 
263
344
  findContextFile(executedLocation, contextFileName){
@@ -330,11 +411,18 @@ class Migrations{
330
411
  const relMigrationFolder = '.'; // the snapshot sits inside migrationsDirectory
331
412
  const relSnapshotLocation = path.basename(snapshotPath);
332
413
 
414
+ // Order seed data by dependencies if context instance is available
415
+ const orderedSeedData = snap.context && snap.context.getOrderedSeedData
416
+ ? snap.context.getOrderedSeedData()
417
+ : snap.contextSeedData || {};
418
+
333
419
  const content = {
334
420
  contextLocation: relContextLocation,
335
421
  migrationFolder: relMigrationFolder,
336
422
  snapShotLocation: relSnapshotLocation,
337
- schema : snap.contextEntities
423
+ schema : snap.contextEntities,
424
+ seedData: orderedSeedData,
425
+ seedConfig: snap.contextSeedConfig || {}
338
426
  };
339
427
 
340
428
  const jsonContent = JSON.stringify(content, null, 2);
@@ -388,8 +476,8 @@ class Migrations{
388
476
  }
389
477
 
390
478
  // Returns true if there are any changes between old and new schema
391
- hasChanges(oldSchema, newSchema){
392
- const tables = this.#buildMigrationObject(oldSchema, newSchema);
479
+ hasChanges(oldSchema, newSchema, newSeedData = {}){
480
+ const tables = this.#buildMigrationObject(oldSchema, newSchema, newSeedData);
393
481
  for(const t of tables){
394
482
  if(!t) continue;
395
483
  if((t.newTables && t.newTables.length) ||
@@ -398,6 +486,9 @@ class Migrations{
398
486
  (t.updatedColumns && t.updatedColumns.length) ||
399
487
  (t.newIndexes && t.newIndexes.length) ||
400
488
  (t.deletedIndexes && t.deletedIndexes.length) ||
489
+ (t.newCompositeIndexes && t.newCompositeIndexes.length) ||
490
+ (t.deletedCompositeIndexes && t.deletedCompositeIndexes.length) ||
491
+ (t.newSeedData && t.newSeedData.length) ||
401
492
  (t.old === null) || (t.new === null)){
402
493
  return true;
403
494
  }
@@ -405,16 +496,20 @@ class Migrations{
405
496
  return false;
406
497
  }
407
498
 
408
- template(name, oldSchema, newSchema){
499
+ template(name, oldSchema, newSchema, newSeedData = {}, seedConfig = {}, currentEnv = null){
409
500
  var MT = new MigrationTemplate(name);
410
- var tables = this.#buildMigrationObject(oldSchema, newSchema);
411
-
501
+ // Determine current environment if not provided
502
+ if (!currentEnv) {
503
+ currentEnv = process.env.NODE_ENV || process.env.master || 'development';
504
+ }
505
+ var tables = this.#buildMigrationObject(oldSchema, newSchema, newSeedData);
506
+
412
507
  tables.forEach(function (item, index) {
413
508
  if(item.old === null){
414
509
  MT.createTable("up", column, item.name);
415
510
  MT.dropTable("down", column, item.name);
416
511
  }
417
-
512
+
418
513
  if(item.new === null){
419
514
  MT.dropTable("up", column, item.name);
420
515
  MT.createTable("down", column, item.name);
@@ -452,6 +547,20 @@ class Migrations{
452
547
  MT.dropIndex("up", indexInfo);
453
548
  });
454
549
 
550
+ item.newCompositeIndexes.forEach(function (indexInfo, index) {
551
+ MT.createCompositeIndex("up", indexInfo);
552
+ });
553
+
554
+ item.deletedCompositeIndexes.forEach(function (indexInfo, index) {
555
+ MT.dropCompositeIndex("up", indexInfo);
556
+ });
557
+
558
+ // Generate seed data code
559
+ if (item.newSeedData && item.newSeedData.length > 0) {
560
+ MT.seedData("up", item.name, item.newSeedData, currentEnv);
561
+ MT.seedDataDown("down", item.name, item.newSeedData, seedConfig);
562
+ }
563
+
455
564
  });
456
565
 
457
566
  return MT.get();
@@ -126,6 +126,19 @@ class schema{
126
126
  });
127
127
  }
128
128
  });
129
+
130
+ // Create composite indexes
131
+ if (table.__compositeIndexes) {
132
+ table.__compositeIndexes.forEach(function(compositeIdx) {
133
+ const indexInfo = {
134
+ tableName: tableName,
135
+ columns: compositeIdx.columns,
136
+ indexName: compositeIdx.name,
137
+ unique: compositeIdx.unique
138
+ };
139
+ self.createCompositeIndex(indexInfo);
140
+ });
141
+ }
129
142
  }
130
143
  }else{
131
144
  console.log("Table that you're trying to create is undefined. Please check if there are any changes that need to be made");
@@ -460,6 +473,56 @@ class schema{
460
473
  }
461
474
  }
462
475
 
476
+ createCompositeIndex(indexInfo){
477
+ if(indexInfo){
478
+ if(this.context.isSQLite){
479
+ var sqliteQuery = require("./migrationSQLiteQuery");
480
+ var queryBuilder = new sqliteQuery();
481
+ var query = queryBuilder.createCompositeIndex(indexInfo);
482
+ this.context._execute(query);
483
+ }
484
+
485
+ if(this.context.isMySQL){
486
+ var sqlquery = require("./migrationMySQLQuery");
487
+ var queryBuilder = new sqlquery();
488
+ var query = queryBuilder.createCompositeIndex(indexInfo);
489
+ this.context._execute(query);
490
+ }
491
+
492
+ if(this.context.isPostgres){
493
+ var postgresQuery = require("./migrationPostgresQuery");
494
+ var queryBuilder = new postgresQuery();
495
+ var query = queryBuilder.createCompositeIndex(indexInfo);
496
+ this.context._execute(query);
497
+ }
498
+ }
499
+ }
500
+
501
+ dropCompositeIndex(indexInfo){
502
+ if(indexInfo){
503
+ if(this.context.isSQLite){
504
+ var sqliteQuery = require("./migrationSQLiteQuery");
505
+ var queryBuilder = new sqliteQuery();
506
+ var query = queryBuilder.dropCompositeIndex(indexInfo);
507
+ this.context._execute(query);
508
+ }
509
+
510
+ if(this.context.isMySQL){
511
+ var sqlquery = require("./migrationMySQLQuery");
512
+ var queryBuilder = new sqlquery();
513
+ var query = queryBuilder.dropCompositeIndex(indexInfo);
514
+ this.context._execute(query);
515
+ }
516
+
517
+ if(this.context.isPostgres){
518
+ var postgresQuery = require("./migrationPostgresQuery");
519
+ var queryBuilder = new postgresQuery();
520
+ var query = queryBuilder.dropCompositeIndex(indexInfo);
521
+ this.context._execute(query);
522
+ }
523
+ }
524
+ }
525
+
463
526
  seed(tableName, rows){
464
527
  if(!tableName || !rows){ return; }
465
528
  const items = Array.isArray(rows) ? rows : [rows];
package/context.js CHANGED
@@ -150,6 +150,16 @@ class context {
150
150
  __trackedEntities = [];
151
151
  __trackedEntitiesMap = new Map(); // Performance: O(1) entity lookup instead of O(n) linear search
152
152
  __relationshipModels = [];
153
+ __contextSeedData = {}; // Store seed data by table name
154
+ __contextSeedConfig = { // Seed data configuration
155
+ generateDownMigrations: false,
156
+ downStrategy: 'delete',
157
+ deleteByPrimaryKey: true,
158
+ onRollbackError: 'warn',
159
+ detectCircularDependencies: true,
160
+ circularStrategy: 'warn',
161
+ defaultStrategy: 'insert' // 'insert' | 'upsert'
162
+ };
153
163
 
154
164
  // Configuration
155
165
  __environment = '';
@@ -711,6 +721,32 @@ class context {
711
721
  }
712
722
  }
713
723
 
724
+ /**
725
+ * Configure seed data behavior for migrations
726
+ *
727
+ * @param {object} config - Seed configuration options
728
+ * @param {boolean} [config.generateDownMigrations=false] - Generate rollback logic for seed data
729
+ * @param {string} [config.downStrategy='delete'] - Strategy for down migrations ('delete' | 'skip')
730
+ * @param {boolean} [config.deleteByPrimaryKey=true] - Use primary key for deletion in down migrations
731
+ * @param {string} [config.onRollbackError='warn'] - How to handle rollback errors ('warn' | 'throw' | 'ignore')
732
+ * @returns {this} Context instance for chaining
733
+ *
734
+ * @example
735
+ * context.seedConfig({
736
+ * generateDownMigrations: true,
737
+ * downStrategy: 'delete'
738
+ * });
739
+ */
740
+ seedConfig(config) {
741
+ if (config && typeof config === 'object') {
742
+ this.__contextSeedConfig = {
743
+ ...this.__contextSeedConfig,
744
+ ...config
745
+ };
746
+ }
747
+ return this;
748
+ }
749
+
714
750
  /**
715
751
  * Initialize SQLite database connection using environment file
716
752
  *
@@ -925,11 +961,13 @@ class context {
925
961
  *
926
962
  * @param {Function|object} model - Entity class or model definition
927
963
  * @param {string} [name] - Optional custom table name (defaults to model.name)
964
+ * @returns {object} Chainable object with seed() method
928
965
  * @throws {EntityValidationError} If model is invalid or table name contains SQL injection
929
966
  *
930
967
  * @example
931
968
  * context.dbset(User);
932
969
  * context.dbset(Post, 'blog_posts');
970
+ * context.dbset(User).seed({ name: 'Admin', email: 'admin@example.com' });
933
971
  */
934
972
  dbset(model, name) {
935
973
  // Input validation
@@ -980,6 +1018,10 @@ class context {
980
1018
  }
981
1019
 
982
1020
  validModel.__name = tableName;
1021
+
1022
+ // Merge context-level composite indexes with entity-defined indexes
1023
+ this.#mergeCompositeIndexes(validModel, tableName);
1024
+
983
1025
  this.__entities.push(validModel); // Store model object
984
1026
  const buildMod = tools.createNewInstance(validModel, query, this);
985
1027
  this.__builderEntities.push(buildMod); // Store query builder entity
@@ -992,6 +1034,252 @@ class context {
992
1034
  configurable: true,
993
1035
  enumerable: true
994
1036
  });
1037
+
1038
+ // Return chainable object with seed() method
1039
+ return {
1040
+ seed: (data) => this.#addSeedData(tableName, data)
1041
+ };
1042
+ }
1043
+
1044
+ /**
1045
+ * Define a composite index on an entity (Option C - Context-level)
1046
+ * @param {Function|string} model - Entity class or table name
1047
+ * @param {Array<string>} columns - Column names to include in index
1048
+ * @param {Object} options - Index options { name?: string, unique?: boolean }
1049
+ */
1050
+ compositeIndex(model, columns, options = {}) {
1051
+ // Resolve table name
1052
+ let tableName;
1053
+ if (typeof model === 'string') {
1054
+ tableName = model;
1055
+ } else if (typeof model === 'function') {
1056
+ tableName = model.name;
1057
+ } else {
1058
+ throw new Error('compositeIndex: model must be entity class or table name');
1059
+ }
1060
+
1061
+ // Validate columns
1062
+ if (!Array.isArray(columns) || columns.length < 2) {
1063
+ throw new Error('compositeIndex: columns must be array with at least 2 columns');
1064
+ }
1065
+
1066
+ // Auto-generate name if not provided
1067
+ const indexName = options.name ||
1068
+ `idx_${tableName.toLowerCase()}_${columns.join('_')}`;
1069
+
1070
+ const indexDef = {
1071
+ columns: columns,
1072
+ name: indexName,
1073
+ unique: options.unique || false
1074
+ };
1075
+
1076
+ // Store in context for later merging with entity-defined indexes
1077
+ if (!this.__contextCompositeIndexes) {
1078
+ this.__contextCompositeIndexes = {};
1079
+ }
1080
+ if (!this.__contextCompositeIndexes[tableName]) {
1081
+ this.__contextCompositeIndexes[tableName] = [];
1082
+ }
1083
+
1084
+ // Check for duplicate index names
1085
+ const existing = this.__contextCompositeIndexes[tableName].find(
1086
+ idx => idx.name === indexName
1087
+ );
1088
+ if (existing) {
1089
+ console.warn(`Warning: Composite index '${indexName}' already defined on ${tableName}`);
1090
+ return;
1091
+ }
1092
+
1093
+ this.__contextCompositeIndexes[tableName].push(indexDef);
1094
+ }
1095
+
1096
+ /**
1097
+ * Merge context-level and entity-level composite indexes
1098
+ * @private
1099
+ * @param {Object} entityObj - Entity object with __compositeIndexes
1100
+ * @param {string} tableName - Table name
1101
+ */
1102
+ #mergeCompositeIndexes(entityObj, tableName) {
1103
+ // Start with entity-defined indexes
1104
+ const entityIndexes = entityObj.__compositeIndexes || [];
1105
+
1106
+ // Add context-defined indexes
1107
+ const contextIndexes = (this.__contextCompositeIndexes &&
1108
+ this.__contextCompositeIndexes[tableName]) || [];
1109
+
1110
+ // Merge and deduplicate by name
1111
+ const allIndexes = [...entityIndexes];
1112
+ const existingNames = new Set(entityIndexes.map(idx => idx.name));
1113
+
1114
+ contextIndexes.forEach(idx => {
1115
+ if (!existingNames.has(idx.name)) {
1116
+ allIndexes.push(idx);
1117
+ }
1118
+ });
1119
+
1120
+ entityObj.__compositeIndexes = allIndexes;
1121
+ }
1122
+
1123
+ /**
1124
+ * Add seed data for a table
1125
+ * @private
1126
+ * @param {string} tableName - Table name
1127
+ * @param {object|Array<object>} data - Seed data (single object or array)
1128
+ * @returns {object} Chainable object with seed() method
1129
+ */
1130
+ #addSeedData(tableName, data) {
1131
+ // Initialize seed data storage if not exists
1132
+ if (!this.__contextSeedData) {
1133
+ this.__contextSeedData = {};
1134
+ }
1135
+ if (!this.__contextSeedData[tableName]) {
1136
+ this.__contextSeedData[tableName] = [];
1137
+ }
1138
+
1139
+ // Handle both single object and array of objects
1140
+ const records = Array.isArray(data) ? data : [data];
1141
+
1142
+ // Attach rollback metadata if down migrations are enabled
1143
+ if (this.__contextSeedConfig.generateDownMigrations) {
1144
+ // Find primary key for this table
1145
+ const entity = this.__entities.find(e => e.__name === tableName);
1146
+ let primaryKey = 'id'; // Default
1147
+ if (entity) {
1148
+ for (const key in entity) {
1149
+ if (entity[key] && entity[key].primary) {
1150
+ primaryKey = key;
1151
+ break;
1152
+ }
1153
+ }
1154
+ }
1155
+
1156
+ records.forEach(record => {
1157
+ if (record[primaryKey] !== undefined) {
1158
+ record.__rollback = {
1159
+ strategy: this.__contextSeedConfig.downStrategy,
1160
+ key: primaryKey,
1161
+ value: record[primaryKey]
1162
+ };
1163
+ }
1164
+ });
1165
+ }
1166
+
1167
+ // Apply default upsert strategy if configured
1168
+ if (this.__contextSeedConfig.defaultStrategy === 'upsert') {
1169
+ records.forEach(record => {
1170
+ if (!record.__seedStrategy) {
1171
+ record.__seedStrategy = {
1172
+ type: 'upsert',
1173
+ conflictKey: 'primaryKey',
1174
+ updateFields: null
1175
+ };
1176
+ }
1177
+ });
1178
+ }
1179
+
1180
+ this.__contextSeedData[tableName].push(...records);
1181
+
1182
+ // Return chainable object with seed(), when(), seedFactory(), and upsert() methods
1183
+ const chainable = {
1184
+ seed: (moreData) => this.#addSeedData(tableName, moreData),
1185
+ seedFactory: (count, generator) => this.#seedFactory(tableName, count, generator),
1186
+ when: (...envs) => {
1187
+ // Mark last batch of records with environment condition
1188
+ const lastBatch = this.__contextSeedData[tableName].slice(-records.length);
1189
+ lastBatch.forEach(r => {
1190
+ r.__seedEnv = {
1191
+ conditions: envs,
1192
+ strategy: 'generation-time'
1193
+ };
1194
+ });
1195
+ return chainable; // Return self for further chaining
1196
+ },
1197
+ upsert: (options = {}) => {
1198
+ // Mark last batch of records with upsert strategy
1199
+ const lastBatch = this.__contextSeedData[tableName].slice(-records.length);
1200
+ lastBatch.forEach(r => {
1201
+ r.__seedStrategy = {
1202
+ type: 'upsert',
1203
+ conflictKey: options.conflictKey || 'primaryKey',
1204
+ updateFields: options.updateFields || null
1205
+ };
1206
+ });
1207
+ return chainable; // Return self for further chaining
1208
+ }
1209
+ };
1210
+
1211
+ return chainable;
1212
+ }
1213
+
1214
+ /**
1215
+ * Add factory-generated seed data for a table
1216
+ * @private
1217
+ * @param {string} tableName - Table name
1218
+ * @param {number} count - Number of records to generate
1219
+ * @param {Function} generator - Function that takes index and returns record object
1220
+ * @returns {object} Chainable object
1221
+ */
1222
+ #seedFactory(tableName, count, generator) {
1223
+ if (typeof generator !== 'function') {
1224
+ throw new Error('seedFactory requires a generator function as the second parameter');
1225
+ }
1226
+
1227
+ if (typeof count !== 'number' || count < 1) {
1228
+ throw new Error('seedFactory requires a positive number as the first parameter');
1229
+ }
1230
+
1231
+ // Generate records using the generator function
1232
+ const records = Array.from({ length: count }, (_, i) => {
1233
+ const record = generator(i);
1234
+ if (!record || typeof record !== 'object') {
1235
+ throw new Error(`Generator function must return an object (returned ${typeof record} for index ${i})`);
1236
+ }
1237
+
1238
+ // Mark as factory-generated
1239
+ record.__seedMeta = {
1240
+ generated: true,
1241
+ index: i,
1242
+ generatedAt: Date.now()
1243
+ };
1244
+
1245
+ return record;
1246
+ });
1247
+
1248
+ // Add generated records using the existing #addSeedData method
1249
+ return this.#addSeedData(tableName, records);
1250
+ }
1251
+
1252
+ /**
1253
+ * Get seed data ordered by dependency relationships
1254
+ * Uses topological sort to ensure foreign key constraints are satisfied
1255
+ * @returns {Object} Ordered seed data by table name
1256
+ */
1257
+ getOrderedSeedData() {
1258
+ if (!this.__contextSeedData || Object.keys(this.__contextSeedData).length === 0) {
1259
+ return {};
1260
+ }
1261
+
1262
+ const DependencyGraph = require('./Migrations/dependencyGraph');
1263
+ const graph = new DependencyGraph(this.__entities);
1264
+ graph.buildFromEntities();
1265
+
1266
+ try {
1267
+ const orderedTables = graph.filterToSeededTables(this.__contextSeedData);
1268
+ const orderedSeedData = {};
1269
+ orderedTables.forEach(table => {
1270
+ orderedSeedData[table] = this.__contextSeedData[table];
1271
+ });
1272
+ return orderedSeedData;
1273
+ } catch (error) {
1274
+ // Handle circular dependency based on strategy
1275
+ if (this.__contextSeedConfig.circularStrategy === 'throw') {
1276
+ throw error;
1277
+ } else if (this.__contextSeedConfig.circularStrategy === 'warn') {
1278
+ console.warn(`[MasterRecord] ${error.message}, using insertion order instead`);
1279
+ }
1280
+ // Fall back to original insertion order
1281
+ return this.__contextSeedData;
1282
+ }
995
1283
  }
996
1284
 
997
1285
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "masterrecord",
3
- "version": "0.3.30",
3
+ "version": "0.3.32",
4
4
  "description": "An Object-relational mapping for the Master framework. Master Record connects classes to relational database tables to establish a database with almost zero-configuration ",
5
5
  "main": "MasterRecord.js",
6
6
  "bin": {