masterrecord 0.3.29 → 0.3.30

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.
@@ -111,8 +111,16 @@ class EntityModel {
111
111
 
112
112
  unique(){
113
113
  this.obj.unique = true; // yes
114
- return this;
114
+ return this;
115
+
116
+ }
115
117
 
118
+ index(indexName){
119
+ if(!this.obj.indexes){
120
+ this.obj.indexes = [];
121
+ }
122
+ this.obj.indexes.push(indexName || true);
123
+ return this;
116
124
  }
117
125
 
118
126
  // this means that it can be an empty field
@@ -232,6 +232,20 @@ class migrationMySQLQuery {
232
232
  return `ALTER TABLE \`${table.tableName}\` RENAME COLUMN \`${table.name}\` TO \`${table.newName}\``
233
233
  }
234
234
 
235
+ createIndex(indexInfo){
236
+ const indexName = indexInfo.indexName === true
237
+ ? `idx_${indexInfo.tableName.toLowerCase()}_${indexInfo.columnName.toLowerCase()}`
238
+ : indexInfo.indexName;
239
+ return `CREATE INDEX \`${indexName}\` ON \`${indexInfo.tableName}\`(\`${indexInfo.columnName}\`)`;
240
+ }
241
+
242
+ dropIndex(indexInfo){
243
+ const indexName = indexInfo.indexName === true
244
+ ? `idx_${indexInfo.tableName.toLowerCase()}_${indexInfo.columnName.toLowerCase()}`
245
+ : indexInfo.indexName;
246
+ return `DROP INDEX \`${indexName}\` ON \`${indexInfo.tableName}\``;
247
+ }
248
+
235
249
  /**
236
250
  * SEED DATA METHODS
237
251
  * Support for inserting seed data during migrations
@@ -219,6 +219,20 @@ class migrationPostgresQuery {
219
219
  return `ALTER TABLE "${table.tableName}" RENAME COLUMN "${table.name}" TO "${table.newName}"`;
220
220
  }
221
221
 
222
+ createIndex(indexInfo){
223
+ const indexName = indexInfo.indexName === true
224
+ ? `idx_${indexInfo.tableName.toLowerCase()}_${indexInfo.columnName.toLowerCase()}`
225
+ : indexInfo.indexName;
226
+ return `CREATE INDEX IF NOT EXISTS "${indexName}" ON "${indexInfo.tableName}"("${indexInfo.columnName}")`;
227
+ }
228
+
229
+ dropIndex(indexInfo){
230
+ const indexName = indexInfo.indexName === true
231
+ ? `idx_${indexInfo.tableName.toLowerCase()}_${indexInfo.columnName.toLowerCase()}`
232
+ : indexInfo.indexName;
233
+ return `DROP INDEX IF EXISTS "${indexName}"`;
234
+ }
235
+
222
236
  /**
223
237
  * SEED DATA METHODS
224
238
  * Support for inserting seed data during migrations
@@ -187,6 +187,20 @@ class migrationSQLiteQuery {
187
187
  return `ALTER TABLE ${table.tableName} RENAME COLUMN ${table.name} TO ${table.newName}`
188
188
  }
189
189
 
190
+ createIndex(indexInfo){
191
+ const indexName = indexInfo.indexName === true
192
+ ? `idx_${indexInfo.tableName.toLowerCase()}_${indexInfo.columnName.toLowerCase()}`
193
+ : indexInfo.indexName;
194
+ return `CREATE INDEX IF NOT EXISTS ${indexName} ON ${indexInfo.tableName}(${indexInfo.columnName})`;
195
+ }
196
+
197
+ dropIndex(indexInfo){
198
+ const indexName = indexInfo.indexName === true
199
+ ? `idx_${indexInfo.tableName.toLowerCase()}_${indexInfo.columnName.toLowerCase()}`
200
+ : indexInfo.indexName;
201
+ return `DROP INDEX IF EXISTS ${indexName}`;
202
+ }
203
+
190
204
  /**
191
205
  * SEED DATA METHODS
192
206
  * Support for inserting seed data during migrations
@@ -82,6 +82,44 @@ module.exports = ${this.name};
82
82
  }
83
83
  }
84
84
 
85
+ createIndex(type, indexInfo){
86
+ const indexName = indexInfo.indexName === true
87
+ ? `idx_${indexInfo.tableName.toLowerCase()}_${indexInfo.columnName.toLowerCase()}`
88
+ : indexInfo.indexName;
89
+
90
+ const indexInfoStr = JSON.stringify({
91
+ tableName: indexInfo.tableName,
92
+ columnName: indexInfo.columnName,
93
+ indexName: indexInfo.indexName
94
+ });
95
+
96
+ if(type === "up"){
97
+ this.#up += os.EOL + ` this.createIndex(${indexInfoStr});`
98
+ }
99
+ else{
100
+ this.#down += os.EOL + ` this.dropIndex(${indexInfoStr});`
101
+ }
102
+ }
103
+
104
+ dropIndex(type, indexInfo){
105
+ const indexName = indexInfo.indexName === true
106
+ ? `idx_${indexInfo.tableName.toLowerCase()}_${indexInfo.columnName.toLowerCase()}`
107
+ : indexInfo.indexName;
108
+
109
+ const indexInfoStr = JSON.stringify({
110
+ tableName: indexInfo.tableName,
111
+ columnName: indexInfo.columnName,
112
+ indexName: indexInfo.indexName
113
+ });
114
+
115
+ if(type === "up"){
116
+ this.#up += os.EOL + ` this.dropIndex(${indexInfoStr});`
117
+ }
118
+ else{
119
+ this.#down += os.EOL + ` this.createIndex(${indexInfoStr});`
120
+ }
121
+ }
122
+
85
123
  }
86
124
 
87
125
  module.exports = MigrationTemplate;
@@ -24,7 +24,9 @@ class Migrations{
24
24
  newColumns : [],
25
25
  newTables : [],
26
26
  deletedColumns : [],
27
- updatedColumns : []
27
+ updatedColumns : [],
28
+ newIndexes : [],
29
+ deletedIndexes : []
28
30
  }
29
31
  tables.push(table);
30
32
  });
@@ -38,7 +40,9 @@ class Migrations{
38
40
  newColumns : [],
39
41
  newTables : [],
40
42
  deletedColumns : [],
41
- updatedColumns : []
43
+ updatedColumns : [],
44
+ newIndexes : [],
45
+ deletedIndexes : []
42
46
  }
43
47
 
44
48
  oldSchema.forEach(function (oldItem, index) {
@@ -156,11 +160,101 @@ class Migrations{
156
160
  #buildMigrationObject(oldSchema, newSchema){
157
161
 
158
162
  var tables = this.#organizeSchemaByTables(oldSchema, newSchema);
159
-
163
+
160
164
  tables = this.#findNewTables(tables);
161
165
  tables = this.#findNewColumns(tables);
162
166
  tables = this.#findDeletedColumns(tables);
163
167
  tables = this.#findUpdatedColumns(tables);
168
+ tables = this.#findNewIndexes(tables);
169
+ tables = this.#findDeletedIndexes(tables);
170
+ return tables;
171
+ }
172
+
173
+ #findNewIndexes(tables){
174
+ tables.forEach(function (item, index) {
175
+ if(item.new && item.old){
176
+ Object.keys(item.new).forEach(function (key) {
177
+ if(typeof item.new[key] === "object" && item.new[key].indexes){
178
+ var columnName = item.new[key].name;
179
+ var newIndexes = item.new[key].indexes;
180
+
181
+ // Check if this column existed before
182
+ var oldColumn = null;
183
+ Object.keys(item.old).forEach(function (oldKey) {
184
+ if(typeof item.old[oldKey] === "object" && item.old[oldKey].name === columnName){
185
+ oldColumn = item.old[oldKey];
186
+ }
187
+ });
188
+
189
+ // If column didn't exist before, or didn't have indexes, all indexes are new
190
+ if(!oldColumn || !oldColumn.indexes){
191
+ newIndexes.forEach(function(indexName){
192
+ item.newIndexes.push({
193
+ tableName: item.name,
194
+ columnName: columnName,
195
+ indexName: indexName
196
+ });
197
+ });
198
+ } else {
199
+ // Check for new indexes that weren't in the old column
200
+ newIndexes.forEach(function(indexName){
201
+ if(!oldColumn.indexes.includes(indexName)){
202
+ item.newIndexes.push({
203
+ tableName: item.name,
204
+ columnName: columnName,
205
+ indexName: indexName
206
+ });
207
+ }
208
+ });
209
+ }
210
+ }
211
+ });
212
+ }
213
+ });
214
+ return tables;
215
+ }
216
+
217
+ #findDeletedIndexes(tables){
218
+ tables.forEach(function (item, index) {
219
+ if(item.new && item.old){
220
+ Object.keys(item.old).forEach(function (key) {
221
+ if(typeof item.old[key] === "object" && item.old[key].indexes){
222
+ var columnName = item.old[key].name;
223
+ var oldIndexes = item.old[key].indexes;
224
+
225
+ // Check if this column still exists
226
+ var newColumn = null;
227
+ Object.keys(item.new).forEach(function (newKey) {
228
+ if(typeof item.new[newKey] === "object" && item.new[newKey].name === columnName){
229
+ newColumn = item.new[newKey];
230
+ }
231
+ });
232
+
233
+ // If column doesn't exist anymore, or doesn't have indexes, all indexes are deleted
234
+ if(!newColumn || !newColumn.indexes){
235
+ oldIndexes.forEach(function(indexName){
236
+ item.deletedIndexes.push({
237
+ tableName: item.name,
238
+ columnName: columnName,
239
+ indexName: indexName
240
+ });
241
+ });
242
+ } else {
243
+ // Check for indexes that were removed
244
+ oldIndexes.forEach(function(indexName){
245
+ if(!newColumn.indexes.includes(indexName)){
246
+ item.deletedIndexes.push({
247
+ tableName: item.name,
248
+ columnName: columnName,
249
+ indexName: indexName
250
+ });
251
+ }
252
+ });
253
+ }
254
+ }
255
+ });
256
+ }
257
+ });
164
258
  return tables;
165
259
  }
166
260
 
@@ -302,6 +396,8 @@ class Migrations{
302
396
  (t.newColumns && t.newColumns.length) ||
303
397
  (t.deletedColumns && t.deletedColumns.length) ||
304
398
  (t.updatedColumns && t.updatedColumns.length) ||
399
+ (t.newIndexes && t.newIndexes.length) ||
400
+ (t.deletedIndexes && t.deletedIndexes.length) ||
305
401
  (t.old === null) || (t.new === null)){
306
402
  return true;
307
403
  }
@@ -348,6 +444,14 @@ class Migrations{
348
444
  }
349
445
  });
350
446
 
447
+ item.newIndexes.forEach(function (indexInfo, index) {
448
+ MT.createIndex("up", indexInfo);
449
+ });
450
+
451
+ item.deletedIndexes.forEach(function (indexInfo, index) {
452
+ MT.dropIndex("up", indexInfo);
453
+ });
454
+
351
455
  });
352
456
 
353
457
  return MT.get();
@@ -110,6 +110,22 @@ class schema{
110
110
  var query = queryBuilder.createTable(table);
111
111
  this.context._execute(query);
112
112
  }
113
+
114
+ // Create indexes for columns that have .index() defined
115
+ const self = this;
116
+ Object.keys(table).forEach(function(key){
117
+ if(typeof table[key] === "object" && table[key].indexes && !key.startsWith('__')){
118
+ const columnName = table[key].name;
119
+ table[key].indexes.forEach(function(indexName){
120
+ const indexInfo = {
121
+ tableName: tableName,
122
+ columnName: columnName,
123
+ indexName: indexName
124
+ };
125
+ self.createIndex(indexInfo);
126
+ });
127
+ }
128
+ });
113
129
  }
114
130
  }else{
115
131
  console.log("Table that you're trying to create is undefined. Please check if there are any changes that need to be made");
@@ -394,6 +410,56 @@ class schema{
394
410
  }
395
411
  }
396
412
 
413
+ createIndex(indexInfo){
414
+ if(indexInfo){
415
+ if(this.context.isSQLite){
416
+ var sqliteQuery = require("./migrationSQLiteQuery");
417
+ var queryBuilder = new sqliteQuery();
418
+ var query = queryBuilder.createIndex(indexInfo);
419
+ this.context._execute(query);
420
+ }
421
+
422
+ if(this.context.isMySQL){
423
+ var sqlquery = require("./migrationMySQLQuery");
424
+ var queryBuilder = new sqlquery();
425
+ var query = queryBuilder.createIndex(indexInfo);
426
+ this.context._execute(query);
427
+ }
428
+
429
+ if(this.context.isPostgres){
430
+ var postgresQuery = require("./migrationPostgresQuery");
431
+ var queryBuilder = new postgresQuery();
432
+ var query = queryBuilder.createIndex(indexInfo);
433
+ this.context._execute(query);
434
+ }
435
+ }
436
+ }
437
+
438
+ dropIndex(indexInfo){
439
+ if(indexInfo){
440
+ if(this.context.isSQLite){
441
+ var sqliteQuery = require("./migrationSQLiteQuery");
442
+ var queryBuilder = new sqliteQuery();
443
+ var query = queryBuilder.dropIndex(indexInfo);
444
+ this.context._execute(query);
445
+ }
446
+
447
+ if(this.context.isMySQL){
448
+ var sqlquery = require("./migrationMySQLQuery");
449
+ var queryBuilder = new sqlquery();
450
+ var query = queryBuilder.dropIndex(indexInfo);
451
+ this.context._execute(query);
452
+ }
453
+
454
+ if(this.context.isPostgres){
455
+ var postgresQuery = require("./migrationPostgresQuery");
456
+ var queryBuilder = new postgresQuery();
457
+ var query = queryBuilder.dropIndex(indexInfo);
458
+ this.context._execute(query);
459
+ }
460
+ }
461
+ }
462
+
397
463
  seed(tableName, rows){
398
464
  if(!tableName || !rows){ return; }
399
465
  const items = Array.isArray(rows) ? rows : [rows];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "masterrecord",
3
- "version": "0.3.29",
3
+ "version": "0.3.30",
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
@@ -1790,6 +1790,116 @@ try {
1790
1790
 
1791
1791
  ---
1792
1792
 
1793
+ ## Field Constraints & Indexes
1794
+
1795
+ Define database constraints and performance indexes using the fluent API:
1796
+
1797
+ ```javascript
1798
+ class User {
1799
+ id(db) {
1800
+ db.integer().primary().auto();
1801
+ }
1802
+
1803
+ email(db) {
1804
+ db.string()
1805
+ .notNullable()
1806
+ .unique()
1807
+ .index(); // Creates performance index
1808
+ }
1809
+
1810
+ username(db) {
1811
+ db.string()
1812
+ .notNullable()
1813
+ .index('idx_username_custom'); // Custom index name
1814
+ }
1815
+
1816
+ status(db) {
1817
+ db.string().nullable();
1818
+ }
1819
+
1820
+ created_at(db) {
1821
+ db.timestamp().default('CURRENT_TIMESTAMP');
1822
+ }
1823
+ }
1824
+ ```
1825
+
1826
+ ### Available Constraint Methods
1827
+
1828
+ - `.notNullable()` - Column cannot be NULL
1829
+ - `.nullable()` - Column can be NULL (default)
1830
+ - `.unique()` - Unique constraint (enforces uniqueness at DB level)
1831
+ - `.index()` - Creates performance index (auto-generated name: `idx_tablename_columnname`)
1832
+ - `.index('custom_name')` - Creates index with custom name
1833
+ - `.primary()` - Primary key (automatically indexed)
1834
+ - `.default(value)` - Default value
1835
+
1836
+ ### Index vs Unique Constraint
1837
+
1838
+ **Understanding the difference:**
1839
+
1840
+ - `.unique()` creates a UNIQUE constraint (prevents duplicate values, enforces data integrity)
1841
+ - `.index()` creates a performance index (improves query speed, allows duplicates)
1842
+ - You can use both together: `.unique().index()` creates a unique index for both integrity and performance
1843
+
1844
+ **Examples:**
1845
+
1846
+ ```javascript
1847
+ // Email must be unique (no performance index)
1848
+ email(db) {
1849
+ db.string().notNullable().unique();
1850
+ }
1851
+
1852
+ // Username indexed for fast lookups (allows duplicates)
1853
+ username(db) {
1854
+ db.string().notNullable().index();
1855
+ }
1856
+
1857
+ // Email with both unique constraint AND performance index
1858
+ email(db) {
1859
+ db.string().notNullable().unique().index();
1860
+ }
1861
+ ```
1862
+
1863
+ ### Automatic Index Migration
1864
+
1865
+ When you add `.index()` to a field, MasterRecord automatically generates migration code:
1866
+
1867
+ ```javascript
1868
+ // In your entity
1869
+ class User {
1870
+ email(db) {
1871
+ db.string().notNullable().index();
1872
+ }
1873
+ }
1874
+
1875
+ // Generated migration (automatic)
1876
+ class Migration_20250101 extends masterrecord.schema {
1877
+ async up(table) {
1878
+ this.init(table);
1879
+ this.createIndex({
1880
+ tableName: 'User',
1881
+ columnName: 'email',
1882
+ indexName: 'idx_user_email'
1883
+ });
1884
+ }
1885
+
1886
+ async down(table) {
1887
+ this.init(table);
1888
+ this.dropIndex({
1889
+ tableName: 'User',
1890
+ columnName: 'email',
1891
+ indexName: 'idx_user_email'
1892
+ });
1893
+ }
1894
+ }
1895
+ ```
1896
+
1897
+ **Rollback support:**
1898
+
1899
+ Migrations automatically include rollback logic. Running `masterrecord migrate down` will drop all indexes created by that migration.
1900
+
1901
+ ---
1902
+
1793
1903
  ## Business Logic Validation
1794
1904
 
1795
1905
  Add validators to your entity definitions for automatic validation on property assignment.
@@ -2301,20 +2411,60 @@ await db.saveChanges(); // Batch insert
2301
2411
 
2302
2412
  ### 3. Use Indexes
2303
2413
 
2414
+ Define indexes directly in your entity using `.index()`:
2415
+
2304
2416
  ```javascript
2305
2417
  class User {
2306
- constructor() {
2307
- this.email = {
2308
- type: 'string',
2309
- unique: true // Automatically creates index
2310
- };
2418
+ id(db) {
2419
+ db.integer().primary().auto(); // Primary keys are automatically indexed
2420
+ }
2421
+
2422
+ 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
2311
2435
  }
2312
2436
  }
2437
+ ```
2438
+
2439
+ **Migration automatically generates:**
2313
2440
 
2314
- // For complex queries, add database indexes manually
2315
- // CREATE INDEX idx_user_status ON User(status);
2441
+ ```javascript
2442
+ // In migration file (generated automatically)
2443
+ this.createIndex({
2444
+ tableName: 'User',
2445
+ columnName: 'email',
2446
+ indexName: 'idx_user_email'
2447
+ });
2316
2448
  ```
2317
2449
 
2450
+ **Rollback support:**
2451
+
2452
+ ```javascript
2453
+ // Down migration automatically includes
2454
+ this.dropIndex({
2455
+ tableName: 'User',
2456
+ columnName: 'email',
2457
+ indexName: 'idx_user_email'
2458
+ });
2459
+ ```
2460
+
2461
+ **Best practices:**
2462
+ - Index columns used in WHERE clauses
2463
+ - Index foreign key columns for join performance
2464
+ - Don't over-index - each index adds write overhead
2465
+ - Primary keys are automatically indexed (no need for `.index()`)
2466
+ - Use `.unique()` for data integrity, `.index()` for query performance
2467
+
2318
2468
  ### 4. Limit Result Sets
2319
2469
 
2320
2470
  ```javascript