masterrecord 0.3.30 → 0.3.31
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/migrationMySQLQuery.js +10 -0
- package/Migrations/migrationPostgresQuery.js +10 -0
- package/Migrations/migrationSQLiteQuery.js +10 -0
- package/Migrations/migrationTemplate.js +32 -0
- package/Migrations/migrations.js +81 -4
- package/Migrations/schema.js +63 -0
- package/context.js +83 -0
- package/package.json +1 -1
- package/readme.md +238 -37
|
@@ -46,17 +46,60 @@ class EntityModelBuilder {
|
|
|
46
46
|
MDB.obj.name = methodName;
|
|
47
47
|
obj[methodName] = MDB.obj;
|
|
48
48
|
}
|
|
49
|
+
|
|
50
|
+
// Extract composite indexes from static property (Option A)
|
|
51
|
+
if (model.compositeIndexes) {
|
|
52
|
+
obj.__compositeIndexes = this.#normalizeCompositeIndexes(
|
|
53
|
+
model.compositeIndexes,
|
|
54
|
+
model.name
|
|
55
|
+
);
|
|
56
|
+
} else {
|
|
57
|
+
obj.__compositeIndexes = []; // Initialize empty array
|
|
58
|
+
}
|
|
59
|
+
|
|
49
60
|
return obj;
|
|
50
61
|
}
|
|
51
62
|
|
|
52
63
|
static cleanNull(obj) {
|
|
53
|
-
for (var propName in obj) {
|
|
64
|
+
for (var propName in obj) {
|
|
54
65
|
if (obj[propName] === null) {
|
|
55
66
|
delete obj[propName];
|
|
56
67
|
}
|
|
57
68
|
}
|
|
58
69
|
}
|
|
59
70
|
|
|
71
|
+
static #normalizeCompositeIndexes(indexes, tableName) {
|
|
72
|
+
if (!Array.isArray(indexes)) {
|
|
73
|
+
throw new Error(`compositeIndexes must be an array`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return indexes.map((index, i) => {
|
|
77
|
+
// Simple array: ['col1', 'col2'] -> auto-generate name
|
|
78
|
+
if (Array.isArray(index)) {
|
|
79
|
+
const colNames = index.join('_');
|
|
80
|
+
return {
|
|
81
|
+
columns: index,
|
|
82
|
+
name: `idx_${tableName.toLowerCase()}_${colNames}`,
|
|
83
|
+
unique: false
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Object: { columns: [...], name?, unique? }
|
|
88
|
+
if (!index.columns || !Array.isArray(index.columns)) {
|
|
89
|
+
throw new Error(`Composite index must have 'columns' array`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const name = index.name ||
|
|
93
|
+
`idx_${tableName.toLowerCase()}_${index.columns.join('_')}`;
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
columns: index.columns,
|
|
97
|
+
name: name,
|
|
98
|
+
unique: index.unique || false
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
60
103
|
}
|
|
61
104
|
|
|
62
105
|
module.exports = EntityModelBuilder;
|
|
@@ -246,6 +246,16 @@ class migrationMySQLQuery {
|
|
|
246
246
|
return `DROP INDEX \`${indexName}\` ON \`${indexInfo.tableName}\``;
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
createCompositeIndex(indexInfo){
|
|
250
|
+
const columns = indexInfo.columns.map(c => `\`${c}\``).join(', ');
|
|
251
|
+
const uniqueKeyword = indexInfo.unique ? 'UNIQUE ' : '';
|
|
252
|
+
return `CREATE ${uniqueKeyword}INDEX \`${indexInfo.indexName}\` ON \`${indexInfo.tableName}\`(${columns})`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
dropCompositeIndex(indexInfo){
|
|
256
|
+
return `DROP INDEX \`${indexInfo.indexName}\` ON \`${indexInfo.tableName}\``;
|
|
257
|
+
}
|
|
258
|
+
|
|
249
259
|
/**
|
|
250
260
|
* SEED DATA METHODS
|
|
251
261
|
* Support for inserting seed data during migrations
|
|
@@ -233,6 +233,16 @@ class migrationPostgresQuery {
|
|
|
233
233
|
return `DROP INDEX IF EXISTS "${indexName}"`;
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
createCompositeIndex(indexInfo){
|
|
237
|
+
const columns = indexInfo.columns.map(c => `"${c}"`).join(', ');
|
|
238
|
+
const uniqueKeyword = indexInfo.unique ? 'UNIQUE ' : '';
|
|
239
|
+
return `CREATE ${uniqueKeyword}INDEX IF NOT EXISTS "${indexInfo.indexName}" ON "${indexInfo.tableName}"(${columns})`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
dropCompositeIndex(indexInfo){
|
|
243
|
+
return `DROP INDEX IF EXISTS "${indexInfo.indexName}"`;
|
|
244
|
+
}
|
|
245
|
+
|
|
236
246
|
/**
|
|
237
247
|
* SEED DATA METHODS
|
|
238
248
|
* Support for inserting seed data during migrations
|
|
@@ -201,6 +201,16 @@ class migrationSQLiteQuery {
|
|
|
201
201
|
return `DROP INDEX IF EXISTS ${indexName}`;
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
createCompositeIndex(indexInfo){
|
|
205
|
+
const columns = indexInfo.columns.join(', ');
|
|
206
|
+
const uniqueKeyword = indexInfo.unique ? 'UNIQUE ' : '';
|
|
207
|
+
return `CREATE ${uniqueKeyword}INDEX IF NOT EXISTS ${indexInfo.indexName} ON ${indexInfo.tableName}(${columns})`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
dropCompositeIndex(indexInfo){
|
|
211
|
+
return `DROP INDEX IF EXISTS ${indexInfo.indexName}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
204
214
|
/**
|
|
205
215
|
* SEED DATA METHODS
|
|
206
216
|
* Support for inserting seed data during migrations
|
|
@@ -120,6 +120,38 @@ module.exports = ${this.name};
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
createCompositeIndex(type, indexInfo){
|
|
124
|
+
const indexInfoStr = JSON.stringify({
|
|
125
|
+
tableName: indexInfo.tableName,
|
|
126
|
+
columns: indexInfo.columns,
|
|
127
|
+
indexName: indexInfo.indexName,
|
|
128
|
+
unique: indexInfo.unique
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if(type === "up"){
|
|
132
|
+
this.#up += os.EOL + ` this.createCompositeIndex(${indexInfoStr});`
|
|
133
|
+
}
|
|
134
|
+
else{
|
|
135
|
+
this.#down += os.EOL + ` this.dropCompositeIndex(${indexInfoStr});`
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
dropCompositeIndex(type, indexInfo){
|
|
140
|
+
const indexInfoStr = JSON.stringify({
|
|
141
|
+
tableName: indexInfo.tableName,
|
|
142
|
+
columns: indexInfo.columns,
|
|
143
|
+
indexName: indexInfo.indexName,
|
|
144
|
+
unique: indexInfo.unique
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if(type === "up"){
|
|
148
|
+
this.#up += os.EOL + ` this.dropCompositeIndex(${indexInfoStr});`
|
|
149
|
+
}
|
|
150
|
+
else{
|
|
151
|
+
this.#down += os.EOL + ` this.createCompositeIndex(${indexInfoStr});`
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
123
155
|
}
|
|
124
156
|
|
|
125
157
|
module.exports = MigrationTemplate;
|
package/Migrations/migrations.js
CHANGED
|
@@ -26,7 +26,9 @@ class Migrations{
|
|
|
26
26
|
deletedColumns : [],
|
|
27
27
|
updatedColumns : [],
|
|
28
28
|
newIndexes : [],
|
|
29
|
-
deletedIndexes : []
|
|
29
|
+
deletedIndexes : [],
|
|
30
|
+
newCompositeIndexes : [],
|
|
31
|
+
deletedCompositeIndexes : []
|
|
30
32
|
}
|
|
31
33
|
tables.push(table);
|
|
32
34
|
});
|
|
@@ -42,9 +44,11 @@ class Migrations{
|
|
|
42
44
|
deletedColumns : [],
|
|
43
45
|
updatedColumns : [],
|
|
44
46
|
newIndexes : [],
|
|
45
|
-
deletedIndexes : []
|
|
47
|
+
deletedIndexes : [],
|
|
48
|
+
newCompositeIndexes : [],
|
|
49
|
+
deletedCompositeIndexes : []
|
|
46
50
|
}
|
|
47
|
-
|
|
51
|
+
|
|
48
52
|
oldSchema.forEach(function (oldItem, index) {
|
|
49
53
|
var oldItemName = oldItem["__name"];
|
|
50
54
|
if(table.name === oldItemName){
|
|
@@ -52,7 +56,7 @@ class Migrations{
|
|
|
52
56
|
tables.push(table);
|
|
53
57
|
}
|
|
54
58
|
});
|
|
55
|
-
|
|
59
|
+
|
|
56
60
|
});
|
|
57
61
|
}
|
|
58
62
|
|
|
@@ -167,6 +171,8 @@ class Migrations{
|
|
|
167
171
|
tables = this.#findUpdatedColumns(tables);
|
|
168
172
|
tables = this.#findNewIndexes(tables);
|
|
169
173
|
tables = this.#findDeletedIndexes(tables);
|
|
174
|
+
tables = this.#findNewCompositeIndexes(tables);
|
|
175
|
+
tables = this.#findDeletedCompositeIndexes(tables);
|
|
170
176
|
return tables;
|
|
171
177
|
}
|
|
172
178
|
|
|
@@ -258,6 +264,67 @@ class Migrations{
|
|
|
258
264
|
return tables;
|
|
259
265
|
}
|
|
260
266
|
|
|
267
|
+
#findNewCompositeIndexes(tables) {
|
|
268
|
+
tables.forEach(function (item, index) {
|
|
269
|
+
if (item.new && item.old) {
|
|
270
|
+
const newComposite = item.new.__compositeIndexes || [];
|
|
271
|
+
const oldComposite = item.old.__compositeIndexes || [];
|
|
272
|
+
|
|
273
|
+
newComposite.forEach(function(newIdx) {
|
|
274
|
+
const exists = oldComposite.some(oldIdx =>
|
|
275
|
+
oldIdx.name === newIdx.name
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
if (!exists) {
|
|
279
|
+
item.newCompositeIndexes.push({
|
|
280
|
+
tableName: item.name,
|
|
281
|
+
columns: newIdx.columns,
|
|
282
|
+
indexName: newIdx.name,
|
|
283
|
+
unique: newIdx.unique
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
} else if (item.new && !item.old) {
|
|
288
|
+
// New table - all composite indexes are new
|
|
289
|
+
const composites = item.new.__compositeIndexes || [];
|
|
290
|
+
composites.forEach(function(idx) {
|
|
291
|
+
item.newCompositeIndexes.push({
|
|
292
|
+
tableName: item.name,
|
|
293
|
+
columns: idx.columns,
|
|
294
|
+
indexName: idx.name,
|
|
295
|
+
unique: idx.unique
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
return tables;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
#findDeletedCompositeIndexes(tables) {
|
|
304
|
+
tables.forEach(function (item, index) {
|
|
305
|
+
if (item.new && item.old) {
|
|
306
|
+
const newComposite = item.new.__compositeIndexes || [];
|
|
307
|
+
const oldComposite = item.old.__compositeIndexes || [];
|
|
308
|
+
|
|
309
|
+
oldComposite.forEach(function(oldIdx) {
|
|
310
|
+
const exists = newComposite.some(newIdx =>
|
|
311
|
+
newIdx.name === oldIdx.name
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
if (!exists) {
|
|
315
|
+
item.deletedCompositeIndexes.push({
|
|
316
|
+
tableName: item.name,
|
|
317
|
+
columns: oldIdx.columns,
|
|
318
|
+
indexName: oldIdx.name,
|
|
319
|
+
unique: oldIdx.unique
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
return tables;
|
|
326
|
+
}
|
|
327
|
+
|
|
261
328
|
|
|
262
329
|
|
|
263
330
|
findContextFile(executedLocation, contextFileName){
|
|
@@ -398,6 +465,8 @@ class Migrations{
|
|
|
398
465
|
(t.updatedColumns && t.updatedColumns.length) ||
|
|
399
466
|
(t.newIndexes && t.newIndexes.length) ||
|
|
400
467
|
(t.deletedIndexes && t.deletedIndexes.length) ||
|
|
468
|
+
(t.newCompositeIndexes && t.newCompositeIndexes.length) ||
|
|
469
|
+
(t.deletedCompositeIndexes && t.deletedCompositeIndexes.length) ||
|
|
401
470
|
(t.old === null) || (t.new === null)){
|
|
402
471
|
return true;
|
|
403
472
|
}
|
|
@@ -452,6 +521,14 @@ class Migrations{
|
|
|
452
521
|
MT.dropIndex("up", indexInfo);
|
|
453
522
|
});
|
|
454
523
|
|
|
524
|
+
item.newCompositeIndexes.forEach(function (indexInfo, index) {
|
|
525
|
+
MT.createCompositeIndex("up", indexInfo);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
item.deletedCompositeIndexes.forEach(function (indexInfo, index) {
|
|
529
|
+
MT.dropCompositeIndex("up", indexInfo);
|
|
530
|
+
});
|
|
531
|
+
|
|
455
532
|
});
|
|
456
533
|
|
|
457
534
|
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
|
@@ -980,6 +980,10 @@ class context {
|
|
|
980
980
|
}
|
|
981
981
|
|
|
982
982
|
validModel.__name = tableName;
|
|
983
|
+
|
|
984
|
+
// Merge context-level composite indexes with entity-defined indexes
|
|
985
|
+
this.#mergeCompositeIndexes(validModel, tableName);
|
|
986
|
+
|
|
983
987
|
this.__entities.push(validModel); // Store model object
|
|
984
988
|
const buildMod = tools.createNewInstance(validModel, query, this);
|
|
985
989
|
this.__builderEntities.push(buildMod); // Store query builder entity
|
|
@@ -994,6 +998,85 @@ class context {
|
|
|
994
998
|
});
|
|
995
999
|
}
|
|
996
1000
|
|
|
1001
|
+
/**
|
|
1002
|
+
* Define a composite index on an entity (Option C - Context-level)
|
|
1003
|
+
* @param {Function|string} model - Entity class or table name
|
|
1004
|
+
* @param {Array<string>} columns - Column names to include in index
|
|
1005
|
+
* @param {Object} options - Index options { name?: string, unique?: boolean }
|
|
1006
|
+
*/
|
|
1007
|
+
compositeIndex(model, columns, options = {}) {
|
|
1008
|
+
// Resolve table name
|
|
1009
|
+
let tableName;
|
|
1010
|
+
if (typeof model === 'string') {
|
|
1011
|
+
tableName = model;
|
|
1012
|
+
} else if (typeof model === 'function') {
|
|
1013
|
+
tableName = model.name;
|
|
1014
|
+
} else {
|
|
1015
|
+
throw new Error('compositeIndex: model must be entity class or table name');
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Validate columns
|
|
1019
|
+
if (!Array.isArray(columns) || columns.length < 2) {
|
|
1020
|
+
throw new Error('compositeIndex: columns must be array with at least 2 columns');
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Auto-generate name if not provided
|
|
1024
|
+
const indexName = options.name ||
|
|
1025
|
+
`idx_${tableName.toLowerCase()}_${columns.join('_')}`;
|
|
1026
|
+
|
|
1027
|
+
const indexDef = {
|
|
1028
|
+
columns: columns,
|
|
1029
|
+
name: indexName,
|
|
1030
|
+
unique: options.unique || false
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
// Store in context for later merging with entity-defined indexes
|
|
1034
|
+
if (!this.__contextCompositeIndexes) {
|
|
1035
|
+
this.__contextCompositeIndexes = {};
|
|
1036
|
+
}
|
|
1037
|
+
if (!this.__contextCompositeIndexes[tableName]) {
|
|
1038
|
+
this.__contextCompositeIndexes[tableName] = [];
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// Check for duplicate index names
|
|
1042
|
+
const existing = this.__contextCompositeIndexes[tableName].find(
|
|
1043
|
+
idx => idx.name === indexName
|
|
1044
|
+
);
|
|
1045
|
+
if (existing) {
|
|
1046
|
+
console.warn(`Warning: Composite index '${indexName}' already defined on ${tableName}`);
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
this.__contextCompositeIndexes[tableName].push(indexDef);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Merge context-level and entity-level composite indexes
|
|
1055
|
+
* @private
|
|
1056
|
+
* @param {Object} entityObj - Entity object with __compositeIndexes
|
|
1057
|
+
* @param {string} tableName - Table name
|
|
1058
|
+
*/
|
|
1059
|
+
#mergeCompositeIndexes(entityObj, tableName) {
|
|
1060
|
+
// Start with entity-defined indexes
|
|
1061
|
+
const entityIndexes = entityObj.__compositeIndexes || [];
|
|
1062
|
+
|
|
1063
|
+
// Add context-defined indexes
|
|
1064
|
+
const contextIndexes = (this.__contextCompositeIndexes &&
|
|
1065
|
+
this.__contextCompositeIndexes[tableName]) || [];
|
|
1066
|
+
|
|
1067
|
+
// Merge and deduplicate by name
|
|
1068
|
+
const allIndexes = [...entityIndexes];
|
|
1069
|
+
const existingNames = new Set(entityIndexes.map(idx => idx.name));
|
|
1070
|
+
|
|
1071
|
+
contextIndexes.forEach(idx => {
|
|
1072
|
+
if (!existingNames.has(idx.name)) {
|
|
1073
|
+
allIndexes.push(idx);
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
entityObj.__compositeIndexes = allIndexes;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
997
1080
|
/**
|
|
998
1081
|
* Get current model validation state
|
|
999
1082
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "masterrecord",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.31",
|
|
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": {
|
package/readme.md
CHANGED
|
@@ -1900,6 +1900,226 @@ Migrations automatically include rollback logic. Running `masterrecord migrate d
|
|
|
1900
1900
|
|
|
1901
1901
|
---
|
|
1902
1902
|
|
|
1903
|
+
## Composite Indexes
|
|
1904
|
+
|
|
1905
|
+
Create multi-column indexes for queries that filter or sort on multiple columns together.
|
|
1906
|
+
|
|
1907
|
+
### API - Two Ways to Define
|
|
1908
|
+
|
|
1909
|
+
**Option A: Entity Class (Recommended for core indexes)**
|
|
1910
|
+
|
|
1911
|
+
```javascript
|
|
1912
|
+
class CreditLedger {
|
|
1913
|
+
id(db) {
|
|
1914
|
+
db.integer().primary().auto();
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
organization_id(db) {
|
|
1918
|
+
db.integer().notNullable();
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
created_at(db) {
|
|
1922
|
+
db.timestamp().default('CURRENT_TIMESTAMP');
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
resource_type(db) {
|
|
1926
|
+
db.string().notNullable();
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
resource_id(db) {
|
|
1930
|
+
db.integer().notNullable();
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
// Define composite indexes in entity
|
|
1934
|
+
static compositeIndexes = [
|
|
1935
|
+
// Simple array - auto-generates name
|
|
1936
|
+
['organization_id', 'created_at'],
|
|
1937
|
+
['resource_type', 'resource_id'],
|
|
1938
|
+
|
|
1939
|
+
// With custom name
|
|
1940
|
+
{
|
|
1941
|
+
columns: ['status', 'created_at'],
|
|
1942
|
+
name: 'idx_status_timeline'
|
|
1943
|
+
},
|
|
1944
|
+
|
|
1945
|
+
// Unique composite index
|
|
1946
|
+
{
|
|
1947
|
+
columns: ['email', 'tenant_id'],
|
|
1948
|
+
unique: true
|
|
1949
|
+
}
|
|
1950
|
+
];
|
|
1951
|
+
}
|
|
1952
|
+
```
|
|
1953
|
+
|
|
1954
|
+
**Option C: Context-Level (For environment-specific or centralized schema)**
|
|
1955
|
+
|
|
1956
|
+
```javascript
|
|
1957
|
+
class AppContext extends context {
|
|
1958
|
+
onConfig() {
|
|
1959
|
+
this.dbset(CreditLedger);
|
|
1960
|
+
|
|
1961
|
+
// Define composite indexes in context
|
|
1962
|
+
this.compositeIndex(CreditLedger, ['organization_id', 'created_at']);
|
|
1963
|
+
this.compositeIndex(CreditLedger, ['resource_type', 'resource_id']);
|
|
1964
|
+
this.compositeIndex(CreditLedger, ['status', 'created_at'], {
|
|
1965
|
+
name: 'idx_status_timeline'
|
|
1966
|
+
});
|
|
1967
|
+
this.compositeIndex(CreditLedger, ['email', 'tenant_id'], {
|
|
1968
|
+
unique: true
|
|
1969
|
+
});
|
|
1970
|
+
|
|
1971
|
+
// Can also use table name as string
|
|
1972
|
+
this.compositeIndex('CreditLedger', ['user_id', 'created_at']);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
```
|
|
1976
|
+
|
|
1977
|
+
**Combined Usage (Best of Both)**
|
|
1978
|
+
|
|
1979
|
+
```javascript
|
|
1980
|
+
class User {
|
|
1981
|
+
email(db) { db.string(); }
|
|
1982
|
+
tenant_id(db) { db.integer(); }
|
|
1983
|
+
last_name(db) { db.string(); }
|
|
1984
|
+
first_name(db) { db.string(); }
|
|
1985
|
+
|
|
1986
|
+
// Core indexes in entity
|
|
1987
|
+
static compositeIndexes = [
|
|
1988
|
+
['last_name', 'first_name']
|
|
1989
|
+
];
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
class AppContext extends context {
|
|
1993
|
+
onConfig() {
|
|
1994
|
+
this.dbset(User);
|
|
1995
|
+
|
|
1996
|
+
// Add tenant-specific index for multi-tenant deployments
|
|
1997
|
+
if (process.env.MULTI_TENANT === 'true') {
|
|
1998
|
+
this.compositeIndex(User, ['tenant_id', 'email'], { unique: true });
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
// Add performance index for production
|
|
2002
|
+
if (process.env.NODE_ENV === 'production') {
|
|
2003
|
+
this.compositeIndex(User, ['tenant_id', 'last_name']);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
```
|
|
2008
|
+
|
|
2009
|
+
### When to Use Composite Indexes
|
|
2010
|
+
|
|
2011
|
+
Composite indexes are most effective for queries that:
|
|
2012
|
+
1. **Filter on multiple columns**: `WHERE org_id = ? AND status = ?`
|
|
2013
|
+
2. **Filter and sort**: `WHERE status = ? ORDER BY created_at`
|
|
2014
|
+
3. **Enforce uniqueness**: Unique constraint on multiple columns together
|
|
2015
|
+
|
|
2016
|
+
**Example queries that benefit:**
|
|
2017
|
+
|
|
2018
|
+
```javascript
|
|
2019
|
+
// Benefits from composite index (organization_id, created_at)
|
|
2020
|
+
const ledger = await db.CreditLedger
|
|
2021
|
+
.where(c => c.organization_id == $$, orgId)
|
|
2022
|
+
.orderBy(c => c.created_at)
|
|
2023
|
+
.toList();
|
|
2024
|
+
|
|
2025
|
+
// Benefits from composite index (resource_type, resource_id)
|
|
2026
|
+
const entry = await db.CreditLedger
|
|
2027
|
+
.where(c => c.resource_type == $$ && c.resource_id == $$, 'Order', 123)
|
|
2028
|
+
.single();
|
|
2029
|
+
```
|
|
2030
|
+
|
|
2031
|
+
### Column Order Matters
|
|
2032
|
+
|
|
2033
|
+
The order of columns in a composite index affects query performance:
|
|
2034
|
+
|
|
2035
|
+
```javascript
|
|
2036
|
+
static compositeIndexes = [
|
|
2037
|
+
// Index: (status, created_at)
|
|
2038
|
+
['status', 'created_at']
|
|
2039
|
+
];
|
|
2040
|
+
|
|
2041
|
+
// ✅ FAST: Uses index efficiently
|
|
2042
|
+
// WHERE status = ? ORDER BY created_at
|
|
2043
|
+
await db.Orders
|
|
2044
|
+
.where(o => o.status == $$, 'pending')
|
|
2045
|
+
.orderBy(o => o.created_at)
|
|
2046
|
+
.toList();
|
|
2047
|
+
|
|
2048
|
+
// ⚠️ SLOWER: Can only use first column
|
|
2049
|
+
// WHERE created_at > ?
|
|
2050
|
+
await db.Orders
|
|
2051
|
+
.where(o => o.created_at > $$, yesterday)
|
|
2052
|
+
.toList();
|
|
2053
|
+
```
|
|
2054
|
+
|
|
2055
|
+
**Rule of thumb:** Put the most selective (filtered) columns first, then sort columns.
|
|
2056
|
+
|
|
2057
|
+
### Automatic Migration Generation
|
|
2058
|
+
|
|
2059
|
+
```javascript
|
|
2060
|
+
// Your entity definition triggers migration
|
|
2061
|
+
class CreditLedger {
|
|
2062
|
+
organization_id(db) { db.integer(); }
|
|
2063
|
+
created_at(db) { db.timestamp(); }
|
|
2064
|
+
|
|
2065
|
+
static compositeIndexes = [
|
|
2066
|
+
['organization_id', 'created_at']
|
|
2067
|
+
];
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
// Generated migration (automatic)
|
|
2071
|
+
class Migration_20250101 extends masterrecord.schema {
|
|
2072
|
+
async up(table) {
|
|
2073
|
+
this.init(table);
|
|
2074
|
+
this.createCompositeIndex({
|
|
2075
|
+
tableName: 'CreditLedger',
|
|
2076
|
+
columns: ['organization_id', 'created_at'],
|
|
2077
|
+
indexName: 'idx_creditleger_organization_id_created_at',
|
|
2078
|
+
unique: false
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
async down(table) {
|
|
2083
|
+
this.init(table);
|
|
2084
|
+
this.dropCompositeIndex({
|
|
2085
|
+
tableName: 'CreditLedger',
|
|
2086
|
+
columns: ['organization_id', 'created_at'],
|
|
2087
|
+
indexName: 'idx_creditleger_organization_id_created_at',
|
|
2088
|
+
unique: false
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
```
|
|
2093
|
+
|
|
2094
|
+
### Single vs Composite Indexes
|
|
2095
|
+
|
|
2096
|
+
```javascript
|
|
2097
|
+
class User {
|
|
2098
|
+
email(db) {
|
|
2099
|
+
db.string().index(); // Single-column index
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
first_name(db) {
|
|
2103
|
+
db.string(); // Part of composite below
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
last_name(db) {
|
|
2107
|
+
db.string(); // Part of composite below
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
static compositeIndexes = [
|
|
2111
|
+
// Composite index for name lookups
|
|
2112
|
+
['last_name', 'first_name']
|
|
2113
|
+
];
|
|
2114
|
+
}
|
|
2115
|
+
```
|
|
2116
|
+
|
|
2117
|
+
**When to use single vs composite:**
|
|
2118
|
+
- **Single index**: Column queried independently (`WHERE email = ?`)
|
|
2119
|
+
- **Composite index**: Columns queried together (`WHERE last_name = ? AND first_name = ?`)
|
|
2120
|
+
|
|
2121
|
+
---
|
|
2122
|
+
|
|
1903
2123
|
## Business Logic Validation
|
|
1904
2124
|
|
|
1905
2125
|
Add validators to your entity definitions for automatic validation on property assignment.
|
|
@@ -2411,59 +2631,40 @@ await db.saveChanges(); // Batch insert
|
|
|
2411
2631
|
|
|
2412
2632
|
### 3. Use Indexes
|
|
2413
2633
|
|
|
2414
|
-
|
|
2634
|
+
**Single-column indexes:**
|
|
2415
2635
|
|
|
2416
2636
|
```javascript
|
|
2417
2637
|
class User {
|
|
2418
|
-
id(db) {
|
|
2419
|
-
db.integer().primary().auto(); // Primary keys are automatically indexed
|
|
2420
|
-
}
|
|
2421
|
-
|
|
2422
2638
|
email(db) {
|
|
2423
|
-
db.string()
|
|
2424
|
-
.notNullable()
|
|
2425
|
-
.unique()
|
|
2426
|
-
.index(); // Creates: idx_user_email
|
|
2427
|
-
}
|
|
2428
|
-
|
|
2429
|
-
last_name(db) {
|
|
2430
|
-
db.string().index(); // Creates: idx_user_last_name
|
|
2431
|
-
}
|
|
2432
|
-
|
|
2433
|
-
status(db) {
|
|
2434
|
-
db.string().index('idx_user_status'); // Custom index name
|
|
2639
|
+
db.string().index(); // Single column
|
|
2435
2640
|
}
|
|
2436
2641
|
}
|
|
2437
2642
|
```
|
|
2438
2643
|
|
|
2439
|
-
**
|
|
2644
|
+
**Composite indexes for multi-column queries:**
|
|
2440
2645
|
|
|
2441
2646
|
```javascript
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
indexName: 'idx_user_email'
|
|
2447
|
-
});
|
|
2448
|
-
```
|
|
2647
|
+
class Order {
|
|
2648
|
+
user_id(db) { db.integer(); }
|
|
2649
|
+
status(db) { db.string(); }
|
|
2650
|
+
created_at(db) { db.timestamp(); }
|
|
2449
2651
|
|
|
2450
|
-
|
|
2652
|
+
static compositeIndexes = [
|
|
2653
|
+
// For: WHERE user_id = ? AND status = ?
|
|
2654
|
+
['user_id', 'status'],
|
|
2451
2655
|
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
columnName: 'email',
|
|
2457
|
-
indexName: 'idx_user_email'
|
|
2458
|
-
});
|
|
2656
|
+
// For: WHERE status = ? ORDER BY created_at
|
|
2657
|
+
['status', 'created_at']
|
|
2658
|
+
];
|
|
2659
|
+
}
|
|
2459
2660
|
```
|
|
2460
2661
|
|
|
2461
2662
|
**Best practices:**
|
|
2462
|
-
- Index
|
|
2463
|
-
-
|
|
2663
|
+
- Index foreign keys for join performance
|
|
2664
|
+
- Use composite indexes for queries with multiple WHERE conditions
|
|
2665
|
+
- Column order matters: most selective (filtered) columns first
|
|
2464
2666
|
- Don't over-index - each index adds write overhead
|
|
2465
|
-
- Primary keys are automatically indexed
|
|
2466
|
-
- Use `.unique()` for data integrity, `.index()` for query performance
|
|
2667
|
+
- Primary keys are automatically indexed
|
|
2467
2668
|
|
|
2468
2669
|
### 4. Limit Result Sets
|
|
2469
2670
|
|