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.
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Test: Lifecycle Hooks
3
+ * Tests: beforeSave, afterSave, beforeDelete, afterDelete
4
+ */
5
+
6
+ const masterrecord = require('../MasterRecord.js');
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+
10
+ console.log("╔════════════════════════════════════════════════════════════════╗");
11
+ console.log("║ Lifecycle Hooks Test ║");
12
+ console.log("╚════════════════════════════════════════════════════════════════╝\n");
13
+
14
+ let passed = 0;
15
+ let failed = 0;
16
+
17
+ // Track hook executions
18
+ const hookLog = [];
19
+
20
+ class User {
21
+ id(db) {
22
+ db.integer().primary().auto();
23
+ }
24
+ name(db) {
25
+ db.string();
26
+ }
27
+ email(db) {
28
+ db.string();
29
+ }
30
+
31
+ beforeSave() {
32
+ hookLog.push('beforeSave');
33
+ // Automatically set email domain if missing
34
+ if (this.email && !this.email.includes('@')) {
35
+ this.email = this.email + '@example.com';
36
+ }
37
+ }
38
+
39
+ afterSave() {
40
+ hookLog.push('afterSave');
41
+ }
42
+
43
+ beforeDelete() {
44
+ hookLog.push('beforeDelete');
45
+ // Prevent deletion of admin users
46
+ if (this.name === 'admin') {
47
+ throw new Error('Cannot delete admin user');
48
+ }
49
+ }
50
+
51
+ afterDelete() {
52
+ hookLog.push('afterDelete');
53
+ }
54
+ }
55
+
56
+ class TestContext extends masterrecord.context {
57
+ constructor() {
58
+ super();
59
+ }
60
+ onConfig(db) {
61
+ this.dbset(User);
62
+ }
63
+ }
64
+
65
+ const dbPath = path.join(__dirname, '..', 'database', 'lifecycleHooks.db');
66
+ if (fs.existsSync(dbPath)) {
67
+ fs.unlinkSync(dbPath);
68
+ }
69
+
70
+ async function runTests() {
71
+ const db = new TestContext();
72
+
73
+ // Initialize SQLite database
74
+ const SQLLiteEngine = require('../SQLLiteEngine');
75
+ const sqlite3 = require('better-sqlite3');
76
+ db.isSQLite = true;
77
+ db.isMySQL = false;
78
+ db.isPostgres = false;
79
+ db._SQLEngine = new SQLLiteEngine();
80
+ db.db = new sqlite3(dbPath);
81
+ db._SQLEngine.setDB(db.db, 'better-sqlite3');
82
+
83
+ db.onConfig();
84
+
85
+ // Create table
86
+ db.db.exec('CREATE TABLE IF NOT EXISTS User (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT)');
87
+
88
+ // Test 1: beforeSave hook executes on insert
89
+ console.log("📝 Test 1: beforeSave hook executes on insert");
90
+ console.log("──────────────────────────────────────────────────");
91
+ try {
92
+ hookLog.length = 0;
93
+
94
+ const user = db.User.new();
95
+ user.name = 'Alice';
96
+ user.email = 'alice'; // No @ - should be fixed by hook
97
+ await user.save();
98
+
99
+ if (hookLog.includes('beforeSave') && user.email === 'alice@example.com') {
100
+ console.log(" ✓ beforeSave executed before insert");
101
+ console.log(" ✓ Email auto-completed to: alice@example.com");
102
+ passed++;
103
+ } else {
104
+ console.log(` ✗ Hook failed: ${hookLog.join(', ')}, email: ${user.email}`);
105
+ failed++;
106
+ }
107
+ } catch(err) {
108
+ console.log(` ✗ Error: ${err.message}`);
109
+ failed++;
110
+ }
111
+
112
+ // Test 2: afterSave hook executes after insert
113
+ console.log("\n📝 Test 2: afterSave hook executes after insert");
114
+ console.log("──────────────────────────────────────────────────");
115
+ try {
116
+ if (hookLog.includes('afterSave') && hookLog.indexOf('afterSave') > hookLog.indexOf('beforeSave')) {
117
+ console.log(" ✓ afterSave executed after insert");
118
+ console.log(" ✓ Hooks executed in order: beforeSave → afterSave");
119
+ passed++;
120
+ } else {
121
+ console.log(` ✗ Hook order incorrect: ${hookLog.join(' → ')}`);
122
+ failed++;
123
+ }
124
+ } catch(err) {
125
+ console.log(` ✗ Error: ${err.message}`);
126
+ failed++;
127
+ }
128
+
129
+ // Test 3: beforeSave hook executes on update
130
+ console.log("\n📝 Test 3: beforeSave hook executes on update");
131
+ console.log("──────────────────────────────────────────────────");
132
+ try {
133
+ hookLog.length = 0;
134
+
135
+ const user = await db.User.findById(1);
136
+ user.name = 'Alice Updated';
137
+ await user.save();
138
+
139
+ if (hookLog.includes('beforeSave')) {
140
+ console.log(" ✓ beforeSave executed on update");
141
+ passed++;
142
+ } else {
143
+ console.log(` ✗ beforeSave not called on update`);
144
+ failed++;
145
+ }
146
+ } catch(err) {
147
+ console.log(` ✗ Error: ${err.message}`);
148
+ failed++;
149
+ }
150
+
151
+ // Test 4: beforeDelete hook can prevent deletion
152
+ console.log("\n📝 Test 4: beforeDelete hook can prevent deletion");
153
+ console.log("──────────────────────────────────────────────────");
154
+ try {
155
+ const admin = db.User.new();
156
+ admin.name = 'admin';
157
+ admin.email = 'admin@example.com';
158
+ await admin.save();
159
+
160
+ const adminId = admin.id;
161
+
162
+ try {
163
+ await admin.delete();
164
+ console.log(` ✗ Deletion should have been prevented`);
165
+ failed++;
166
+ } catch (hookError) {
167
+ if (hookError.message.includes('Cannot delete admin user')) {
168
+ // Check that admin still exists
169
+ const check = await db.User.findById(adminId);
170
+ if (check && check.name === 'admin') {
171
+ console.log(" ✓ beforeDelete prevented deletion of admin user");
172
+ passed++;
173
+ } else {
174
+ console.log(` ✗ Admin was deleted despite hook`);
175
+ failed++;
176
+ }
177
+ } else {
178
+ console.log(` ✗ Unexpected error: ${hookError.message}`);
179
+ failed++;
180
+ }
181
+ }
182
+ } catch(err) {
183
+ console.log(` ✗ Error: ${err.message}`);
184
+ failed++;
185
+ }
186
+
187
+ // Test 5: afterDelete hook executes after successful deletion
188
+ console.log("\n📝 Test 5: afterDelete hook executes after deletion");
189
+ console.log("──────────────────────────────────────────────────");
190
+ try {
191
+ hookLog.length = 0;
192
+
193
+ const user = db.User.new();
194
+ user.name = 'ToDelete';
195
+ user.email = 'delete@example.com';
196
+ await user.save();
197
+
198
+ await user.delete();
199
+
200
+ if (hookLog.includes('beforeDelete') && hookLog.includes('afterDelete')) {
201
+ console.log(" ✓ beforeDelete and afterDelete executed");
202
+ console.log(" ✓ Hooks executed in order: beforeDelete → afterDelete");
203
+ passed++;
204
+ } else {
205
+ console.log(` ✗ Delete hooks missing: ${hookLog.join(', ')}`);
206
+ failed++;
207
+ }
208
+ } catch(err) {
209
+ console.log(` ✗ Error: ${err.message}`);
210
+ failed++;
211
+ }
212
+
213
+ // Test 6: Batch operations execute hooks for all entities
214
+ console.log("\n📝 Test 6: Batch operations execute hooks for all entities");
215
+ console.log("──────────────────────────────────────────────────");
216
+ try {
217
+ hookLog.length = 0;
218
+
219
+ const user1 = db.User.new();
220
+ user1.name = 'Batch1';
221
+ user1.email = 'batch1';
222
+
223
+ const user2 = db.User.new();
224
+ user2.name = 'Batch2';
225
+ user2.email = 'batch2';
226
+
227
+ await db.saveChanges();
228
+
229
+ const beforeSaveCount = hookLog.filter(h => h === 'beforeSave').length;
230
+ const afterSaveCount = hookLog.filter(h => h === 'afterSave').length;
231
+
232
+ if (beforeSaveCount === 2 && afterSaveCount === 2) {
233
+ console.log(" ✓ Hooks executed for all entities in batch");
234
+ console.log(` ✓ beforeSave: ${beforeSaveCount}x, afterSave: ${afterSaveCount}x`);
235
+ passed++;
236
+ } else {
237
+ console.log(` ✗ Hook counts incorrect: beforeSave=${beforeSaveCount}, afterSave=${afterSaveCount}`);
238
+ failed++;
239
+ }
240
+ } catch(err) {
241
+ console.log(` ✗ Error: ${err.message}`);
242
+ failed++;
243
+ }
244
+
245
+ // Summary
246
+ console.log("\n" + "=".repeat(64));
247
+ console.log(`Test Results: ${passed} passed, ${failed} failed`);
248
+ console.log("=".repeat(64));
249
+
250
+ if (failed > 0) {
251
+ process.exit(1);
252
+ }
253
+ }
254
+
255
+ runTests().catch(err => {
256
+ console.error('Fatal error:', err);
257
+ process.exit(1);
258
+ });
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Test: Query Helper Methods
3
+ * Tests: .first(), .last(), .exists(), .pluck()
4
+ */
5
+
6
+ const masterrecord = require('../MasterRecord.js');
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+
10
+ console.log("╔════════════════════════════════════════════════════════════════╗");
11
+ console.log("║ Query Helper Methods Test ║");
12
+ console.log("╚════════════════════════════════════════════════════════════════╝\n");
13
+
14
+ let passed = 0;
15
+ let failed = 0;
16
+
17
+ // Define test entities
18
+ class User {
19
+ id(db) {
20
+ db.integer().primary().auto();
21
+ }
22
+ name(db) {
23
+ db.string();
24
+ }
25
+ email(db) {
26
+ db.string();
27
+ }
28
+ }
29
+
30
+ // Create test context
31
+ class TestContext extends masterrecord.context {
32
+ constructor() {
33
+ super();
34
+ }
35
+ onConfig(db) {
36
+ this.dbset(User);
37
+ }
38
+ }
39
+
40
+ // Clean test database
41
+ const dbPath = path.join(__dirname, '..', 'database', 'testQueryHelpers.db');
42
+ if (fs.existsSync(dbPath)) {
43
+ fs.unlinkSync(dbPath);
44
+ }
45
+
46
+ async function runTests() {
47
+ const db = new TestContext();
48
+
49
+ // Initialize SQLite database
50
+ const SQLLiteEngine = require('../SQLLiteEngine');
51
+ const sqlite3 = require('better-sqlite3');
52
+ db.isSQLite = true;
53
+ db.isMySQL = false;
54
+ db.isPostgres = false;
55
+ db._SQLEngine = new SQLLiteEngine();
56
+ db.db = new sqlite3(dbPath);
57
+ db._SQLEngine.setDB(db.db, 'better-sqlite3');
58
+
59
+ db.onConfig();
60
+
61
+ // Create table
62
+ db.db.exec('CREATE TABLE IF NOT EXISTS User (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT)');
63
+
64
+ // Create test data
65
+ const users = [
66
+ { name: 'Alice', email: 'alice@example.com' },
67
+ { name: 'Bob', email: 'bob@example.com' },
68
+ { name: 'Charlie', email: 'charlie@example.com' },
69
+ { name: 'Dave', email: 'dave@example.com' },
70
+ { name: 'Eve', email: 'eve@example.com' }
71
+ ];
72
+
73
+ for (const userData of users) {
74
+ const user = db.User.new();
75
+ user.name = userData.name;
76
+ user.email = userData.email;
77
+ await user.save();
78
+ }
79
+
80
+ // Test 1: .first() returns first record
81
+ console.log("📝 Test 1: .first() returns first record");
82
+ console.log("──────────────────────────────────────────────────");
83
+ try {
84
+ const first = await db.User.first();
85
+
86
+ if (first && first.name === 'Alice') {
87
+ console.log(" ✓ .first() returns first record ordered by primary key");
88
+ passed++;
89
+ } else {
90
+ console.log(` ✗ .first() failed - got: ${first?.name}`);
91
+ failed++;
92
+ }
93
+ } catch(err) {
94
+ console.log(` ✗ Error: ${err.message}`);
95
+ failed++;
96
+ }
97
+
98
+ // Test 2: .last() returns last record
99
+ console.log("\n📝 Test 2: .last() returns last record");
100
+ console.log("──────────────────────────────────────────────────");
101
+ try {
102
+ const last = await db.User.last();
103
+
104
+ if (last && last.name === 'Eve') {
105
+ console.log(" ✓ .last() returns last record ordered by primary key descending");
106
+ passed++;
107
+ } else {
108
+ console.log(` ✗ .last() failed - got: ${last?.name}`);
109
+ failed++;
110
+ }
111
+ } catch(err) {
112
+ console.log(` ✗ Error: ${err.message}`);
113
+ failed++;
114
+ }
115
+
116
+ // Test 3: .exists() returns true for existing records
117
+ console.log("\n📝 Test 3: .exists() checks record existence");
118
+ console.log("──────────────────────────────────────────────────");
119
+ try {
120
+ const exists = await db.User
121
+ .where(u => u.email == $$, 'alice@example.com')
122
+ .exists();
123
+
124
+ const notExists = await db.User
125
+ .where(u => u.email == $$, 'nonexistent@example.com')
126
+ .exists();
127
+
128
+ if (exists === true && notExists === false) {
129
+ console.log(" ✓ .exists() returns true for existing records");
130
+ console.log(" ✓ .exists() returns false for non-existing records");
131
+ passed++;
132
+ } else {
133
+ console.log(` ✗ .exists() failed - exists: ${exists}, notExists: ${notExists}`);
134
+ failed++;
135
+ }
136
+ } catch(err) {
137
+ console.log(` ✗ Error: ${err.message}`);
138
+ failed++;
139
+ }
140
+
141
+ // Test 4: .pluck() extracts column values
142
+ console.log("\n📝 Test 4: .pluck() extracts column values");
143
+ console.log("──────────────────────────────────────────────────");
144
+ try {
145
+ const emails = await db.User.pluck('email');
146
+
147
+ if (Array.isArray(emails) &&
148
+ emails.length === 5 &&
149
+ emails.includes('alice@example.com') &&
150
+ emails.includes('eve@example.com')) {
151
+ console.log(" ✓ .pluck() extracts column as array");
152
+ console.log(" ✓ All values included");
153
+ passed++;
154
+ } else {
155
+ console.log(` ✗ .pluck() failed - got: ${emails?.length} items`);
156
+ failed++;
157
+ }
158
+ } catch(err) {
159
+ console.log(` ✗ Error: ${err.message}`);
160
+ failed++;
161
+ }
162
+
163
+ // Test 5: .pluck() with where clause
164
+ console.log("\n📝 Test 5: .pluck() respects where clause");
165
+ console.log("──────────────────────────────────────────────────");
166
+ try {
167
+ const emails = await db.User
168
+ .where(u => u.name == $$, 'Alice')
169
+ .pluck('email');
170
+
171
+ if (Array.isArray(emails) &&
172
+ emails.length === 1 &&
173
+ emails[0] === 'alice@example.com') {
174
+ console.log(" ✓ .pluck() respects where clause");
175
+ passed++;
176
+ } else {
177
+ console.log(` ✗ .pluck() with where failed`);
178
+ failed++;
179
+ }
180
+ } catch(err) {
181
+ console.log(` ✗ Error: ${err.message}`);
182
+ failed++;
183
+ }
184
+
185
+ // Test 6: .first() with where clause
186
+ console.log("\n📝 Test 6: .first() respects where clause");
187
+ console.log("──────────────────────────────────────────────────");
188
+ try {
189
+ const first = await db.User
190
+ .where(u => u.name == $$, 'Charlie')
191
+ .first();
192
+
193
+ if (first && first.name === 'Charlie') {
194
+ console.log(" ✓ .first() respects where clause");
195
+ passed++;
196
+ } else {
197
+ console.log(` ✗ .first() with where failed`);
198
+ failed++;
199
+ }
200
+ } catch(err) {
201
+ console.log(` ✗ Error: ${err.message}`);
202
+ failed++;
203
+ }
204
+
205
+ // Test 7: .first() returns null if no records
206
+ console.log("\n📝 Test 7: .first() returns null if no records");
207
+ console.log("──────────────────────────────────────────────────");
208
+ try {
209
+ const first = await db.User
210
+ .where(u => u.name == $$, 'NonExistent')
211
+ .first();
212
+
213
+ if (first === null) {
214
+ console.log(" ✓ .first() returns null if no records");
215
+ passed++;
216
+ } else {
217
+ console.log(` ✗ .first() should return null`);
218
+ failed++;
219
+ }
220
+ } catch(err) {
221
+ console.log(` ✗ Error: ${err.message}`);
222
+ failed++;
223
+ }
224
+
225
+ // Test 8: .pluck() with ordering
226
+ console.log("\n📝 Test 8: .pluck() respects ordering");
227
+ console.log("──────────────────────────────────────────────────");
228
+ try {
229
+ const names = await db.User
230
+ .orderBy(u => u.name)
231
+ .pluck('name');
232
+
233
+ if (names[0] === 'Alice' && names[4] === 'Eve') {
234
+ console.log(" ✓ .pluck() respects orderBy");
235
+ passed++;
236
+ } else {
237
+ console.log(` ✗ .pluck() ordering failed`);
238
+ failed++;
239
+ }
240
+ } catch(err) {
241
+ console.log(` ✗ Error: ${err.message}`);
242
+ failed++;
243
+ }
244
+
245
+ // Summary
246
+ console.log("\n" + "=".repeat(64));
247
+ console.log(`Test Results: ${passed} passed, ${failed} failed`);
248
+ console.log("=".repeat(64));
249
+
250
+ if (failed > 0) {
251
+ process.exit(1);
252
+ }
253
+ }
254
+
255
+ runTests().catch(err => {
256
+ console.error('Fatal error:', err);
257
+ process.exit(1);
258
+ });
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Test for query instance isolation
3
+ * Verifies that accessing an entity multiple times returns fresh queryMethods instances
4
+ */
5
+
6
+ const queryMethods = require('../QueryLanguage/queryMethods');
7
+ const queryScript = require('../QueryLanguage/queryScript');
8
+
9
+ console.log('=== Testing Query Instance Isolation ===\n');
10
+
11
+ // Mock entity
12
+ const mockEntity = {
13
+ __name: 'TestEntity',
14
+ id: { type: 'number' },
15
+ name: { type: 'string' }
16
+ };
17
+
18
+ // Mock context
19
+ const mockContext = {
20
+ __track: () => {},
21
+ __entities: []
22
+ };
23
+
24
+ console.log('Test 1: Creating separate query instances');
25
+ const query1 = new queryMethods(mockEntity, mockContext);
26
+ const query2 = new queryMethods(mockEntity, mockContext);
27
+
28
+ console.log(` Query 1 instance ID: ${query1.__queryObject !== undefined ? 'exists' : 'undefined'}`);
29
+ console.log(` Query 2 instance ID: ${query2.__queryObject !== undefined ? 'exists' : 'undefined'}`);
30
+ console.log(` ✓ Instances are separate: ${query1 !== query2}`);
31
+ console.log(` ✓ QueryObjects are separate: ${query1.__queryObject !== query2.__queryObject}\n`);
32
+
33
+ console.log('Test 2: Building queries with same parameters in different instances');
34
+ query1.where(e => e.id == $$, 1);
35
+ query2.where(e => e.id == $$, 2);
36
+
37
+ const params1 = query1.__queryObject.parameters.get();
38
+ const params2 = query2.__queryObject.parameters.get();
39
+
40
+ console.log(` Query 1 parameters: [${params1}]`);
41
+ console.log(` Query 2 parameters: [${params2}]`);
42
+
43
+ if (params1.length === 1 && params1[0] === 1) {
44
+ console.log(' ✓ Query 1 has correct parameters');
45
+ } else {
46
+ console.error(' ✗ FAIL: Query 1 parameters are wrong!');
47
+ process.exit(1);
48
+ }
49
+
50
+ if (params2.length === 1 && params2[0] === 2) {
51
+ console.log(' ✓ Query 2 has correct parameters\n');
52
+ } else {
53
+ console.error(' ✗ FAIL: Query 2 parameters are wrong!');
54
+ process.exit(1);
55
+ }
56
+
57
+ console.log('✅ All tests passed! Query instances are properly isolated.\n');
58
+ console.log('Note: This confirms the fix will work. The getter in context.js');
59
+ console.log('now creates a new queryMethods instance each time an entity is accessed.');
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Simple test to check if ID is set after save
3
+ */
4
+
5
+ const masterrecord = require('../MasterRecord.js');
6
+ const path = require('path');
7
+ const fs = require('fs');
8
+
9
+ class User {
10
+ id(db) {
11
+ db.integer().primary().auto();
12
+ }
13
+ name(db) {
14
+ db.string();
15
+ }
16
+ }
17
+
18
+ class TestContext extends masterrecord.context {
19
+ constructor() {
20
+ super();
21
+ this.database = path.join(__dirname, '..', 'database', 'simpleIdTest.db');
22
+ }
23
+ onConfig(db) {
24
+ this.dbset(User);
25
+ }
26
+ }
27
+
28
+ // Clean
29
+ if (fs.existsSync(path.join(__dirname, '..', 'database', 'simpleIdTest.db'))) {
30
+ fs.unlinkSync(path.join(__dirname, '..', 'database', 'simpleIdTest.db'));
31
+ }
32
+
33
+ async function test() {
34
+ const db = new TestContext();
35
+ db.onConfig();
36
+
37
+ const user = db.User.new();
38
+ user.name = 'Test';
39
+
40
+ console.log('Before save - user.id:', user.id);
41
+ console.log('Before save - user.__proto__:', Object.keys(user.__proto__));
42
+
43
+ await user.save();
44
+
45
+ console.log('After save - user.id:', user.id);
46
+ console.log('After save - user.__proto__:', Object.keys(user.__proto__));
47
+ console.log('After save - user.__proto__._id:', user.__proto__._id);
48
+
49
+ // Try direct access
50
+ console.log('Direct access test:');
51
+ for (const key in user) {
52
+ if (key === 'id') {
53
+ console.log('Found id property via for-in');
54
+ }
55
+ }
56
+
57
+ console.log('Has own property id?', user.hasOwnProperty('id'));
58
+ console.log('Keys:', Object.keys(user));
59
+ }
60
+
61
+ test().catch(console.error);