@peers-app/peers-device 0.0.1
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/dist/chunk-download-manager.d.ts +14 -0
- package/dist/chunk-download-manager.js +132 -0
- package/dist/chunk-download-manager.js.map +1 -0
- package/dist/chunk-download-manager.test.d.ts +1 -0
- package/dist/chunk-download-manager.test.js +166 -0
- package/dist/chunk-download-manager.test.js.map +1 -0
- package/dist/chunk-download.types.d.ts +15 -0
- package/dist/chunk-download.types.js +3 -0
- package/dist/chunk-download.types.js.map +1 -0
- package/dist/connection-manager/connection-manager.d.ts +32 -0
- package/dist/connection-manager/connection-manager.js +235 -0
- package/dist/connection-manager/connection-manager.js.map +1 -0
- package/dist/connection-manager/least-preferred-connection.d.ts +3 -0
- package/dist/connection-manager/least-preferred-connection.js +45 -0
- package/dist/connection-manager/least-preferred-connection.js.map +1 -0
- package/dist/connection-manager/least-preferred-connection.test.d.ts +1 -0
- package/dist/connection-manager/least-preferred-connection.test.js +300 -0
- package/dist/connection-manager/least-preferred-connection.test.js.map +1 -0
- package/dist/device-sync.d.ts +78 -0
- package/dist/device-sync.js +541 -0
- package/dist/device-sync.js.map +1 -0
- package/dist/device-sync.test.d.ts +1 -0
- package/dist/device-sync.test.js +1618 -0
- package/dist/device-sync.test.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/json-diff.d.ts +9 -0
- package/dist/json-diff.js +137 -0
- package/dist/json-diff.js.map +1 -0
- package/dist/local.data-source.d.ts +19 -0
- package/dist/local.data-source.js +146 -0
- package/dist/local.data-source.js.map +1 -0
- package/dist/local.data-source.test.d.ts +1 -0
- package/dist/local.data-source.test.js +68 -0
- package/dist/local.data-source.test.js.map +1 -0
- package/dist/main.d.ts +4 -0
- package/dist/main.js +138 -0
- package/dist/main.js.map +1 -0
- package/dist/packages.tracked-data-source.d.ts +10 -0
- package/dist/packages.tracked-data-source.js +28 -0
- package/dist/packages.tracked-data-source.js.map +1 -0
- package/dist/pvars.tracked-data-source.d.ts +5 -0
- package/dist/pvars.tracked-data-source.js +54 -0
- package/dist/pvars.tracked-data-source.js.map +1 -0
- package/dist/tracked-data-source.d.ts +46 -0
- package/dist/tracked-data-source.js +387 -0
- package/dist/tracked-data-source.js.map +1 -0
- package/dist/tracked-data-source.test.d.ts +1 -0
- package/dist/tracked-data-source.test.js +825 -0
- package/dist/tracked-data-source.test.js.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,825 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const lodash_1 = require("lodash");
|
|
4
|
+
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const tracked_data_source_1 = require("./tracked-data-source");
|
|
7
|
+
const local_data_source_1 = require("./local.data-source");
|
|
8
|
+
describe('tracked-table', () => {
|
|
9
|
+
const db = new local_data_source_1.DBLocal(':memory:');
|
|
10
|
+
const notesSchema = zod_1.z.object({
|
|
11
|
+
noteId: zod_1.z.string(),
|
|
12
|
+
title: zod_1.z.string(),
|
|
13
|
+
completed: zod_1.z.boolean().optional(),
|
|
14
|
+
number: zod_1.z.number().optional(),
|
|
15
|
+
string: zod_1.z.string().optional(),
|
|
16
|
+
date: zod_1.z.date().optional(),
|
|
17
|
+
});
|
|
18
|
+
const NotesMetaData = {
|
|
19
|
+
name: 'notes',
|
|
20
|
+
description: '',
|
|
21
|
+
primaryKeyName: 'noteId',
|
|
22
|
+
fields: (0, peers_sdk_1.schemaToFields)(notesSchema),
|
|
23
|
+
};
|
|
24
|
+
const sqlDataSource = new peers_sdk_1.SQLDataSource(db, NotesMetaData, notesSchema);
|
|
25
|
+
const mockDataContext = {
|
|
26
|
+
dataSourceFactory: () => sqlDataSource,
|
|
27
|
+
eventRegistry: {
|
|
28
|
+
getEmitter: () => ({
|
|
29
|
+
event: { subscribe: () => { } },
|
|
30
|
+
emit: () => { }
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const deps = {
|
|
35
|
+
dataSource: sqlDataSource,
|
|
36
|
+
eventRegistry: mockDataContext.eventRegistry
|
|
37
|
+
};
|
|
38
|
+
const Notes = new peers_sdk_1.Table(NotesMetaData, deps);
|
|
39
|
+
const changeTrackingTable = new peers_sdk_1.ChangeTrackingTable({ db });
|
|
40
|
+
const trackedTable = new tracked_data_source_1.TrackedDataSource(Notes, changeTrackingTable);
|
|
41
|
+
beforeEach(async () => {
|
|
42
|
+
// await Notes.dropTableIfExists();
|
|
43
|
+
await changeTrackingTable.dropTableIfExists();
|
|
44
|
+
await (0, peers_sdk_1.sleep)(200);
|
|
45
|
+
});
|
|
46
|
+
afterAll(async () => {
|
|
47
|
+
await (0, peers_sdk_1.sleep)(200);
|
|
48
|
+
});
|
|
49
|
+
it('should track changes when inserting data', async () => {
|
|
50
|
+
// Insert a new record
|
|
51
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: 'test note' });
|
|
52
|
+
expect(note.noteId).toBeDefined();
|
|
53
|
+
// Check that the change was recorded
|
|
54
|
+
const changes = await changeTrackingTable.list();
|
|
55
|
+
expect(changes.length).toBe(1);
|
|
56
|
+
expect(changes[0].tableName).toBe('notes');
|
|
57
|
+
const change = changes[0];
|
|
58
|
+
expect(change).toEqual({
|
|
59
|
+
changeId: expect.any(String),
|
|
60
|
+
changeType: 'insert',
|
|
61
|
+
timestamp: expect.any(Number),
|
|
62
|
+
timestampApplied: expect.any(Number),
|
|
63
|
+
tableName: 'notes',
|
|
64
|
+
recordId: note.noteId,
|
|
65
|
+
newRecord: note,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
it('should track changes when updating data', async () => {
|
|
69
|
+
// Insert a new record
|
|
70
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: 'test note' });
|
|
71
|
+
// Update the record
|
|
72
|
+
note.title = 'updated note';
|
|
73
|
+
note.completed = true;
|
|
74
|
+
await trackedTable.update(note);
|
|
75
|
+
// Check that both changes were recorded
|
|
76
|
+
const changes = await changeTrackingTable.list({ tableName: 'notes' }, { sortBy: ['timestamp'] });
|
|
77
|
+
expect(changes.length).toBe(2);
|
|
78
|
+
// Verify insert change
|
|
79
|
+
expect(changes[0]).toEqual({
|
|
80
|
+
changeId: expect.any(String),
|
|
81
|
+
changeType: 'insert',
|
|
82
|
+
timestamp: expect.any(Number),
|
|
83
|
+
timestampApplied: expect.any(Number),
|
|
84
|
+
tableName: 'notes',
|
|
85
|
+
recordId: note.noteId,
|
|
86
|
+
newRecord: expect.objectContaining({
|
|
87
|
+
title: 'test note',
|
|
88
|
+
noteId: note.noteId
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
// Verify update change
|
|
92
|
+
const updateChange = changes[1];
|
|
93
|
+
expect(updateChange).toEqual({
|
|
94
|
+
changeId: expect.any(String),
|
|
95
|
+
changeType: 'update',
|
|
96
|
+
timestamp: expect.any(Number),
|
|
97
|
+
timestampApplied: expect.any(Number),
|
|
98
|
+
tableName: 'notes',
|
|
99
|
+
recordId: note.noteId,
|
|
100
|
+
newRecord: expect.objectContaining({
|
|
101
|
+
title: 'updated note',
|
|
102
|
+
noteId: note.noteId,
|
|
103
|
+
completed: true
|
|
104
|
+
}),
|
|
105
|
+
jsonDiff: expect.any(Array),
|
|
106
|
+
});
|
|
107
|
+
// TypeScript needs this cast for type narrowing
|
|
108
|
+
expect(updateChange.jsonDiff.length).toBeGreaterThan(0);
|
|
109
|
+
});
|
|
110
|
+
it('should track changes when deleting data', async () => {
|
|
111
|
+
// Insert a new record
|
|
112
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: 'test note' });
|
|
113
|
+
// Delete the record
|
|
114
|
+
await trackedTable.delete(note);
|
|
115
|
+
// Check that both changes were recorded
|
|
116
|
+
const changes = await changeTrackingTable.list({ tableName: 'notes' }, { sortBy: ['timestamp'] });
|
|
117
|
+
expect(changes.length).toBe(2);
|
|
118
|
+
// Get the changes in the right order
|
|
119
|
+
const insertChange = changes.find(c => c.changeType === 'insert');
|
|
120
|
+
const deleteChange = changes.find(c => c.changeType === 'delete');
|
|
121
|
+
expect(insertChange).toEqual({
|
|
122
|
+
changeId: expect.any(String),
|
|
123
|
+
changeType: 'insert',
|
|
124
|
+
timestamp: expect.any(Number),
|
|
125
|
+
timestampApplied: expect.any(Number),
|
|
126
|
+
tableName: 'notes',
|
|
127
|
+
recordId: note.noteId,
|
|
128
|
+
newRecord: expect.objectContaining({
|
|
129
|
+
title: 'test note',
|
|
130
|
+
noteId: note.noteId
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
// Verify delete change
|
|
134
|
+
expect(deleteChange).toEqual({
|
|
135
|
+
changeId: expect.any(String),
|
|
136
|
+
changeType: 'delete',
|
|
137
|
+
timestamp: expect.any(Number),
|
|
138
|
+
timestampApplied: expect.any(Number),
|
|
139
|
+
tableName: 'notes',
|
|
140
|
+
recordId: note.noteId,
|
|
141
|
+
oldRecord: expect.objectContaining({
|
|
142
|
+
title: 'test note',
|
|
143
|
+
noteId: note.noteId
|
|
144
|
+
}),
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
it('should track changes when using save method for new records', async () => {
|
|
148
|
+
// Save a new record
|
|
149
|
+
const note = await trackedTable.save({ noteId: (0, peers_sdk_1.newid)(), title: 'test note' });
|
|
150
|
+
// Check that the change was recorded
|
|
151
|
+
const changes = await changeTrackingTable.list();
|
|
152
|
+
expect(changes.length).toBe(1);
|
|
153
|
+
expect(changes[0]).toEqual({
|
|
154
|
+
changeId: expect.any(String),
|
|
155
|
+
changeType: 'insert',
|
|
156
|
+
timestamp: expect.any(Number),
|
|
157
|
+
timestampApplied: expect.any(Number),
|
|
158
|
+
tableName: 'notes',
|
|
159
|
+
recordId: note.noteId,
|
|
160
|
+
newRecord: expect.objectContaining({
|
|
161
|
+
title: 'test note',
|
|
162
|
+
noteId: note.noteId
|
|
163
|
+
}),
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
it('should track changes when using save method for existing records', async () => {
|
|
167
|
+
// Save a new record
|
|
168
|
+
const note = await trackedTable.save({ noteId: (0, peers_sdk_1.newid)(), title: 'test note' });
|
|
169
|
+
// Update using save
|
|
170
|
+
note.title = 'updated note';
|
|
171
|
+
await trackedTable.save(note);
|
|
172
|
+
// Check that both changes were recorded
|
|
173
|
+
const changes = await changeTrackingTable.list({ tableName: 'notes' }, { sortBy: ['timestamp'] });
|
|
174
|
+
expect(changes.length).toBe(2);
|
|
175
|
+
// Verify first change (insert)
|
|
176
|
+
expect(changes[0]).toEqual({
|
|
177
|
+
changeId: expect.any(String),
|
|
178
|
+
changeType: 'insert',
|
|
179
|
+
timestamp: expect.any(Number),
|
|
180
|
+
timestampApplied: expect.any(Number),
|
|
181
|
+
tableName: 'notes',
|
|
182
|
+
recordId: note.noteId,
|
|
183
|
+
newRecord: expect.objectContaining({
|
|
184
|
+
title: 'test note',
|
|
185
|
+
noteId: note.noteId
|
|
186
|
+
}),
|
|
187
|
+
});
|
|
188
|
+
// Verify second change (update)
|
|
189
|
+
const updateChange = changes[1];
|
|
190
|
+
expect(updateChange).toEqual({
|
|
191
|
+
changeId: expect.any(String),
|
|
192
|
+
changeType: 'update',
|
|
193
|
+
timestamp: expect.any(Number),
|
|
194
|
+
timestampApplied: expect.any(Number),
|
|
195
|
+
tableName: 'notes',
|
|
196
|
+
recordId: note.noteId,
|
|
197
|
+
newRecord: expect.objectContaining({
|
|
198
|
+
title: 'updated note',
|
|
199
|
+
noteId: note.noteId
|
|
200
|
+
}),
|
|
201
|
+
jsonDiff: expect.any(Array),
|
|
202
|
+
});
|
|
203
|
+
// TypeScript needs this cast for type narrowing
|
|
204
|
+
expect(updateChange.jsonDiff.length).toBeGreaterThan(0);
|
|
205
|
+
});
|
|
206
|
+
it('should not track read operations', async () => {
|
|
207
|
+
// Insert a record
|
|
208
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: 'test note' });
|
|
209
|
+
// Perform read operations
|
|
210
|
+
await trackedTable.get(note.noteId);
|
|
211
|
+
await trackedTable.list();
|
|
212
|
+
await trackedTable.cursor();
|
|
213
|
+
// Only the insert should be recorded
|
|
214
|
+
const changes = await changeTrackingTable.list();
|
|
215
|
+
expect(changes.length).toBe(1);
|
|
216
|
+
expect(changes[0].changeType).toBe('insert');
|
|
217
|
+
});
|
|
218
|
+
it('should handle snapshot changes', async () => {
|
|
219
|
+
const noteId = (0, peers_sdk_1.newid)();
|
|
220
|
+
// Create a snapshot change directly
|
|
221
|
+
const snapshotChange = {
|
|
222
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
223
|
+
changeType: 'snapshot',
|
|
224
|
+
timestamp: Date.now(),
|
|
225
|
+
timestampApplied: Date.now(),
|
|
226
|
+
tableName: 'notes',
|
|
227
|
+
recordId: noteId,
|
|
228
|
+
newRecord: { noteId, title: 'snapshot note', completed: false }
|
|
229
|
+
};
|
|
230
|
+
// Apply the snapshot change
|
|
231
|
+
await trackedTable.applyChanges([snapshotChange]);
|
|
232
|
+
// Verify the record was created
|
|
233
|
+
const note = await trackedTable.get(noteId);
|
|
234
|
+
expect(note).toEqual({ noteId, title: 'snapshot note', completed: false });
|
|
235
|
+
// Verify the snapshot change was recorded
|
|
236
|
+
const changes = await changeTrackingTable.list({ recordId: noteId });
|
|
237
|
+
expect(changes.length).toBe(1);
|
|
238
|
+
expect(changes[0]).toEqual({
|
|
239
|
+
changeId: snapshotChange.changeId,
|
|
240
|
+
changeType: 'snapshot',
|
|
241
|
+
timestamp: snapshotChange.timestamp,
|
|
242
|
+
timestampApplied: expect.any(Number),
|
|
243
|
+
tableName: 'notes',
|
|
244
|
+
recordId: noteId,
|
|
245
|
+
newRecord: expect.objectContaining({
|
|
246
|
+
noteId,
|
|
247
|
+
title: 'snapshot note',
|
|
248
|
+
completed: false
|
|
249
|
+
})
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
it('should list changes for a specific table', async () => {
|
|
253
|
+
// Insert a record
|
|
254
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: 'test note' });
|
|
255
|
+
// Update the record
|
|
256
|
+
note.title = 'updated note';
|
|
257
|
+
await trackedTable.update(note);
|
|
258
|
+
// Delete the record
|
|
259
|
+
await trackedTable.delete(note);
|
|
260
|
+
// Use listChanges to get changes
|
|
261
|
+
const changes = await trackedTable.listChanges({}, { sortBy: ['timestamp'] });
|
|
262
|
+
// Verify we have 3 changes (insert, update, delete)
|
|
263
|
+
expect(changes.length).toBe(3);
|
|
264
|
+
expect(changes[0].changeType).toBe('insert');
|
|
265
|
+
expect(changes[1].changeType).toBe('update');
|
|
266
|
+
expect(changes[2].changeType).toBe('delete');
|
|
267
|
+
// Test filtering by changeType
|
|
268
|
+
const insertChanges = await trackedTable.listChanges({ changeType: 'insert' });
|
|
269
|
+
expect(insertChanges.length).toBe(1);
|
|
270
|
+
expect(insertChanges[0].changeType).toBe('insert');
|
|
271
|
+
// Test filtering by recordId
|
|
272
|
+
const recordChanges = await trackedTable.listChanges({ recordId: note.noteId }, { sortBy: ['timestamp'] });
|
|
273
|
+
expect(recordChanges.length).toBe(3);
|
|
274
|
+
expect(recordChanges.map(c => c.changeType)).toEqual(['insert', 'update', 'delete']);
|
|
275
|
+
});
|
|
276
|
+
it('should apply new changes to a table and remove old, irrelevant changes', async () => {
|
|
277
|
+
// Create a change to insert a record
|
|
278
|
+
const noteId = (0, peers_sdk_1.newid)();
|
|
279
|
+
const insertChange = {
|
|
280
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
281
|
+
changeType: 'insert',
|
|
282
|
+
timestamp: Date.now(),
|
|
283
|
+
timestampApplied: Date.now(),
|
|
284
|
+
tableName: 'notes',
|
|
285
|
+
recordId: noteId,
|
|
286
|
+
newRecord: { noteId, title: 'test note' }
|
|
287
|
+
};
|
|
288
|
+
// Apply the insert change
|
|
289
|
+
await trackedTable.applyChanges([insertChange]);
|
|
290
|
+
// Verify the record was inserted
|
|
291
|
+
const note = await trackedTable.get(noteId);
|
|
292
|
+
expect(note).toEqual({ noteId, title: 'test note' });
|
|
293
|
+
// Create an update change
|
|
294
|
+
const updateChange = {
|
|
295
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
296
|
+
changeType: 'update',
|
|
297
|
+
timestamp: insertChange.timestamp + 1,
|
|
298
|
+
timestampApplied: insertChange.timestamp + 1,
|
|
299
|
+
tableName: 'notes',
|
|
300
|
+
recordId: noteId,
|
|
301
|
+
oldRecord: { noteId, title: 'test note' },
|
|
302
|
+
newRecord: { noteId, title: 'updated note', completed: true },
|
|
303
|
+
jsonDiff: [{ op: 'replace', path: '/title', value: 'updated note' },
|
|
304
|
+
{ op: 'add', path: '/completed', value: true }]
|
|
305
|
+
};
|
|
306
|
+
// Apply the update change
|
|
307
|
+
await trackedTable.applyChanges([updateChange]);
|
|
308
|
+
// Verify the record was updated
|
|
309
|
+
const updatedNote = await trackedTable.get(noteId);
|
|
310
|
+
expect(updatedNote).toEqual({ noteId, title: 'updated note', completed: true });
|
|
311
|
+
// Verify the changes were recorded in the change tracking table
|
|
312
|
+
let changes = await changeTrackingTable.list({ recordId: noteId }, { sortBy: ['timestamp'] });
|
|
313
|
+
expect(changes.length).toBe(2);
|
|
314
|
+
expect(changes.map(c => c.changeType)).toEqual(['insert', 'update']);
|
|
315
|
+
// Create a delete change
|
|
316
|
+
const deleteChange = {
|
|
317
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
318
|
+
changeType: 'delete',
|
|
319
|
+
timestamp: insertChange.timestamp + 2,
|
|
320
|
+
timestampApplied: insertChange.timestamp + 2,
|
|
321
|
+
tableName: 'notes',
|
|
322
|
+
recordId: noteId,
|
|
323
|
+
oldRecord: { noteId, title: 'updated note', completed: true },
|
|
324
|
+
};
|
|
325
|
+
// Apply the delete change
|
|
326
|
+
await trackedTable.applyChanges([deleteChange]);
|
|
327
|
+
// Verify the record was deleted
|
|
328
|
+
const deletedNote = await trackedTable.get(noteId);
|
|
329
|
+
expect(deletedNote).toBeUndefined();
|
|
330
|
+
// Verify the delete change was recorded and removed older (and no longer applicable changes)
|
|
331
|
+
changes = await changeTrackingTable.list({ recordId: noteId }, { sortBy: ['timestamp'] });
|
|
332
|
+
expect(changes.length).toBe(1);
|
|
333
|
+
expect(changes.map(c => c.changeType)).toEqual(['delete']);
|
|
334
|
+
// reapply the insert change
|
|
335
|
+
// Create a change to insert a record
|
|
336
|
+
const insertChange2 = {
|
|
337
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
338
|
+
changeType: 'insert',
|
|
339
|
+
timestamp: insertChange.timestamp + 3,
|
|
340
|
+
timestampApplied: insertChange.timestamp + 3,
|
|
341
|
+
tableName: 'notes',
|
|
342
|
+
recordId: noteId,
|
|
343
|
+
newRecord: { noteId, title: 'reinsert note' }
|
|
344
|
+
};
|
|
345
|
+
// Apply the insert change
|
|
346
|
+
await trackedTable.applyChanges([insertChange2]);
|
|
347
|
+
// Verify the insert change was recorded and removed older (and no longer applicable delete change)
|
|
348
|
+
changes = await changeTrackingTable.list({ recordId: noteId }, { sortBy: ['timestamp'] });
|
|
349
|
+
expect(changes.length).toBe(1);
|
|
350
|
+
expect(changes.map(c => c.changeType)).toEqual(['insert']);
|
|
351
|
+
});
|
|
352
|
+
it('should not reapply changes that have already been applied', async () => {
|
|
353
|
+
// Create a test change
|
|
354
|
+
const noteId = (0, peers_sdk_1.newid)();
|
|
355
|
+
const changeId = (0, peers_sdk_1.newid)();
|
|
356
|
+
const insertChange = {
|
|
357
|
+
changeId,
|
|
358
|
+
changeType: 'insert',
|
|
359
|
+
timestamp: Date.now(),
|
|
360
|
+
timestampApplied: Date.now(),
|
|
361
|
+
tableName: 'notes',
|
|
362
|
+
recordId: noteId,
|
|
363
|
+
newRecord: { noteId, title: 'duplicate test' }
|
|
364
|
+
};
|
|
365
|
+
// Apply the change
|
|
366
|
+
await trackedTable.applyChanges([insertChange]);
|
|
367
|
+
// Verify the record was inserted
|
|
368
|
+
const note = await trackedTable.get(noteId);
|
|
369
|
+
expect(note).toEqual({ noteId, title: 'duplicate test' });
|
|
370
|
+
// Try to apply the same change again
|
|
371
|
+
await trackedTable.applyChanges([insertChange]);
|
|
372
|
+
// Verify the change was only recorded once
|
|
373
|
+
const changes = await changeTrackingTable.list({ changeId });
|
|
374
|
+
expect(changes.length).toBe(1);
|
|
375
|
+
});
|
|
376
|
+
it('should apply changes in timestamp order', async () => {
|
|
377
|
+
const noteId = (0, peers_sdk_1.newid)();
|
|
378
|
+
const now = Date.now();
|
|
379
|
+
// Create changes with out-of-order timestamps
|
|
380
|
+
const changes = [
|
|
381
|
+
{
|
|
382
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
383
|
+
changeType: 'update',
|
|
384
|
+
timestamp: now + 200,
|
|
385
|
+
timestampApplied: now + 200,
|
|
386
|
+
tableName: 'notes',
|
|
387
|
+
recordId: noteId,
|
|
388
|
+
oldRecord: { noteId, title: 'second title' },
|
|
389
|
+
newRecord: { noteId, title: 'final title' },
|
|
390
|
+
jsonDiff: [{ op: 'replace', path: '/title', value: 'final title' }]
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
394
|
+
changeType: 'insert',
|
|
395
|
+
timestamp: now,
|
|
396
|
+
timestampApplied: now,
|
|
397
|
+
tableName: 'notes',
|
|
398
|
+
recordId: noteId,
|
|
399
|
+
newRecord: { noteId, title: 'initial title' }
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
403
|
+
changeType: 'update',
|
|
404
|
+
timestamp: now + 100,
|
|
405
|
+
timestampApplied: now + 100,
|
|
406
|
+
tableName: 'notes',
|
|
407
|
+
recordId: noteId,
|
|
408
|
+
oldRecord: { noteId, title: 'initial title' },
|
|
409
|
+
newRecord: { noteId, title: 'second title' },
|
|
410
|
+
jsonDiff: [{ op: 'replace', path: '/title', value: 'second title' }]
|
|
411
|
+
}
|
|
412
|
+
];
|
|
413
|
+
// Store the changeIds for verification later
|
|
414
|
+
const changeIds = {
|
|
415
|
+
insert: changes[1].changeId,
|
|
416
|
+
update1: changes[2].changeId,
|
|
417
|
+
update2: changes[0].changeId
|
|
418
|
+
};
|
|
419
|
+
// Apply the out-of-order changes
|
|
420
|
+
await trackedTable.applyChanges(changes);
|
|
421
|
+
// Verify the final state reflects the changes applied in timestamp order
|
|
422
|
+
const note = await trackedTable.get(noteId);
|
|
423
|
+
expect(note).toEqual({ noteId, title: 'final title' });
|
|
424
|
+
// Verify the changes were recorded in order
|
|
425
|
+
const recordedChanges = await changeTrackingTable.list({ recordId: noteId }, { sortBy: ['timestamp'] });
|
|
426
|
+
expect(recordedChanges.length).toBe(3);
|
|
427
|
+
expect(recordedChanges.map(c => c.changeId)).toEqual([
|
|
428
|
+
changeIds.insert,
|
|
429
|
+
changeIds.update1,
|
|
430
|
+
changeIds.update2
|
|
431
|
+
]);
|
|
432
|
+
});
|
|
433
|
+
it('should require a restore operation to bring a record back after being deleted', async () => {
|
|
434
|
+
// Create a change to insert a record
|
|
435
|
+
const noteId = (0, peers_sdk_1.newid)();
|
|
436
|
+
const insertChange = {
|
|
437
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
438
|
+
changeType: 'insert',
|
|
439
|
+
timestamp: Date.now(),
|
|
440
|
+
timestampApplied: Date.now(),
|
|
441
|
+
tableName: 'notes',
|
|
442
|
+
recordId: noteId,
|
|
443
|
+
newRecord: { noteId, title: 'test note' }
|
|
444
|
+
};
|
|
445
|
+
// Apply the insert change
|
|
446
|
+
await trackedTable.applyChanges([insertChange]);
|
|
447
|
+
// Verify the record was inserted
|
|
448
|
+
const note = await trackedTable.get(noteId);
|
|
449
|
+
expect(note).toEqual({ noteId, title: 'test note' });
|
|
450
|
+
// Create a delete change
|
|
451
|
+
const deleteChange = {
|
|
452
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
453
|
+
changeType: 'delete',
|
|
454
|
+
timestamp: insertChange.timestamp + 1,
|
|
455
|
+
timestampApplied: insertChange.timestamp + 1,
|
|
456
|
+
tableName: 'notes',
|
|
457
|
+
recordId: noteId,
|
|
458
|
+
oldRecord: { noteId, title: 'test note' }
|
|
459
|
+
};
|
|
460
|
+
// Apply the delete change
|
|
461
|
+
await trackedTable.applyChanges([deleteChange]);
|
|
462
|
+
// Verify the record was deleted
|
|
463
|
+
const deletedNote = await trackedTable.get(noteId);
|
|
464
|
+
expect(deletedNote).toBeUndefined();
|
|
465
|
+
// Create an update change
|
|
466
|
+
const updateChange = {
|
|
467
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
468
|
+
changeType: 'update',
|
|
469
|
+
timestamp: insertChange.timestamp + 1,
|
|
470
|
+
timestampApplied: insertChange.timestamp + 1,
|
|
471
|
+
tableName: 'notes',
|
|
472
|
+
recordId: noteId,
|
|
473
|
+
oldRecord: { noteId, title: 'test note' },
|
|
474
|
+
newRecord: { noteId, title: 'updated note', completed: true },
|
|
475
|
+
jsonDiff: [{ op: 'replace', path: '/title', value: 'updated note' },
|
|
476
|
+
{ op: 'add', path: '/completed', value: true }]
|
|
477
|
+
};
|
|
478
|
+
// Apply the update change
|
|
479
|
+
await trackedTable.applyChanges([updateChange]);
|
|
480
|
+
// Verify the record was not updated
|
|
481
|
+
const updatedNote = await trackedTable.get(noteId);
|
|
482
|
+
expect(updatedNote).toBeUndefined();
|
|
483
|
+
// Create a restore change
|
|
484
|
+
const restoreChange = {
|
|
485
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
486
|
+
changeType: 'restore',
|
|
487
|
+
timestamp: insertChange.timestamp + 2,
|
|
488
|
+
timestampApplied: insertChange.timestamp + 2,
|
|
489
|
+
tableName: 'notes',
|
|
490
|
+
recordId: noteId,
|
|
491
|
+
newRecord: { noteId, title: 'restored note' }
|
|
492
|
+
};
|
|
493
|
+
// Apply the restore change
|
|
494
|
+
await trackedTable.applyChanges([restoreChange]);
|
|
495
|
+
// Verify the record was restored
|
|
496
|
+
const restoredNote = await trackedTable.get(noteId);
|
|
497
|
+
expect(restoredNote).toEqual({ noteId, title: 'restored note' });
|
|
498
|
+
});
|
|
499
|
+
it('should correctly and efficiently apply new changes to records with a very large amount of existing writes', async () => {
|
|
500
|
+
const db2 = new local_data_source_1.DBLocal(':memory:');
|
|
501
|
+
const mockDataContext2 = {
|
|
502
|
+
dataSourceFactory: () => new peers_sdk_1.SQLDataSource(db2, NotesMetaData, notesSchema),
|
|
503
|
+
eventRegistry: {
|
|
504
|
+
getEmitter: () => ({
|
|
505
|
+
event: { subscribe: () => { } },
|
|
506
|
+
emit: () => { }
|
|
507
|
+
})
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
const deps2 = {
|
|
511
|
+
dataSource: new (require("@peers-app/peers-sdk")).SQLDataSource(db2, NotesMetaData, notesSchema),
|
|
512
|
+
eventRegistry: mockDataContext2.eventRegistry,
|
|
513
|
+
};
|
|
514
|
+
const Notes2 = new peers_sdk_1.Table(NotesMetaData, deps2);
|
|
515
|
+
const changeTrackingTable2 = new peers_sdk_1.ChangeTrackingTable({ db: db2 });
|
|
516
|
+
const trackedTable2 = new tracked_data_source_1.TrackedDataSource(Notes2, changeTrackingTable2);
|
|
517
|
+
// Create a record with a large number of changes
|
|
518
|
+
const noteId = (0, peers_sdk_1.newid)();
|
|
519
|
+
const note = { noteId, title: 'test note' };
|
|
520
|
+
const checkpointCount = 20;
|
|
521
|
+
const writesBetweenCheckpointsCount = 20;
|
|
522
|
+
let lastCheckpointTimestamp = 0;
|
|
523
|
+
let note1Deleted = false;
|
|
524
|
+
let note2Deleted = false;
|
|
525
|
+
async function save1() {
|
|
526
|
+
if (note1Deleted) {
|
|
527
|
+
await trackedTable.save(note, { restoreIfDeleted: true });
|
|
528
|
+
note1Deleted = false;
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
await trackedTable.save(note);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
async function save2() {
|
|
535
|
+
if (note2Deleted) {
|
|
536
|
+
await trackedTable2.save(note, { restoreIfDeleted: true });
|
|
537
|
+
note2Deleted = false;
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
await trackedTable2.save(note);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
for (let i = 0; i < checkpointCount; i++) {
|
|
544
|
+
for (let j = 0; j < writesBetweenCheckpointsCount; j++) {
|
|
545
|
+
note.title = `update ${i} ${j}`;
|
|
546
|
+
note.completed = (0, lodash_1.random)(0, 10) === 0;
|
|
547
|
+
if ((0, lodash_1.random)(0, 10) === 0) {
|
|
548
|
+
note.number = (0, lodash_1.random)(0, 1000);
|
|
549
|
+
}
|
|
550
|
+
if ((0, lodash_1.random)(0, 2) === 0) {
|
|
551
|
+
note.string = (note.string || '') + note.title;
|
|
552
|
+
note.string = (note.string || '') + note.title;
|
|
553
|
+
const replaceStartI = (0, lodash_1.random)(0, note.string.length - 1);
|
|
554
|
+
const replaceEndI = (0, lodash_1.random)(replaceStartI, note.string.length - 1);
|
|
555
|
+
note.string = note.string.replace(note.string?.substring(replaceStartI, replaceEndI), note.title);
|
|
556
|
+
}
|
|
557
|
+
if ((0, lodash_1.random)(0, 4) > 0) {
|
|
558
|
+
note.date = new Date();
|
|
559
|
+
}
|
|
560
|
+
if ((0, lodash_1.random)(0, 10) > 0 || j === writesBetweenCheckpointsCount - 1) {
|
|
561
|
+
await save1();
|
|
562
|
+
}
|
|
563
|
+
if ((0, lodash_1.random)(0, 10) > 0) {
|
|
564
|
+
await save2();
|
|
565
|
+
}
|
|
566
|
+
// random delete
|
|
567
|
+
if ((0, lodash_1.random)(0, 10) === 0) {
|
|
568
|
+
await trackedTable.delete(note);
|
|
569
|
+
note1Deleted = true;
|
|
570
|
+
}
|
|
571
|
+
if ((0, lodash_1.random)(0, 10) === 0) {
|
|
572
|
+
await trackedTable2.delete(note);
|
|
573
|
+
note2Deleted = true;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (note1Deleted !== note2Deleted) {
|
|
577
|
+
note.title = `checkpoint ${i}`;
|
|
578
|
+
await save1();
|
|
579
|
+
await save2();
|
|
580
|
+
note1Deleted = false;
|
|
581
|
+
note2Deleted = false;
|
|
582
|
+
}
|
|
583
|
+
let changes = await trackedTable.listChanges({ timestamp: { $gte: lastCheckpointTimestamp } }, { sortBy: ['timestamp'] });
|
|
584
|
+
let changes2 = await trackedTable2.listChanges({ timestamp: { $gte: lastCheckpointTimestamp } }, { sortBy: ['timestamp'] });
|
|
585
|
+
await trackedTable.applyChanges(changes2);
|
|
586
|
+
await trackedTable2.applyChanges(changes);
|
|
587
|
+
changes = await trackedTable.listChanges({ timestamp: { $gte: lastCheckpointTimestamp } }, { sortBy: ['timestamp'] });
|
|
588
|
+
changes2 = await trackedTable2.listChanges({ timestamp: { $gte: lastCheckpointTimestamp } }, { sortBy: ['timestamp'] });
|
|
589
|
+
expect(changes.map(c => c.changeId)).toEqual(changes2.map(c => c.changeId));
|
|
590
|
+
lastCheckpointTimestamp = changes[changes.length - 1].timestamp;
|
|
591
|
+
}
|
|
592
|
+
const changes = await changeTrackingTable.list({ recordId: noteId }, { sortBy: ['timestamp'] });
|
|
593
|
+
const changes2 = await changeTrackingTable2.list({ recordId: noteId }, { sortBy: ['timestamp'] });
|
|
594
|
+
expect(changes.map(c => c.changeId)).toEqual(changes2.map(c => c.changeId));
|
|
595
|
+
const dbNote = await Notes.get(noteId);
|
|
596
|
+
expect(dbNote).toEqual(note);
|
|
597
|
+
const dbNote2 = await Notes2.get(noteId);
|
|
598
|
+
expect(dbNote2).toEqual(note);
|
|
599
|
+
});
|
|
600
|
+
it("should be able to compact changes to remain performant", async () => {
|
|
601
|
+
const db = new local_data_source_1.DBLocal(':memory:');
|
|
602
|
+
const mockDataContext3 = {
|
|
603
|
+
dataSourceFactory: () => new peers_sdk_1.SQLDataSource(db, NotesMetaData, notesSchema),
|
|
604
|
+
eventRegistry: {
|
|
605
|
+
getEmitter: () => ({
|
|
606
|
+
event: { subscribe: () => { } },
|
|
607
|
+
emit: () => { }
|
|
608
|
+
})
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
const deps3 = {
|
|
612
|
+
dataSource: new (require("@peers-app/peers-sdk")).SQLDataSource(db, NotesMetaData, notesSchema),
|
|
613
|
+
eventRegistry: mockDataContext3.eventRegistry,
|
|
614
|
+
};
|
|
615
|
+
const NotesTable = new peers_sdk_1.Table(NotesMetaData, deps3);
|
|
616
|
+
const changeTrackingTable = new peers_sdk_1.ChangeTrackingTable({ db: db });
|
|
617
|
+
const trackedTable = new tracked_data_source_1.TrackedDataSource(NotesTable, changeTrackingTable);
|
|
618
|
+
// Create a record with a large number of updates
|
|
619
|
+
const noteId = (0, peers_sdk_1.newid)();
|
|
620
|
+
const note = { noteId, title: 'test note' };
|
|
621
|
+
const writesCount = 200;
|
|
622
|
+
for (let i = 0; i < writesCount; i++) {
|
|
623
|
+
note.title = `update ${i}`;
|
|
624
|
+
note.completed = (0, lodash_1.random)(0, 10) === 0;
|
|
625
|
+
if ((0, lodash_1.random)(0, 10) === 0) {
|
|
626
|
+
note.number = (0, lodash_1.random)(0, 1000);
|
|
627
|
+
}
|
|
628
|
+
if ((0, lodash_1.random)(0, 2) === 0) {
|
|
629
|
+
note.string = (note.string || '') + note.title;
|
|
630
|
+
note.string = (note.string || '') + note.title;
|
|
631
|
+
const replaceStartI = (0, lodash_1.random)(0, note.string.length - 1);
|
|
632
|
+
const replaceEndI = (0, lodash_1.random)(replaceStartI, note.string.length - 1);
|
|
633
|
+
note.string = note.string.replace(note.string?.substring(replaceStartI, replaceEndI), note.title);
|
|
634
|
+
}
|
|
635
|
+
if ((0, lodash_1.random)(0, 4) > 0) {
|
|
636
|
+
note.date = new Date();
|
|
637
|
+
}
|
|
638
|
+
await trackedTable.save(note);
|
|
639
|
+
}
|
|
640
|
+
let dbNote = await NotesTable.get(noteId);
|
|
641
|
+
expect(dbNote).toEqual(note);
|
|
642
|
+
const changesBeforeCompact = await changeTrackingTable.list({ recordId: noteId }, { sortBy: ['timestamp'] });
|
|
643
|
+
expect(changesBeforeCompact.length).toBeGreaterThan(100);
|
|
644
|
+
const tenthLastChange = changesBeforeCompact[changesBeforeCompact.length - 10];
|
|
645
|
+
await trackedTable.compact(tenthLastChange.timestamp);
|
|
646
|
+
const changesAfterCompact = await changeTrackingTable.list({ recordId: noteId }, { sortBy: ['timestamp'] });
|
|
647
|
+
expect(changesAfterCompact.length).toEqual(11);
|
|
648
|
+
dbNote = await NotesTable.get(noteId);
|
|
649
|
+
expect(dbNote).toEqual(note);
|
|
650
|
+
});
|
|
651
|
+
it('should handle restore operation correctly', async () => {
|
|
652
|
+
const noteId = (0, peers_sdk_1.newid)();
|
|
653
|
+
// Insert and then delete a record
|
|
654
|
+
await trackedTable.insert({ noteId, title: 'original note' });
|
|
655
|
+
await trackedTable.delete(noteId);
|
|
656
|
+
// Verify it's deleted
|
|
657
|
+
const deletedNote = await trackedTable.get(noteId);
|
|
658
|
+
expect(deletedNote).toBeUndefined();
|
|
659
|
+
// Restore the record
|
|
660
|
+
const restoredNote = await trackedTable.restore({ noteId, title: 'restored note' });
|
|
661
|
+
expect(restoredNote).toEqual({ noteId, title: 'restored note' });
|
|
662
|
+
// Verify it's restored
|
|
663
|
+
const retrievedNote = await trackedTable.get(noteId);
|
|
664
|
+
expect(retrievedNote).toEqual({ noteId, title: 'restored note' });
|
|
665
|
+
// Verify restore change was tracked
|
|
666
|
+
const changes = await trackedTable.listChanges({ recordId: noteId, changeType: 'restore' });
|
|
667
|
+
expect(changes.length).toBe(1);
|
|
668
|
+
expect(changes[0].changeType).toBe('restore');
|
|
669
|
+
});
|
|
670
|
+
it('should handle restore without recordId provided', async () => {
|
|
671
|
+
await expect(trackedTable.restore({ title: 'no id' })).rejects.toThrow('No recordId provided to restore');
|
|
672
|
+
});
|
|
673
|
+
it('should handle insert with existing deleted record', async () => {
|
|
674
|
+
const noteId = (0, peers_sdk_1.newid)();
|
|
675
|
+
// Insert and then delete a record
|
|
676
|
+
await trackedTable.insert({ noteId, title: 'original note' });
|
|
677
|
+
await trackedTable.delete(noteId);
|
|
678
|
+
// Try to insert with same ID - should fail
|
|
679
|
+
await expect(trackedTable.insert({ noteId, title: 'new note' })).rejects.toThrow('because it has been deleted. Use restore instead');
|
|
680
|
+
});
|
|
681
|
+
it('should handle save with deleted record without restore flag', async () => {
|
|
682
|
+
const noteId = (0, peers_sdk_1.newid)();
|
|
683
|
+
// Insert and then delete a record
|
|
684
|
+
await trackedTable.insert({ noteId, title: 'original note' });
|
|
685
|
+
await trackedTable.delete(noteId);
|
|
686
|
+
// Try to save without restore flag - should fail
|
|
687
|
+
await expect(trackedTable.save({ noteId, title: 'new note' })).rejects.toThrow('because it has been deleted. Use restore instead');
|
|
688
|
+
});
|
|
689
|
+
it('should handle update with missing record', async () => {
|
|
690
|
+
const noteId = (0, peers_sdk_1.newid)();
|
|
691
|
+
// Try to update non-existent record
|
|
692
|
+
await expect(trackedTable.update({ noteId, title: 'new note' })).rejects.toThrow('No record found to update');
|
|
693
|
+
});
|
|
694
|
+
it('should handle delete with string ID for non-existent record', async () => {
|
|
695
|
+
const noteId = (0, peers_sdk_1.newid)();
|
|
696
|
+
// Mock console.warn to capture the warning
|
|
697
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
698
|
+
// Try to delete non-existent record
|
|
699
|
+
await trackedTable.delete(noteId);
|
|
700
|
+
// Verify warning was logged
|
|
701
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Record not found'));
|
|
702
|
+
warnSpy.mockRestore();
|
|
703
|
+
});
|
|
704
|
+
it('should handle update with no changes', async () => {
|
|
705
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: 'test note' });
|
|
706
|
+
// Update with same data - should return unchanged
|
|
707
|
+
const result = await trackedTable.update(note);
|
|
708
|
+
expect(result).toEqual(note);
|
|
709
|
+
// Should still only have one change (the insert)
|
|
710
|
+
const changes = await trackedTable.listChanges({ recordId: note.noteId });
|
|
711
|
+
expect(changes.length).toBe(1);
|
|
712
|
+
expect(changes[0].changeType).toBe('insert');
|
|
713
|
+
});
|
|
714
|
+
it('should handle compact with default timestamp', async () => {
|
|
715
|
+
const noteId = (0, peers_sdk_1.newid)();
|
|
716
|
+
const note = { noteId, title: 'test note' };
|
|
717
|
+
// Create some changes
|
|
718
|
+
await trackedTable.save(note);
|
|
719
|
+
note.title = 'updated';
|
|
720
|
+
await trackedTable.save(note);
|
|
721
|
+
// Test compact with default timestamp (should use 2 weeks ago)
|
|
722
|
+
await trackedTable.compact();
|
|
723
|
+
// Should still have changes since they're recent
|
|
724
|
+
const changes = await trackedTable.listChanges({ recordId: noteId });
|
|
725
|
+
expect(changes.length).toBeGreaterThan(0);
|
|
726
|
+
});
|
|
727
|
+
it('should handle findAndTrackRecords method', async () => {
|
|
728
|
+
// Use separate instance for isolation
|
|
729
|
+
const db = new local_data_source_1.DBLocal(':memory:');
|
|
730
|
+
const mockDataContext4 = {
|
|
731
|
+
dataSourceFactory: () => new peers_sdk_1.SQLDataSource(db, NotesMetaData, notesSchema),
|
|
732
|
+
eventRegistry: {
|
|
733
|
+
getEmitter: () => ({
|
|
734
|
+
event: { subscribe: () => { } },
|
|
735
|
+
emit: () => { }
|
|
736
|
+
})
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
const deps4 = {
|
|
740
|
+
dataSource: new (require("@peers-app/peers-sdk")).SQLDataSource(db, NotesMetaData, notesSchema),
|
|
741
|
+
eventRegistry: mockDataContext4.eventRegistry,
|
|
742
|
+
};
|
|
743
|
+
const NotesTable = new peers_sdk_1.Table(NotesMetaData, deps4);
|
|
744
|
+
const changeTrackingTable = new peers_sdk_1.ChangeTrackingTable({ db });
|
|
745
|
+
const isolatedTrackedTable = new tracked_data_source_1.TrackedDataSource(NotesTable, changeTrackingTable);
|
|
746
|
+
// Insert some records directly to data source (bypassing tracking)
|
|
747
|
+
const note1 = { noteId: (0, peers_sdk_1.newid)(), title: 'untracked 1' };
|
|
748
|
+
const note2 = { noteId: (0, peers_sdk_1.newid)(), title: 'untracked 2' };
|
|
749
|
+
await NotesTable.insert(note1);
|
|
750
|
+
await NotesTable.insert(note2);
|
|
751
|
+
// Mock console.log to capture output
|
|
752
|
+
const logSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
753
|
+
// Find and track records
|
|
754
|
+
await isolatedTrackedTable.findAndTrackRecords(1);
|
|
755
|
+
// Verify records were tracked
|
|
756
|
+
const changes = await isolatedTrackedTable.listChanges({ changeType: 'snapshot' });
|
|
757
|
+
expect(changes.length).toBe(2);
|
|
758
|
+
// Verify console output
|
|
759
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Tracking recordId'));
|
|
760
|
+
logSpy.mockRestore();
|
|
761
|
+
});
|
|
762
|
+
it('should handle apply changes with restore after delete', async () => {
|
|
763
|
+
const noteId = (0, peers_sdk_1.newid)();
|
|
764
|
+
// Apply delete change
|
|
765
|
+
const deleteChange = {
|
|
766
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
767
|
+
changeType: 'delete',
|
|
768
|
+
timestamp: Date.now(),
|
|
769
|
+
timestampApplied: Date.now(),
|
|
770
|
+
tableName: 'notes',
|
|
771
|
+
recordId: noteId,
|
|
772
|
+
oldRecord: { noteId, title: 'deleted note' }
|
|
773
|
+
};
|
|
774
|
+
await trackedTable.applyChanges([deleteChange]);
|
|
775
|
+
// Apply restore change
|
|
776
|
+
const restoreChange = {
|
|
777
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
778
|
+
changeType: 'restore',
|
|
779
|
+
timestamp: Date.now() + 1,
|
|
780
|
+
timestampApplied: Date.now() + 1,
|
|
781
|
+
tableName: 'notes',
|
|
782
|
+
recordId: noteId,
|
|
783
|
+
newRecord: { noteId, title: 'restored note' }
|
|
784
|
+
};
|
|
785
|
+
await trackedTable.applyChanges([restoreChange]);
|
|
786
|
+
// Verify record was restored
|
|
787
|
+
const note = await trackedTable.get(noteId);
|
|
788
|
+
expect(note).toEqual({ noteId, title: 'restored note' });
|
|
789
|
+
});
|
|
790
|
+
it('should handle error cases in applyChanges', async () => {
|
|
791
|
+
const noteId = (0, peers_sdk_1.newid)();
|
|
792
|
+
// Create a change with unsupported type
|
|
793
|
+
const unsupportedChange = {
|
|
794
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
795
|
+
changeType: 'unsupported',
|
|
796
|
+
timestamp: Date.now(),
|
|
797
|
+
timestampApplied: Date.now(),
|
|
798
|
+
tableName: 'notes',
|
|
799
|
+
recordId: noteId,
|
|
800
|
+
};
|
|
801
|
+
await expect(trackedTable.applyChanges([unsupportedChange])).rejects.toThrow('Unsupported change type');
|
|
802
|
+
});
|
|
803
|
+
it('should handle insert without primaryKey', async () => {
|
|
804
|
+
// Test insert without providing primary key
|
|
805
|
+
const result = await trackedTable.insert({ title: 'no id' });
|
|
806
|
+
expect(result.noteId).toBeDefined();
|
|
807
|
+
expect(result.title).toBe('no id');
|
|
808
|
+
});
|
|
809
|
+
it('should handle save with record that needs insert', async () => {
|
|
810
|
+
const noteId = (0, peers_sdk_1.newid)();
|
|
811
|
+
// Test save with new record (should insert)
|
|
812
|
+
const result = await trackedTable.save({ noteId, title: 'new record' });
|
|
813
|
+
expect(result).toEqual({ noteId, title: 'new record' });
|
|
814
|
+
// Verify it was inserted
|
|
815
|
+
const retrieved = await trackedTable.get(noteId);
|
|
816
|
+
expect(retrieved).toEqual({ noteId, title: 'new record' });
|
|
817
|
+
});
|
|
818
|
+
it('should handle save without primaryKey', async () => {
|
|
819
|
+
// Test save without providing primary key (should insert)
|
|
820
|
+
const result = await trackedTable.save({ title: 'no id' });
|
|
821
|
+
expect(result.noteId).toBeDefined();
|
|
822
|
+
expect(result.title).toBe('no id');
|
|
823
|
+
});
|
|
824
|
+
});
|
|
825
|
+
//# sourceMappingURL=tracked-data-source.test.js.map
|