@proteinjs/db 1.11.0 → 1.12.0

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/generated/index.js +1 -1
  3. package/dist/generated/index.js.map +1 -1
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +1 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/src/Columns.d.ts +30 -0
  9. package/dist/src/Columns.d.ts.map +1 -1
  10. package/dist/src/Columns.js +203 -1
  11. package/dist/src/Columns.js.map +1 -1
  12. package/dist/src/Db.d.ts +2 -1
  13. package/dist/src/Db.d.ts.map +1 -1
  14. package/dist/src/Db.js +8 -7
  15. package/dist/src/Db.js.map +1 -1
  16. package/dist/src/Record.d.ts +1 -1
  17. package/dist/src/Record.d.ts.map +1 -1
  18. package/dist/src/Record.js +3 -3
  19. package/dist/src/Record.js.map +1 -1
  20. package/dist/src/Table.d.ts +5 -4
  21. package/dist/src/Table.d.ts.map +1 -1
  22. package/dist/src/Table.js +13 -12
  23. package/dist/src/Table.js.map +1 -1
  24. package/dist/src/schema/TableManager.d.ts +1 -0
  25. package/dist/src/schema/TableManager.d.ts.map +1 -1
  26. package/dist/src/schema/TableManager.js +48 -1
  27. package/dist/src/schema/TableManager.js.map +1 -1
  28. package/dist/src/transaction/TransactionRunner.d.ts +5 -0
  29. package/dist/src/transaction/TransactionRunner.d.ts.map +1 -1
  30. package/dist/src/transaction/TransactionRunner.js +5 -0
  31. package/dist/src/transaction/TransactionRunner.js.map +1 -1
  32. package/dist/test/reusable/DynamicReferenceColumn.d.ts +77 -0
  33. package/dist/test/reusable/DynamicReferenceColumn.d.ts.map +1 -0
  34. package/dist/test/reusable/DynamicReferenceColumn.js +656 -0
  35. package/dist/test/reusable/DynamicReferenceColumn.js.map +1 -0
  36. package/generated/index.ts +1 -1
  37. package/index.ts +1 -0
  38. package/package.json +3 -3
  39. package/src/Columns.ts +190 -1
  40. package/src/Db.ts +12 -6
  41. package/src/Record.ts +7 -3
  42. package/src/Table.ts +17 -7
  43. package/src/schema/TableManager.ts +61 -0
  44. package/src/transaction/TransactionRunner.ts +6 -0
  45. package/test/reusable/DynamicReferenceColumn.ts +487 -0
@@ -0,0 +1,487 @@
1
+ import { DynamicReferenceColumn, StringColumn, DynamicReferenceTableNameColumn } from '../../src/Columns';
2
+ import { Db, DbDriver } from '../../src/Db';
3
+ import { Reference } from '../../src/reference/Reference';
4
+ import { Table } from '../../src/Table';
5
+ import { withRecordColumns, Record } from '../../src/Record';
6
+ import { QueryBuilder } from '@proteinjs/db-query';
7
+
8
+ interface ProjectAssignment extends Record {
9
+ projectName: string;
10
+ employeeTableName?: string | null;
11
+ employeeRef?: Reference<Engineer | Designer | ProjectManager> | null;
12
+ projectLeadTableName?: string | null;
13
+ projectLeadRef?: Reference<Engineer | Designer | ProjectManager> | null;
14
+ startDate: string;
15
+ }
16
+
17
+ interface Engineer extends Record {
18
+ name: string;
19
+ yearsOfExperience: number;
20
+ }
21
+
22
+ interface Designer extends Record {
23
+ name: string;
24
+ specialization: string;
25
+ }
26
+
27
+ interface ProjectManager extends Record {
28
+ name: string;
29
+ certificate: string;
30
+ }
31
+
32
+ class ProjectAssignmentTable extends Table<ProjectAssignment> {
33
+ name = 'db_test_project_assignments';
34
+ columns = withRecordColumns<ProjectAssignment>({
35
+ projectName: new StringColumn('project_name'),
36
+ employeeTableName: new DynamicReferenceTableNameColumn('employee_table_name', 'employee_ref'),
37
+ employeeRef: new DynamicReferenceColumn<Engineer | Designer | ProjectManager>(
38
+ 'employee_ref',
39
+ 'employee_table_name',
40
+ true // Enable cascade delete for testing
41
+ ),
42
+ projectLeadTableName: new DynamicReferenceTableNameColumn('project_lead_table_name', 'project_lead_ref', {
43
+ defaultValue: async () => 'TEST_DEFAULT_VALUE',
44
+ updateValue: async () => 'TEST_UPDATE_VALUE',
45
+ }),
46
+ projectLeadRef: new DynamicReferenceColumn<Engineer | Designer | ProjectManager>(
47
+ 'project_lead_ref',
48
+ 'project_lead_table_name',
49
+ true // Enable cascade delete for testing
50
+ ),
51
+ startDate: new StringColumn('start_date'),
52
+ });
53
+ }
54
+
55
+ class EngineerTable extends Table<Engineer> {
56
+ name = 'db_test_engineers';
57
+ columns = withRecordColumns<Engineer>({
58
+ name: new StringColumn('name'),
59
+ yearsOfExperience: new StringColumn('years_of_experience'),
60
+ });
61
+ }
62
+
63
+ class DesignerTable extends Table<Designer> {
64
+ name = 'db_test_designers';
65
+ columns = withRecordColumns<Designer>({
66
+ name: new StringColumn('name'),
67
+ specialization: new StringColumn('specialization'),
68
+ });
69
+ }
70
+
71
+ class ProjectManagerTable extends Table<ProjectManager> {
72
+ name = 'db_test_project_managers';
73
+ columns = withRecordColumns<ProjectManager>({
74
+ name: new StringColumn('name'),
75
+ certificate: new StringColumn('certificate'),
76
+ });
77
+ }
78
+
79
+ // Invalid table with missing DynamicReferenceTableNameColumn
80
+ class InvalidTableMissingTableName extends Table<Record> {
81
+ name = 'db_test_invalid_missing_table_name';
82
+ columns = withRecordColumns<Record>({
83
+ // Only has DynamicReferenceColumn without its required table name column
84
+ employeeRef: new DynamicReferenceColumn<Engineer | Designer | ProjectManager>(
85
+ 'employee_ref',
86
+ 'employee_table_name', // References a non-existent column
87
+ true
88
+ ),
89
+ });
90
+ }
91
+
92
+ // Invalid table with unused DynamicReferenceTableNameColumn
93
+ class InvalidTableUnusedTableName extends Table<Record> {
94
+ name = 'db_test_invalid_unused_table_name';
95
+ columns = withRecordColumns<Record>({
96
+ // Has DynamicReferenceTableNameColumn but no DynamicReferenceColumn using it
97
+ employeeTableName: new DynamicReferenceTableNameColumn('employee_table_name', 'employee_ref'),
98
+ });
99
+ }
100
+
101
+ /** Used for testing purposes only. */
102
+ export const getDynamicReferenceColumnTestTable = (tableName: string) => {
103
+ const projectAssignmentTable = new ProjectAssignmentTable();
104
+ const engineerTable = new EngineerTable();
105
+ const designerTable = new DesignerTable();
106
+ const projectManagerTable = new ProjectManagerTable();
107
+
108
+ switch (tableName) {
109
+ case projectAssignmentTable.name:
110
+ return projectAssignmentTable;
111
+ case engineerTable.name:
112
+ return engineerTable;
113
+ case designerTable.name:
114
+ return designerTable;
115
+ case projectManagerTable.name:
116
+ return projectManagerTable;
117
+ default:
118
+ throw new Error(`Cannot find test table: ${tableName}`);
119
+ }
120
+ };
121
+
122
+ // Test suite
123
+ export const dynamicReferenceColumnTests = (driver: DbDriver, dropTable: (table: Table<any>) => Promise<void>) => {
124
+ return () => {
125
+ let projectAssignmentTable: Table<ProjectAssignment>;
126
+ let engineerTable: Table<Engineer>;
127
+ let designerTable: Table<Designer>;
128
+ let projectManagerTable: Table<ProjectManager>;
129
+ const db = new Db(driver, getDynamicReferenceColumnTestTable);
130
+
131
+ beforeAll(async () => {
132
+ if (driver.start) {
133
+ await driver.start();
134
+ }
135
+
136
+ await driver.getTableManager().loadTable(new ProjectAssignmentTable());
137
+ await driver.getTableManager().loadTable(new EngineerTable());
138
+ await driver.getTableManager().loadTable(new DesignerTable());
139
+ await driver.getTableManager().loadTable(new ProjectManagerTable());
140
+ });
141
+
142
+ beforeEach(async () => {
143
+ await driver.getTableManager().loadTable(new ProjectAssignmentTable());
144
+ await driver.getTableManager().loadTable(new EngineerTable());
145
+ await driver.getTableManager().loadTable(new DesignerTable());
146
+ await driver.getTableManager().loadTable(new ProjectManagerTable());
147
+
148
+ projectAssignmentTable = new ProjectAssignmentTable();
149
+ engineerTable = new EngineerTable();
150
+ designerTable = new DesignerTable();
151
+ projectManagerTable = new ProjectManagerTable();
152
+ });
153
+
154
+ afterEach(async () => {
155
+ await dropTable(new ProjectAssignmentTable());
156
+ await dropTable(new EngineerTable());
157
+ await dropTable(new DesignerTable());
158
+ await dropTable(new ProjectManagerTable());
159
+ });
160
+
161
+ describe('TableManager validation', () => {
162
+ test('should throw error when DynamicReferenceTableNameColumn is missing', async () => {
163
+ const invalidTable = new InvalidTableMissingTableName();
164
+
165
+ await expect(async () => {
166
+ await driver.getTableManager().loadTable(invalidTable);
167
+ }).rejects.toThrow(/missing its required DynamicReferenceTableNameColumn 'employee_table_name'/);
168
+ });
169
+
170
+ test('should throw error when DynamicReferenceTableNameColumn is unused', async () => {
171
+ const invalidTable = new InvalidTableUnusedTableName();
172
+
173
+ await expect(async () => {
174
+ await driver.getTableManager().loadTable(invalidTable);
175
+ }).rejects.toThrow(
176
+ /has a DynamicReferenceTableNameColumn 'employee_table_name' but no DynamicReferenceColumn references it/
177
+ );
178
+ });
179
+ });
180
+
181
+ test('should handle references to different types', async () => {
182
+ // Create an engineer
183
+ const engineer = await db.insert(engineerTable, {
184
+ name: 'John Doe',
185
+ yearsOfExperience: 5,
186
+ });
187
+
188
+ // Create a designer
189
+ const designer = await db.insert(designerTable, {
190
+ name: 'Jane Smith',
191
+ specialization: 'UI/UX',
192
+ });
193
+
194
+ // Assign both to different projects
195
+ const engineerAssignment = await db.insert(projectAssignmentTable, {
196
+ projectName: 'Backend API',
197
+ employeeRef: new Reference(engineerTable.name, engineer.id),
198
+ startDate: '2024-01-01',
199
+ });
200
+
201
+ const designerAssignment = await db.insert(projectAssignmentTable, {
202
+ projectName: 'Website Redesign',
203
+ employeeRef: new Reference(designerTable.name, designer.id),
204
+ startDate: '2024-01-15',
205
+ });
206
+
207
+ // Verify engineer assignment
208
+ const fetchedEngineerAssignment = await db.get(projectAssignmentTable, { id: engineerAssignment.id });
209
+ expect(fetchedEngineerAssignment.employeeRef?._table).toBe(engineerTable.name);
210
+ expect(fetchedEngineerAssignment.employeeRef?._id).toBe(engineer.id);
211
+ expect(fetchedEngineerAssignment.employeeTableName).toBe(engineerTable.name);
212
+
213
+ // Verify designer assignment
214
+ const fetchedDesignerAssignment = await db.get(projectAssignmentTable, { id: designerAssignment.id });
215
+ expect(fetchedDesignerAssignment.employeeRef?._table).toBe(designerTable.name);
216
+ expect(fetchedDesignerAssignment.employeeRef?._id).toBe(designer.id);
217
+ expect(fetchedDesignerAssignment.employeeTableName).toBe(designerTable.name);
218
+ });
219
+
220
+ test('should cascade delete multiple types of records when references are deleted', async () => {
221
+ // Create employees
222
+ const engineer = await db.insert(engineerTable, {
223
+ name: 'John Doe',
224
+ yearsOfExperience: 5,
225
+ });
226
+
227
+ const projectManager = await db.insert(projectManagerTable, {
228
+ name: 'Alice Brown',
229
+ certificate: 'Scrum Master',
230
+ });
231
+
232
+ const designer = await db.insert(designerTable, {
233
+ name: 'Jane Smith',
234
+ specialization: 'UI/UX',
235
+ });
236
+
237
+ // Create assignments
238
+ const engineerAssignment = await db.insert(projectAssignmentTable, {
239
+ projectName: 'Backend API',
240
+ employeeRef: new Reference<Engineer>(engineerTable.name, engineer.id),
241
+ startDate: '2024-01-01',
242
+ });
243
+
244
+ const pmAssignment = await db.insert(projectAssignmentTable, {
245
+ projectName: 'Project Planning',
246
+ employeeRef: new Reference<ProjectManager>(projectManagerTable.name, projectManager.id),
247
+ startDate: '2024-01-10',
248
+ });
249
+
250
+ const designerAssignment = await db.insert(projectAssignmentTable, {
251
+ projectName: 'Website Redesign',
252
+ employeeRef: new Reference<Designer>(designerTable.name, designer.id),
253
+ startDate: '2024-01-15',
254
+ });
255
+
256
+ // Delete the engineer's and PM's assignments - should cascade delete both
257
+ const deleteQuery = new QueryBuilder<ProjectAssignment>(projectAssignmentTable.name).condition({
258
+ field: 'id',
259
+ operator: 'IN',
260
+ value: [engineerAssignment.id, pmAssignment.id],
261
+ });
262
+ const recordsDeleted = await db.delete(projectAssignmentTable, deleteQuery);
263
+
264
+ expect(recordsDeleted).toBe(2);
265
+
266
+ // Verify engineer and PM were deleted but designer remains
267
+ const remainingEngineers = await db.query(engineerTable, {});
268
+ expect(remainingEngineers.length).toBe(0); // Engineer should be deleted
269
+
270
+ const remainingPMs = await db.query(projectManagerTable, {});
271
+ expect(remainingPMs.length).toBe(0); // PM should be deleted
272
+
273
+ const remainingDesigners = await db.query(designerTable, {});
274
+ expect(remainingDesigners.length).toBe(1); // Designer should still exist
275
+ expect(remainingDesigners[0].id).toBe(designer.id);
276
+
277
+ // Only designer assignment should still exist
278
+ const remainingAssignments = await db.query(projectAssignmentTable, {});
279
+ expect(remainingAssignments.length).toBe(1);
280
+ expect(remainingAssignments[0].id).toBe(designerAssignment.id);
281
+ });
282
+
283
+ test('should allow changing table name when updating reference to new type', async () => {
284
+ // Create employees
285
+ const engineer = await db.insert(engineerTable, {
286
+ name: 'John Doe',
287
+ yearsOfExperience: 5,
288
+ });
289
+
290
+ const designer = await db.insert(designerTable, {
291
+ name: 'Jane Smith',
292
+ specialization: 'UI/UX',
293
+ });
294
+
295
+ // Create initial assignment to engineer
296
+ const assignment = await db.insert(projectAssignmentTable, {
297
+ projectName: 'Full Stack App',
298
+ employeeRef: new Reference(engineerTable.name, engineer.id),
299
+ startDate: '2024-01-01',
300
+ });
301
+
302
+ // Reassign to designer
303
+ await db.update(
304
+ projectAssignmentTable,
305
+ {
306
+ employeeRef: new Reference(designerTable.name, designer.id),
307
+ },
308
+ { id: assignment.id }
309
+ );
310
+
311
+ // Verify reassignment
312
+ const updatedAssignment = await db.get(projectAssignmentTable, { id: assignment.id });
313
+ expect(updatedAssignment.employeeRef).toBeDefined();
314
+ expect(updatedAssignment.employeeRef?._table).toBe(designerTable.name);
315
+ expect(updatedAssignment.employeeRef?._id).toBe(designer.id);
316
+ expect(updatedAssignment.employeeTableName).toBe(designerTable.name);
317
+ });
318
+
319
+ describe('DynamicReferenceTableNameColumn behavior', () => {
320
+ test('should handle defaultValue logic correctly', async () => {
321
+ // Test case 1: Reference column is populated
322
+ const engineer = await db.insert(engineerTable, {
323
+ name: 'John Doe',
324
+ yearsOfExperience: 5,
325
+ });
326
+
327
+ const assignment = await db.insert(projectAssignmentTable, {
328
+ projectName: 'Test Project',
329
+ employeeRef: new Reference(engineerTable.name, engineer.id),
330
+ startDate: '2024-01-01',
331
+ });
332
+
333
+ // Verify table name was set correctly from reference
334
+ expect(assignment.employeeTableName).toBe(engineerTable.name);
335
+
336
+ // Test case 2: Reference column is null
337
+ const assignmentNoRef = await db.insert(projectAssignmentTable, {
338
+ projectName: 'No Reference Project',
339
+ employeeRef: null,
340
+ startDate: '2024-01-01',
341
+ });
342
+
343
+ // Verify table name is null when reference is null
344
+ expect(assignmentNoRef.employeeTableName).toBeNull();
345
+
346
+ // Test case 3: Reference without table name should throw
347
+ await expect(
348
+ db.insert(projectAssignmentTable, {
349
+ projectName: 'Invalid Reference',
350
+ employeeRef: new Reference('', engineer.id), // Empty table name
351
+ startDate: '2024-01-01',
352
+ })
353
+ ).rejects.toThrow(/table name must be set in Reference object/);
354
+ });
355
+
356
+ test('should handle updateValue logic correctly', async () => {
357
+ // Create initial records
358
+ const engineer = await db.insert(engineerTable, {
359
+ name: 'John Doe',
360
+ yearsOfExperience: 5,
361
+ });
362
+
363
+ const designer = await db.insert(designerTable, {
364
+ name: 'Jane Smith',
365
+ specialization: 'UI/UX',
366
+ });
367
+
368
+ const assignment = await db.insert(projectAssignmentTable, {
369
+ projectName: 'Initial Project',
370
+ employeeRef: new Reference(engineerTable.name, engineer.id),
371
+ startDate: '2024-01-01',
372
+ });
373
+
374
+ // Test case 1: Update reference to new table
375
+ await db.update(
376
+ projectAssignmentTable,
377
+ {
378
+ employeeRef: new Reference(designerTable.name, designer.id),
379
+ },
380
+ { id: assignment.id }
381
+ );
382
+
383
+ const updatedAssignment = await db.get(projectAssignmentTable, { id: assignment.id });
384
+ expect(updatedAssignment.employeeTableName).toBe(designerTable.name);
385
+
386
+ // Test case 2: Update without changing reference
387
+ await db.update(
388
+ projectAssignmentTable,
389
+ {
390
+ projectName: 'Updated Project Name',
391
+ },
392
+ { id: assignment.id }
393
+ );
394
+
395
+ const unchangedAssignment = await db.get(projectAssignmentTable, { id: assignment.id });
396
+ expect(unchangedAssignment.employeeTableName).toBe(designerTable.name); // Should retain previous value
397
+
398
+ // Test case 3: Update with invalid reference should throw
399
+ await expect(
400
+ db.update(
401
+ projectAssignmentTable,
402
+ {
403
+ employeeRef: new Reference('', designer.id), // Empty table name
404
+ },
405
+ { id: assignment.id }
406
+ )
407
+ ).rejects.toThrow(/table name must be set in Reference object/);
408
+ });
409
+
410
+ test('should handle null references correctly', async () => {
411
+ // Create initial assignment with reference
412
+ const engineer = await db.insert(engineerTable, {
413
+ name: 'John Doe',
414
+ yearsOfExperience: 5,
415
+ });
416
+
417
+ const assignment = await db.insert(projectAssignmentTable, {
418
+ projectName: 'Initial Project',
419
+ employeeRef: new Reference(engineerTable.name, engineer.id),
420
+ startDate: '2024-01-01',
421
+ });
422
+
423
+ // Update to null reference
424
+ await db.update(
425
+ projectAssignmentTable,
426
+ {
427
+ employeeRef: null,
428
+ },
429
+ { id: assignment.id }
430
+ );
431
+
432
+ const nullRefAssignment = await db.get(projectAssignmentTable, { id: assignment.id });
433
+ expect(nullRefAssignment.employeeRef).toBeNull();
434
+ expect(nullRefAssignment.employeeTableName).toBe(engineerTable.name); // Should retain previous value
435
+ });
436
+
437
+ test('should handle custom defaultValue override for projectLead', async () => {
438
+ const engineer = await db.insert(engineerTable, {
439
+ name: 'John Doe',
440
+ yearsOfExperience: 5,
441
+ });
442
+
443
+ // Even with a reference provided, should use the custom default value
444
+ const assignment = await db.insert(projectAssignmentTable, {
445
+ projectName: 'Custom Default Test',
446
+ projectLeadRef: new Reference(engineerTable.name, engineer.id),
447
+ startDate: '2024-01-01',
448
+ });
449
+
450
+ // Should use our custom default value instead of the reference's table name
451
+ expect(assignment.projectLeadTableName).toBe('TEST_DEFAULT_VALUE');
452
+ });
453
+
454
+ test('should handle custom updateValue override for projectLead', async () => {
455
+ const engineer = await db.insert(engineerTable, {
456
+ name: 'John Doe',
457
+ yearsOfExperience: 5,
458
+ });
459
+
460
+ const designer = await db.insert(designerTable, {
461
+ name: 'Jane Smith',
462
+ specialization: 'UI/UX',
463
+ });
464
+
465
+ // Create initial assignment
466
+ const assignment = await db.insert(projectAssignmentTable, {
467
+ projectName: 'Initial Project',
468
+ projectLeadRef: new Reference(engineerTable.name, engineer.id),
469
+ startDate: '2024-01-01',
470
+ });
471
+
472
+ // Update the reference
473
+ await db.update(
474
+ projectAssignmentTable,
475
+ {
476
+ projectLeadRef: new Reference(designerTable.name, designer.id),
477
+ },
478
+ { id: assignment.id }
479
+ );
480
+
481
+ const updatedAssignment = await db.get(projectAssignmentTable, { id: assignment.id });
482
+ // Should use our custom update value instead of the new reference's table name
483
+ expect(updatedAssignment.projectLeadTableName).toBe('TEST_UPDATE_VALUE');
484
+ });
485
+ });
486
+ };
487
+ };