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.
- package/Entity/entityModelBuilder.js +44 -1
- package/Migrations/cli.js +17 -4
- package/Migrations/dependencyGraph.js +108 -0
- package/Migrations/migrationMySQLQuery.js +10 -0
- package/Migrations/migrationPostgresQuery.js +10 -0
- package/Migrations/migrationSQLiteQuery.js +10 -0
- package/Migrations/migrationTemplate.js +176 -0
- package/Migrations/migrations.js +121 -12
- package/Migrations/schema.js +63 -0
- package/context.js +288 -0
- package/package.json +1 -1
- package/readme.md +812 -37
- package/test/seed-data-test.js +212 -0
- package/test/seed-features-integration-test.js +418 -0
- package/test/seed-migration-template-test.js +220 -0
package/Migrations/migrations.js
CHANGED
|
@@ -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
|
-
|
|
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();
|
package/Migrations/schema.js
CHANGED
|
@@ -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.
|
|
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": {
|