masterrecord 0.3.26 → 0.3.29
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/.claude/settings.local.json +21 -1
- package/Entity/entityModel.js +136 -0
- package/Entity/entityModelBuilder.js +21 -3
- package/Entity/entityTrackerModel.js +251 -1
- package/QueryLanguage/queryMethods.js +330 -4
- package/SQLLiteEngine.js +4 -0
- package/Tools.js +15 -2
- package/context.js +198 -5
- package/mySQLEngine.js +11 -1
- package/package.json +1 -1
- package/postgresEngine.js +6 -1
- package/readme.md +1070 -102
- package/test/bulk-operations-test.js +235 -0
- package/test/cache-toObject-test.js +105 -0
- package/test/debug-id-test.js +63 -0
- package/test/double-where-bug-test.js +71 -0
- package/test/entity-methods-test.js +269 -0
- package/test/id-setting-validation.js +202 -0
- package/test/insert-return-test.js +39 -0
- package/test/lifecycle-hooks-test.js +258 -0
- package/test/query-helpers-test.js +258 -0
- package/test/query-isolation-test.js +59 -0
- package/test/simple-id-test.js +61 -0
- package/test/single-user-id-test.js +70 -0
- package/test/validation-test.js +302 -0
package/context.js
CHANGED
|
@@ -983,7 +983,15 @@ class context {
|
|
|
983
983
|
this.__entities.push(validModel); // Store model object
|
|
984
984
|
const buildMod = tools.createNewInstance(validModel, query, this);
|
|
985
985
|
this.__builderEntities.push(buildMod); // Store query builder entity
|
|
986
|
-
|
|
986
|
+
|
|
987
|
+
// Use getter to return fresh query instance each time (prevents parameter accumulation)
|
|
988
|
+
Object.defineProperty(this, validModel.__name, {
|
|
989
|
+
get: function() {
|
|
990
|
+
return tools.createNewInstance(validModel, query, this);
|
|
991
|
+
},
|
|
992
|
+
configurable: true,
|
|
993
|
+
enumerable: true
|
|
994
|
+
});
|
|
987
995
|
}
|
|
988
996
|
|
|
989
997
|
/**
|
|
@@ -1057,14 +1065,34 @@ class context {
|
|
|
1057
1065
|
* @param {Array<object>} entities - Entities to insert
|
|
1058
1066
|
*/
|
|
1059
1067
|
async _processBatchInserts(entities) {
|
|
1068
|
+
// Execute beforeSave hooks
|
|
1069
|
+
for (const entity of entities) {
|
|
1070
|
+
if (typeof entity.beforeSave === 'function') {
|
|
1071
|
+
await entity.beforeSave.call(entity);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1060
1075
|
if (entities.length === 1) {
|
|
1061
|
-
// Single insert - use existing insertManager
|
|
1076
|
+
// Single insert - use existing insertManager (already sets ID)
|
|
1062
1077
|
const insert = new insertManager(this._SQLEngine, this._isModelValid, this.__entities);
|
|
1063
1078
|
await insert.init(entities[0]);
|
|
1064
1079
|
} else {
|
|
1065
1080
|
// Batch insert - 100x faster for multiple records
|
|
1066
1081
|
try {
|
|
1067
|
-
await this._SQLEngine.bulkInsert(entities);
|
|
1082
|
+
const results = await this._SQLEngine.bulkInsert(entities);
|
|
1083
|
+
|
|
1084
|
+
// Set auto-increment IDs back on entities
|
|
1085
|
+
for (let i = 0; i < entities.length; i++) {
|
|
1086
|
+
const entity = entities[i];
|
|
1087
|
+
const result = results[i];
|
|
1088
|
+
|
|
1089
|
+
if (result && result.id) {
|
|
1090
|
+
const primaryKey = tools.getPrimaryKeyObject(entity.__entity);
|
|
1091
|
+
if (entity.__entity[primaryKey]?.auto === true) {
|
|
1092
|
+
entity[primaryKey] = result.id;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1068
1096
|
} catch (error) {
|
|
1069
1097
|
console.error('[Context] Bulk insert failed, falling back to individual inserts:', error.message);
|
|
1070
1098
|
// Fallback to individual inserts
|
|
@@ -1074,6 +1102,13 @@ class context {
|
|
|
1074
1102
|
}
|
|
1075
1103
|
}
|
|
1076
1104
|
}
|
|
1105
|
+
|
|
1106
|
+
// Execute afterSave hooks
|
|
1107
|
+
for (const entity of entities) {
|
|
1108
|
+
if (typeof entity.afterSave === 'function') {
|
|
1109
|
+
await entity.afterSave.call(entity);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1077
1112
|
}
|
|
1078
1113
|
|
|
1079
1114
|
/**
|
|
@@ -1083,6 +1118,13 @@ class context {
|
|
|
1083
1118
|
* @param {Array<object>} entities - Entities to update
|
|
1084
1119
|
*/
|
|
1085
1120
|
async _processBatchUpdates(entities) {
|
|
1121
|
+
// Execute beforeSave hooks
|
|
1122
|
+
for (const entity of entities) {
|
|
1123
|
+
if (typeof entity.beforeSave === 'function') {
|
|
1124
|
+
await entity.beforeSave.call(entity);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1086
1128
|
if (entities.length === 1) {
|
|
1087
1129
|
// Single update - use existing logic
|
|
1088
1130
|
const currentModel = entities[0];
|
|
@@ -1132,6 +1174,13 @@ class context {
|
|
|
1132
1174
|
}
|
|
1133
1175
|
}
|
|
1134
1176
|
}
|
|
1177
|
+
|
|
1178
|
+
// Execute afterSave hooks
|
|
1179
|
+
for (const entity of entities) {
|
|
1180
|
+
if (typeof entity.afterSave === 'function') {
|
|
1181
|
+
await entity.afterSave.call(entity);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1135
1184
|
}
|
|
1136
1185
|
|
|
1137
1186
|
/**
|
|
@@ -1141,6 +1190,13 @@ class context {
|
|
|
1141
1190
|
* @param {Array<object>} entities - Entities to delete
|
|
1142
1191
|
*/
|
|
1143
1192
|
async _processBatchDeletes(entities) {
|
|
1193
|
+
// Execute beforeDelete hooks
|
|
1194
|
+
for (const entity of entities) {
|
|
1195
|
+
if (typeof entity.beforeDelete === 'function') {
|
|
1196
|
+
await entity.beforeDelete.call(entity);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1144
1200
|
if (entities.length === 1) {
|
|
1145
1201
|
// Single delete - use existing deleteManager
|
|
1146
1202
|
const deleteObject = new deleteManager(this._SQLEngine, this.__entities);
|
|
@@ -1174,6 +1230,13 @@ class context {
|
|
|
1174
1230
|
}
|
|
1175
1231
|
}
|
|
1176
1232
|
}
|
|
1233
|
+
|
|
1234
|
+
// Execute afterDelete hooks
|
|
1235
|
+
for (const entity of entities) {
|
|
1236
|
+
if (typeof entity.afterDelete === 'function') {
|
|
1237
|
+
await entity.afterDelete.call(entity);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1177
1240
|
}
|
|
1178
1241
|
|
|
1179
1242
|
/**
|
|
@@ -1202,8 +1265,8 @@ class context {
|
|
|
1202
1265
|
// Performance: Collect affected tables for cache invalidation (single pass)
|
|
1203
1266
|
const affectedTables = new Set();
|
|
1204
1267
|
for (const entity of tracked) {
|
|
1205
|
-
if (entity.
|
|
1206
|
-
affectedTables.add(entity.
|
|
1268
|
+
if (entity.__name) {
|
|
1269
|
+
affectedTables.add(entity.__name);
|
|
1207
1270
|
}
|
|
1208
1271
|
}
|
|
1209
1272
|
|
|
@@ -1286,6 +1349,136 @@ class context {
|
|
|
1286
1349
|
this._queryCache.clear();
|
|
1287
1350
|
}
|
|
1288
1351
|
|
|
1352
|
+
/**
|
|
1353
|
+
* Bulk create multiple entities at once
|
|
1354
|
+
*
|
|
1355
|
+
* Creates multiple entity instances and saves them in a single batch operation.
|
|
1356
|
+
* Much faster than creating entities individually.
|
|
1357
|
+
*
|
|
1358
|
+
* @param {string} entityName - Name of the entity class (e.g., 'User')
|
|
1359
|
+
* @param {Array<Object>} data - Array of objects with entity properties
|
|
1360
|
+
* @returns {Promise<Array<Object>>} Array of created entities with IDs set
|
|
1361
|
+
*
|
|
1362
|
+
* @example
|
|
1363
|
+
* const users = await db.bulkCreate('User', [
|
|
1364
|
+
* { name: 'Alice', email: 'alice@example.com' },
|
|
1365
|
+
* { name: 'Bob', email: 'bob@example.com' },
|
|
1366
|
+
* { name: 'Charlie', email: 'charlie@example.com' }
|
|
1367
|
+
* ]);
|
|
1368
|
+
* console.log(users[0].id); // IDs are automatically set
|
|
1369
|
+
*/
|
|
1370
|
+
async bulkCreate(entityName, data) {
|
|
1371
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
1372
|
+
throw new Error('bulkCreate requires a non-empty array of data');
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
const EntityClass = this[entityName];
|
|
1376
|
+
if (!EntityClass) {
|
|
1377
|
+
throw new Error(`Entity ${entityName} not found in context`);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
const entities = [];
|
|
1381
|
+
for (const item of data) {
|
|
1382
|
+
const entity = EntityClass.new();
|
|
1383
|
+
// Copy properties from data object to entity
|
|
1384
|
+
for (const key in item) {
|
|
1385
|
+
if (item.hasOwnProperty(key)) {
|
|
1386
|
+
entity[key] = item[key];
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
entities.push(entity);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
await this.saveChanges();
|
|
1393
|
+
return entities;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/**
|
|
1397
|
+
* Bulk update multiple entities at once
|
|
1398
|
+
*
|
|
1399
|
+
* Updates multiple existing entities in a single batch operation.
|
|
1400
|
+
* Entities must already be tracked in the context.
|
|
1401
|
+
*
|
|
1402
|
+
* @param {string} entityName - Name of the entity class (e.g., 'User')
|
|
1403
|
+
* @param {Array<Object>} updates - Array of objects with id and properties to update
|
|
1404
|
+
* @returns {Promise<boolean>} True if updates were successful
|
|
1405
|
+
*
|
|
1406
|
+
* @example
|
|
1407
|
+
* await db.bulkUpdate('User', [
|
|
1408
|
+
* { id: 1, status: 'active' },
|
|
1409
|
+
* { id: 2, status: 'active' },
|
|
1410
|
+
* { id: 3, status: 'inactive' }
|
|
1411
|
+
* ]);
|
|
1412
|
+
*/
|
|
1413
|
+
async bulkUpdate(entityName, updates) {
|
|
1414
|
+
if (!Array.isArray(updates) || updates.length === 0) {
|
|
1415
|
+
throw new Error('bulkUpdate requires a non-empty array of updates');
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
const EntityClass = this[entityName];
|
|
1419
|
+
if (!EntityClass) {
|
|
1420
|
+
throw new Error(`Entity ${entityName} not found in context`);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// Fetch all entities by ID
|
|
1424
|
+
const ids = updates.map(u => u.id).filter(id => id !== undefined);
|
|
1425
|
+
if (ids.length !== updates.length) {
|
|
1426
|
+
throw new Error('All update objects must have an id property');
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// Load entities and apply updates
|
|
1430
|
+
for (const update of updates) {
|
|
1431
|
+
const entity = await EntityClass.findById(update.id);
|
|
1432
|
+
if (!entity) {
|
|
1433
|
+
throw new Error(`${entityName} with id ${update.id} not found`);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// Apply updates to entity
|
|
1437
|
+
for (const key in update) {
|
|
1438
|
+
if (update.hasOwnProperty(key) && key !== 'id') {
|
|
1439
|
+
entity[key] = update[key];
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
await this.saveChanges();
|
|
1445
|
+
return true;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
/**
|
|
1449
|
+
* Bulk delete multiple entities at once
|
|
1450
|
+
*
|
|
1451
|
+
* Deletes multiple entities by their IDs in a single batch operation.
|
|
1452
|
+
* Much faster than deleting entities individually.
|
|
1453
|
+
*
|
|
1454
|
+
* @param {string} entityName - Name of the entity class (e.g., 'User')
|
|
1455
|
+
* @param {Array<number|string>} ids - Array of entity IDs to delete
|
|
1456
|
+
* @returns {Promise<boolean>} True if deletions were successful
|
|
1457
|
+
*
|
|
1458
|
+
* @example
|
|
1459
|
+
* await db.bulkDelete('User', [1, 2, 3, 4, 5]);
|
|
1460
|
+
*/
|
|
1461
|
+
async bulkDelete(entityName, ids) {
|
|
1462
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
1463
|
+
throw new Error('bulkDelete requires a non-empty array of IDs');
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
const EntityClass = this[entityName];
|
|
1467
|
+
if (!EntityClass) {
|
|
1468
|
+
throw new Error(`Entity ${entityName} not found in context`);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// Load entities and mark for deletion
|
|
1472
|
+
for (const id of ids) {
|
|
1473
|
+
const entity = await EntityClass.findById(id);
|
|
1474
|
+
if (entity) {
|
|
1475
|
+
await entity.delete();
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
return true;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1289
1482
|
/**
|
|
1290
1483
|
* Enable or disable query caching
|
|
1291
1484
|
*
|
package/mySQLEngine.js
CHANGED
|
@@ -93,7 +93,13 @@ class MySQLEngine {
|
|
|
93
93
|
|
|
94
94
|
const query = `INSERT INTO \`${first.tableName}\` (${first.columns}) VALUES ${valueGroups.join(', ')}`;
|
|
95
95
|
const result = await this._runWithParams(query, allParams);
|
|
96
|
-
|
|
96
|
+
|
|
97
|
+
// MySQL returns insertId (first ID) and affectedRows (count)
|
|
98
|
+
// Generate individual result objects for each entity
|
|
99
|
+
const firstId = result.insertId;
|
|
100
|
+
for (let i = 0; i < tableEntities.length; i++) {
|
|
101
|
+
results.push({ id: firstId + i });
|
|
102
|
+
}
|
|
97
103
|
}
|
|
98
104
|
|
|
99
105
|
return results;
|
|
@@ -447,6 +453,10 @@ class MySQLEngine {
|
|
|
447
453
|
|
|
448
454
|
for (const ent in entity) {
|
|
449
455
|
if (!ent.startsWith("_")) {
|
|
456
|
+
// Skip lifecycle hooks - they are not database columns
|
|
457
|
+
if (entity[ent].lifecycle === true) {
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
450
460
|
if (!entity[ent].foreignKey) {
|
|
451
461
|
if (entity[ent].relationshipTable) {
|
|
452
462
|
if ($that.chechUnsupportedWords(entity[ent].relationshipTable)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "masterrecord",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.29",
|
|
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/postgresEngine.js
CHANGED
|
@@ -120,7 +120,12 @@ class postgresEngine {
|
|
|
120
120
|
|
|
121
121
|
const query = `INSERT INTO "${first.tableName}" (${first.columns}) VALUES ${valueGroups.join(', ')} RETURNING ${primaryKey}`;
|
|
122
122
|
const result = await this._runWithParams(query, allParams);
|
|
123
|
-
|
|
123
|
+
|
|
124
|
+
// PostgreSQL returns rows with the primary key values
|
|
125
|
+
// Convert to consistent format: { id: value }
|
|
126
|
+
for (const row of result.rows) {
|
|
127
|
+
results.push({ id: row[primaryKey] });
|
|
128
|
+
}
|
|
124
129
|
}
|
|
125
130
|
|
|
126
131
|
return results;
|