@wszerad/items 0.1.1 → 0.2.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.
@@ -1,1095 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
-
3
- import { Items } from '../src'
4
-
5
- interface User {
6
- id: number
7
- name: string
8
- age?: number
9
- }
10
-
11
- interface Book {
12
- isbn: string
13
- title: string
14
- year: number
15
- }
16
-
17
- describe('Items', () => {
18
- describe('initialization', () => {
19
- it('creates empty collection', () => {
20
- const items = new Items<User>()
21
- expect(items.getIds()).toEqual([])
22
- expect(items.getEntities()).toEqual(new Map())
23
- expect(items.length).toBe(0)
24
- })
25
-
26
- it('creates collection with initial items', () => {
27
- const users = [
28
- { id: 1, name: 'Alice' },
29
- { id: 2, name: 'Bob' }
30
- ]
31
- const items = new Items(users)
32
- expect(items.getIds()).toEqual([1, 2])
33
- expect(items.length).toBe(2)
34
- expect(items.select(1)).toEqual({ id: 1, name: 'Alice' })
35
- })
36
-
37
- it('uses custom selectId', () => {
38
- const items = new Items<Book>(
39
- [],
40
- { selectId: (book) => book.isbn }
41
- )
42
- const updated = items.insert({ isbn: '978-0', title: 'Test', year: 2020 })
43
- expect(updated.getIds()).toEqual(['978-0'])
44
- })
45
-
46
- it('applies sortComparer on initialization', () => {
47
- const items = new Items(
48
- [
49
- { id: 2, name: 'Bob' },
50
- { id: 1, name: 'Alice' }
51
- ],
52
- { sortComparer: (a, b) => a.name.localeCompare(b.name) }
53
- )
54
- expect(items.getIds()).toEqual([1, 2])
55
- })
56
- })
57
-
58
- describe('insert', () => {
59
- it('inserts single entity', () => {
60
- const items = new Items<User>()
61
- const updated = items.insert({ id: 1, name: 'Alice' })
62
-
63
- expect(updated.getIds()).toEqual([1])
64
- expect(updated.select(1)).toEqual({ id: 1, name: 'Alice' })
65
- expect(updated.length).toBe(1)
66
- })
67
-
68
- it('does not insert duplicate', () => {
69
- const items = new Items([{ id: 1, name: 'Alice' }])
70
- const updated = items.insert({ id: 1, name: 'Alice Updated' })
71
-
72
- expect(updated.getIds()).toEqual([1])
73
- expect(updated.select(1)).toEqual({ id: 1, name: 'Alice' })
74
- })
75
-
76
- it('is immutable', () => {
77
- const items = new Items<User>()
78
- const updated = items.insert({ id: 1, name: 'Alice' })
79
-
80
- expect(items.getIds()).toEqual([])
81
- expect(updated.getIds()).toEqual([1])
82
- })
83
- })
84
-
85
- describe('insertMany', () => {
86
- it('inserts multiple entities', () => {
87
- const items = new Items<User>()
88
- const updated = items.insertMany([
89
- { id: 1, name: 'Alice' },
90
- { id: 2, name: 'Bob' }
91
- ])
92
-
93
- expect(updated.getIds()).toEqual([1, 2])
94
- expect(updated.length).toBe(2)
95
- })
96
-
97
- it('skips duplicates in batch', () => {
98
- const items = new Items([{ id: 1, name: 'Alice' }])
99
- const updated = items.insertMany([
100
- { id: 1, name: 'Alice Updated' },
101
- { id: 2, name: 'Bob' }
102
- ])
103
-
104
- expect(updated.getIds()).toEqual([1, 2])
105
- expect(updated.select(1)).toEqual({ id: 1, name: 'Alice' })
106
- })
107
-
108
- it('inserts from iterable (Set)', () => {
109
- const items = new Items<User>()
110
- const usersSet = new Set([
111
- { id: 1, name: 'Alice' },
112
- { id: 2, name: 'Bob' }
113
- ])
114
- const updated = items.insertMany(usersSet)
115
-
116
- expect(updated.getIds()).toEqual([1, 2])
117
- expect(updated.length).toBe(2)
118
- })
119
-
120
- it('maintains sort order when inserting', () => {
121
- const items = new Items<User>(
122
- [],
123
- { sortComparer: (a, b) => a.name.localeCompare(b.name) }
124
- )
125
- const updated = items.insertMany([
126
- { id: 3, name: 'Charlie' },
127
- { id: 1, name: 'Alice' },
128
- { id: 2, name: 'Bob' }
129
- ])
130
-
131
- expect(updated.getIds()).toEqual([1, 2, 3])
132
- })
133
-
134
- it('inserts into existing sorted collection', () => {
135
- const items = new Items(
136
- [{ id: 1, name: 'Alice' }, { id: 3, name: 'Charlie' }],
137
- { sortComparer: (a, b) => a.name.localeCompare(b.name) }
138
- )
139
- const updated = items.insertMany([{ id: 2, name: 'Bob' }])
140
-
141
- expect(updated.getIds()).toEqual([1, 2, 3])
142
- })
143
- })
144
-
145
- describe('upsert', () => {
146
- it('adds new entity', () => {
147
- const items = new Items<User>()
148
- const updated = items.upsert({ id: 1, name: 'Alice' })
149
-
150
- expect(updated.getIds()).toEqual([1])
151
- expect(updated.select(1)).toEqual({ id: 1, name: 'Alice' })
152
- })
153
-
154
- it('merges with existing entity (extends properties)', () => {
155
- const items = new Items([{ id: 1, name: 'Alice', age: 25 } as User])
156
- const updated = items.upsert({ id: 1, name: 'Alice Updated' })
157
-
158
- // upsert merges/extends the entity
159
- expect(updated.select(1)).toEqual({ id: 1, name: 'Alice Updated', age: 25 })
160
- })
161
-
162
- it('is immutable', () => {
163
- const items = new Items([{ id: 1, name: 'Alice' }])
164
- const updated = items.upsert({ id: 1, name: 'Alice Updated' } as User)
165
-
166
- expect(items.select(1)).toEqual({ id: 1, name: 'Alice' })
167
- expect(updated.select(1)).toEqual({ id: 1, name: 'Alice Updated' })
168
- })
169
-
170
- it('adds new property to existing entity', () => {
171
- const items = new Items([{ id: 1, name: 'Alice' }])
172
- const updated = items.upsert({ id: 1, age: 25 } as User)
173
-
174
- // Both properties are present
175
- expect(updated.select(1)).toEqual({ id: 1, name: 'Alice', age: 25 })
176
- })
177
- })
178
-
179
- describe('upsertMany', () => {
180
- it('upserts multiple entities', () => {
181
- const items = new Items<User>([{ id: 1, name: 'Alice' }])
182
- const updated = items.upsertMany([
183
- { id: 1, name: 'Alice Updated', age: 26 },
184
- { id: 2, name: 'Bob' }
185
- ])
186
-
187
- expect(updated.getIds()).toEqual([1, 2])
188
- expect(updated.select(1)).toEqual({ id: 1, name: 'Alice Updated', age: 26 })
189
- expect(updated.select(2)).toEqual({ id: 2, name: 'Bob' })
190
- })
191
-
192
- it('upserts from iterable (Set)', () => {
193
- const items = new Items<User>([{ id: 1, name: 'Alice' }])
194
- const usersSet = new Set<Partial<User>>([
195
- { id: 1, name: 'Alice Updated', age: 26 },
196
- { id: 2, name: 'Bob', age: 30 }
197
- ])
198
- const updated = items.upsertMany(usersSet)
199
-
200
- expect(updated.getIds()).toEqual([1, 2])
201
- expect(updated.select(1)).toEqual({ id: 1, name: 'Alice Updated', age: 26 })
202
- expect(updated.select(2)).toEqual({ id: 2, name: 'Bob', age: 30 })
203
- })
204
-
205
- it('maintains sort order when upserting', () => {
206
- const items = new Items(
207
- [{ id: 1, name: 'Alice' }, { id: 3, name: 'Charlie' }],
208
- { sortComparer: (a, b) => a.name.localeCompare(b.name) }
209
- )
210
- const updated = items.upsertMany([{ id: 2, name: 'Bob' }])
211
-
212
- expect(updated.getIds()).toEqual([1, 2, 3])
213
- })
214
-
215
- it('merges properties correctly', () => {
216
- const items = new Items([{ id: 1, name: 'Alice', age: 25 } as User])
217
- const updated = items.upsertMany([{ id: 1, age: 26 }])
218
-
219
- // Name is preserved, age is updated
220
- expect(updated.select(1)).toEqual({ id: 1, name: 'Alice', age: 26 })
221
- })
222
- })
223
-
224
- describe('set', () => {
225
- it('adds new entity', () => {
226
- const items = new Items<User>()
227
- const updated = items.set({ id: 1, name: 'Alice' })
228
-
229
- expect(updated.getIds()).toEqual([1])
230
- expect(updated.select(1)).toEqual({ id: 1, name: 'Alice' })
231
- })
232
-
233
- it('replaces existing entity completely', () => {
234
- const items = new Items([{ id: 1, name: 'Alice', age: 25 } as User])
235
- const updated = items.set({ id: 1, name: 'Alice Updated' })
236
-
237
- // set replaces the entire entity, so age is removed
238
- expect(updated.select(1)).toEqual({ id: 1, name: 'Alice Updated' })
239
- expect(updated.select(1)?.age).toBeUndefined()
240
- })
241
-
242
- it('is immutable', () => {
243
- const items = new Items([{ id: 1, name: 'Alice', age: 25 } as User])
244
- const updated = items.set({ id: 1, name: 'Alice Updated' })
245
-
246
- expect(items.select(1)).toEqual({ id: 1, name: 'Alice', age: 25 })
247
- expect(updated.select(1)).toEqual({ id: 1, name: 'Alice Updated' })
248
- })
249
-
250
- it('removes properties not in new entity', () => {
251
- const items = new Items([
252
- { id: 1, name: 'Alice', age: 25 } as User
253
- ])
254
- const updated = items.set({ id: 1, name: 'Alice' })
255
-
256
- expect(updated.select(1)).toEqual({ id: 1, name: 'Alice' })
257
- expect(updated.select(1)?.age).toBeUndefined()
258
- })
259
- })
260
-
261
- describe('setMany', () => {
262
- it('sets multiple entities', () => {
263
- const items = new Items([{ id: 1, name: 'Alice', age: 25 } as User])
264
- const updated = items.setMany([
265
- { id: 1, name: 'Alice Updated' },
266
- { id: 2, name: 'Bob' }
267
- ])
268
-
269
- expect(updated.getIds()).toEqual([1, 2])
270
- expect(updated.select(1)).toEqual({ id: 1, name: 'Alice Updated' })
271
- expect(updated.select(2)).toEqual({ id: 2, name: 'Bob' })
272
- })
273
-
274
- it('sets from iterable (Set)', () => {
275
- const items = new Items<User>([{ id: 1, name: 'Alice', age: 25 }])
276
- const usersSet = new Set<User>([
277
- { id: 1, name: 'Alice Updated' },
278
- { id: 2, name: 'Bob', age: 30 }
279
- ])
280
- const updated = items.setMany(usersSet)
281
-
282
- expect(updated.getIds()).toEqual([1, 2])
283
- expect(updated.select(1)).toEqual({ id: 1, name: 'Alice Updated' })
284
- expect(updated.select(1)?.age).toBeUndefined()
285
- expect(updated.select(2)).toEqual({ id: 2, name: 'Bob', age: 30 })
286
- })
287
-
288
- it('maintains sort order when setting', () => {
289
- const items = new Items(
290
- [{ id: 1, name: 'Alice' }, { id: 3, name: 'Charlie' }],
291
- { sortComparer: (a, b) => a.name.localeCompare(b.name) }
292
- )
293
- const updated = items.setMany([{ id: 2, name: 'Bob' }])
294
-
295
- expect(updated.getIds()).toEqual([1, 2, 3])
296
- })
297
- })
298
-
299
- describe('every', () => {
300
- it('returns true when all entities match condition', () => {
301
- const items = new Items([
302
- { id: 1, name: 'Alice', age: 25 },
303
- { id: 2, name: 'Bob', age: 30 },
304
- { id: 3, name: 'Charlie', age: 35 }
305
- ])
306
-
307
- expect(items.every(user => user.age! >= 20)).toBe(true)
308
- })
309
-
310
- it('returns false when at least one entity does not match', () => {
311
- const items = new Items([
312
- { id: 1, name: 'Alice', age: 25 },
313
- { id: 2, name: 'Bob', age: 30 },
314
- { id: 3, name: 'Charlie', age: 15 }
315
- ])
316
-
317
- expect(items.every(user => user.age! >= 20)).toBe(false)
318
- })
319
-
320
- it('returns true for empty collection', () => {
321
- const items = new Items<User>()
322
-
323
- expect(items.every(user => user.age! >= 20)).toBe(true)
324
- })
325
-
326
- it('works with name matching', () => {
327
- const items = new Items([
328
- { id: 1, name: 'Alice', age: 25 },
329
- { id: 2, name: 'Bob', age: 30 }
330
- ])
331
-
332
- expect(items.every(user => user.name.length > 0)).toBe(true)
333
- expect(items.every(user => user.name.startsWith('A'))).toBe(false)
334
- })
335
-
336
- it('checks for optional properties', () => {
337
- const items = new Items([
338
- { id: 1, name: 'Alice', age: 25 },
339
- { id: 2, name: 'Bob', age: 30 },
340
- { id: 3, name: 'Charlie', age: 35 }
341
- ])
342
-
343
- expect(items.every(user => user.age !== undefined)).toBe(true)
344
- })
345
-
346
- it('returns false when not all have optional property', () => {
347
- const items = new Items([
348
- { id: 1, name: 'Alice', age: 25 },
349
- { id: 2, name: 'Bob' },
350
- { id: 3, name: 'Charlie', age: 35 }
351
- ])
352
-
353
- expect(items.every(user => user.age !== undefined)).toBe(false)
354
- })
355
- })
356
-
357
- describe('some', () => {
358
- it('returns true when at least one entity matches', () => {
359
- const items = new Items([
360
- { id: 1, name: 'Alice', age: 15 },
361
- { id: 2, name: 'Bob', age: 30 },
362
- { id: 3, name: 'Charlie', age: 35 }
363
- ])
364
-
365
- expect(items.some(user => user.age! < 20)).toBe(true)
366
- })
367
-
368
- it('returns false when no entities match', () => {
369
- const items = new Items([
370
- { id: 1, name: 'Alice', age: 25 },
371
- { id: 2, name: 'Bob', age: 30 },
372
- { id: 3, name: 'Charlie', age: 35 }
373
- ])
374
-
375
- expect(items.some(user => user.age! < 20)).toBe(false)
376
- })
377
-
378
- it('returns false for empty collection', () => {
379
- const items = new Items<User>()
380
-
381
- expect(items.some(user => user.age! >= 20)).toBe(false)
382
- })
383
-
384
- it('works with name matching', () => {
385
- const items = new Items([
386
- { id: 1, name: 'Alice', age: 25 },
387
- { id: 2, name: 'Bob', age: 30 }
388
- ])
389
-
390
- expect(items.some(user => user.name.startsWith('A'))).toBe(true)
391
- expect(items.some(user => user.name.startsWith('Z'))).toBe(false)
392
- })
393
-
394
- it('checks for optional properties', () => {
395
- const items = new Items<User>([
396
- { id: 1, name: 'Alice' },
397
- { id: 2, name: 'Bob' },
398
- { id: 3, name: 'Charlie' }
399
- ])
400
-
401
- expect(items.some(user => user.age !== undefined)).toBe(false)
402
- })
403
-
404
- it('returns true when at least one has optional property', () => {
405
- const items = new Items<User>([
406
- { id: 1, name: 'Alice' },
407
- { id: 2, name: 'Bob', age: 30 },
408
- { id: 3, name: 'Charlie' }
409
- ])
410
-
411
- expect(items.some(user => user.age !== undefined)).toBe(true)
412
- })
413
-
414
- it('checks complex conditions', () => {
415
- const items = new Items([
416
- { id: 1, name: 'Alice', age: 25 },
417
- { id: 2, name: 'Bob', age: 30 },
418
- { id: 3, name: 'Charlie', age: 35 }
419
- ])
420
-
421
- expect(items.some(user => user.name === 'Bob' && user.age === 30)).toBe(true)
422
- expect(items.some(user => user.name === 'Bob' && user.age === 25)).toBe(false)
423
- })
424
- })
425
-
426
- describe('has', () => {
427
- it('checks if entity exists by single id', () => {
428
- const items = new Items([
429
- { id: 1, name: 'Alice' },
430
- { id: 2, name: 'Bob' }
431
- ])
432
-
433
- expect(items.has(1)).toBe(true)
434
- expect(items.has(2)).toBe(true)
435
- expect(items.has(3)).toBe(false)
436
- expect(items.has(99)).toBe(false)
437
- })
438
-
439
- it('returns false for non-existent id', () => {
440
- const items = new Items([{ id: 1, name: 'Alice' }])
441
-
442
- expect(items.has(1)).toBe(true)
443
- expect(items.has(2)).toBe(false)
444
- })
445
-
446
- it('works with empty collection', () => {
447
- const items = new Items<User>()
448
-
449
- expect(items.has(1)).toBe(false)
450
- })
451
- })
452
-
453
- describe('hasMany', () => {
454
- it('checks if all entities exist by ids', () => {
455
- const items = new Items([
456
- { id: 1, name: 'Alice' },
457
- { id: 2, name: 'Bob' }
458
- ])
459
-
460
- expect(items.hasMany([1, 2])).toBe(true)
461
- expect(items.hasMany([1, 3])).toBe(false)
462
- expect(items.hasMany([1])).toBe(true)
463
- expect(items.hasMany([99])).toBe(false)
464
- })
465
-
466
- it('checks if entity exists by predicate', () => {
467
- const items = new Items([
468
- { id: 1, name: 'Alice', age: 25 }
469
- ])
470
-
471
- expect(items.hasMany(user => user.age === 25)).toBe(true)
472
- expect(items.hasMany(user => user.age === 30)).toBe(false)
473
- })
474
-
475
- it('returns false when no entities match predicate', () => {
476
- const items = new Items([
477
- { id: 1, name: 'Alice', age: 25 },
478
- { id: 2, name: 'Bob', age: 30 }
479
- ])
480
-
481
- expect(items.hasMany(user => user.age === 35)).toBe(false)
482
- })
483
-
484
- it('returns true when all ids exist', () => {
485
- const items = new Items([
486
- { id: 1, name: 'Alice' },
487
- { id: 2, name: 'Bob' },
488
- { id: 3, name: 'Charlie' }
489
- ])
490
-
491
- expect(items.hasMany([1, 2, 3])).toBe(true)
492
- })
493
-
494
- it('works with empty array', () => {
495
- const items = new Items([
496
- { id: 1, name: 'Alice' }
497
- ])
498
-
499
- expect(items.hasMany([])).toBe(false)
500
- })
501
- })
502
-
503
- describe('update', () => {
504
- it('updates entity by id with partial', () => {
505
- const items = new Items([{ id: 1, name: 'Alice', age: 25 }])
506
- const updated = items.update(1, { age: 26 })
507
-
508
- expect(updated.select(1)).toEqual({ id: 1, name: 'Alice', age: 26 })
509
- })
510
-
511
- it('updates entity by id with function', () => {
512
- const items = new Items([{ id: 1, name: 'Alice', age: 25 }])
513
- const updated = items.update(1, user => ({ ...user, age: user.age! + 1 }))
514
-
515
- expect(updated.select(1)).toEqual({ id: 1, name: 'Alice', age: 26 })
516
- })
517
-
518
- it('partial update preserves other fields', () => {
519
- const items = new Items([{ id: 1, name: 'Alice', age: 25 }])
520
- const updated = items.update(1, { name: 'Alicia' })
521
-
522
- expect(updated.select(1)).toEqual({ id: 1, name: 'Alicia', age: 25 })
523
- })
524
-
525
- it('returns same instance if entity does not exist', () => {
526
- const items = new Items([{ id: 1, name: 'Alice', age: 25 }])
527
- const updated = items.update(99, { age: 26 })
528
-
529
- expect(updated).toBe(items)
530
- })
531
- })
532
-
533
- describe('updateMany', () => {
534
- it('updates multiple entities by ids', () => {
535
- const items = new Items([
536
- { id: 1, name: 'Alice', age: 25 },
537
- { id: 2, name: 'Bob', age: 30 }
538
- ])
539
- const updated = items.updateMany([1, 2], user => ({ ...user, age: user.age! + 1 }))
540
-
541
- expect(updated.select(1)?.age).toBe(26)
542
- expect(updated.select(2)?.age).toBe(31)
543
- })
544
-
545
- it('updates entities by predicate', () => {
546
- const items = new Items([
547
- { id: 1, name: 'Alice', age: 25 },
548
- { id: 2, name: 'Bob', age: 30 }
549
- ])
550
- const updated = items.updateMany(
551
- user => user.age! < 30,
552
- { age: 26 }
553
- )
554
-
555
- expect(updated.select(1)?.age).toBe(26)
556
- expect(updated.select(2)?.age).toBe(30)
557
- })
558
-
559
- it('partial update preserves other fields', () => {
560
- const items = new Items([
561
- { id: 1, name: 'Alice', age: 25 },
562
- { id: 2, name: 'Bob', age: 30 }
563
- ])
564
- const updated = items.updateMany([1, 2], { age: 99 })
565
-
566
- expect(updated.select(1)).toEqual({ id: 1, name: 'Alice', age: 99 })
567
- expect(updated.select(2)).toEqual({ id: 2, name: 'Bob', age: 99 })
568
- })
569
- })
570
-
571
- describe('remove', () => {
572
- it('removes entity by id', () => {
573
- const items = new Items([
574
- { id: 1, name: 'Alice' },
575
- { id: 2, name: 'Bob' }
576
- ])
577
- const updated = items.remove(1)
578
-
579
- expect(updated.getIds()).toEqual([2])
580
- expect(updated.select(1)).toBeUndefined()
581
- expect(updated.length).toBe(1)
582
- })
583
-
584
- it('is immutable', () => {
585
- const items = new Items([
586
- { id: 1, name: 'Alice' },
587
- { id: 2, name: 'Bob' }
588
- ])
589
- const updated = items.remove(1)
590
-
591
- expect(items.getIds()).toEqual([1, 2])
592
- expect(updated.getIds()).toEqual([2])
593
- })
594
-
595
- it('removes non-existent entity without error', () => {
596
- const items = new Items([
597
- { id: 1, name: 'Alice' }
598
- ])
599
- const updated = items.remove(99)
600
-
601
- expect(updated.getIds()).toEqual([1])
602
- })
603
- })
604
-
605
- describe('removeMany', () => {
606
- it('removes multiple entities by ids', () => {
607
- const items = new Items([
608
- { id: 1, name: 'Alice' },
609
- { id: 2, name: 'Bob' },
610
- { id: 3, name: 'Charlie' }
611
- ])
612
- const updated = items.removeMany([1, 3])
613
-
614
- expect(updated.getIds()).toEqual([2])
615
- expect(updated.length).toBe(1)
616
- })
617
-
618
- it('removes entities by predicate', () => {
619
- const items = new Items([
620
- { id: 1, name: 'Alice', age: 25 },
621
- { id: 2, name: 'Bob', age: 30 },
622
- { id: 3, name: 'Charlie', age: 20 }
623
- ])
624
- const updated = items.removeMany(user => user.age! < 25)
625
-
626
- expect(updated.getIds()).toEqual([1, 2])
627
- })
628
- })
629
-
630
- describe('clear', () => {
631
- it('clears collection', () => {
632
- const items = new Items([
633
- { id: 1, name: 'Alice' },
634
- { id: 2, name: 'Bob' }
635
- ])
636
- const updated = items.clear()
637
-
638
- expect(updated.getIds()).toEqual([])
639
- expect(updated.getEntities()).toEqual(new Map())
640
- expect(updated.length).toBe(0)
641
- })
642
- })
643
-
644
- describe('filter', () => {
645
- it('filters by ids', () => {
646
- const items = new Items([
647
- { id: 1, name: 'Alice' },
648
- { id: 2, name: 'Bob' },
649
- { id: 3, name: 'Charlie' }
650
- ])
651
- const filtered = items.filter([1, 3])
652
-
653
- expect(filtered.getIds()).toEqual([1, 3])
654
- })
655
-
656
- it('filters by single id in array', () => {
657
- const items = new Items([
658
- { id: 1, name: 'Alice' },
659
- { id: 2, name: 'Bob' },
660
- { id: 3, name: 'Charlie' }
661
- ])
662
- const filtered = items.filter([2])
663
-
664
- expect(filtered.getIds()).toEqual([2])
665
- expect(filtered.length).toBe(1)
666
- })
667
-
668
- it('filters by predicate', () => {
669
- const items = new Items([
670
- { id: 1, name: 'Alice', age: 25 },
671
- { id: 2, name: 'Bob', age: 30 },
672
- { id: 3, name: 'Charlie', age: 20 }
673
- ])
674
- const filtered = items.filter(user => user.age! >= 25)
675
-
676
- expect(filtered.getIds()).toEqual([1, 2])
677
- })
678
- })
679
-
680
- describe('select', () => {
681
- it('selects entity by id', () => {
682
- const items = new Items([{ id: 1, name: 'Alice' }])
683
-
684
- expect(items.select(1)).toEqual({ id: 1, name: 'Alice' })
685
- expect(items.select(2)).toBeUndefined()
686
- })
687
- })
688
-
689
- describe('sortComparer', () => {
690
- it('sorts by name ascending', () => {
691
- const items = new Items<User>(
692
- [],
693
- { sortComparer: (a, b) => a.name.localeCompare(b.name) }
694
- )
695
- const updated = items.insertMany([
696
- { id: 3, name: 'Charlie' },
697
- { id: 1, name: 'Alice' },
698
- { id: 2, name: 'Bob' }
699
- ])
700
-
701
- expect(updated.getIds()).toEqual([1, 2, 3])
702
- })
703
-
704
- it('sorts by age descending', () => {
705
- const items = new Items<User>(
706
- [],
707
- { sortComparer: (a, b) => (b.age ?? 0) - (a.age ?? 0) }
708
- )
709
- const updated = items.insertMany([
710
- { id: 1, name: 'Alice', age: 25 },
711
- { id: 2, name: 'Bob', age: 30 },
712
- { id: 3, name: 'Charlie', age: 20 }
713
- ])
714
-
715
- const ages = updated.getIds().map(id => updated.select(id)!.age)
716
- expect(ages).toEqual([30, 25, 20])
717
- })
718
-
719
- it('re-sorts after update', () => {
720
- const items = new Items(
721
- [
722
- { id: 1, name: 'Alice', age: 25 },
723
- { id: 2, name: 'Bob', age: 30 }
724
- ],
725
- { sortComparer: (a, b) => (a.age ?? 0) - (b.age ?? 0) }
726
- )
727
- const updated = items.update(1, { age: 35 })
728
-
729
- const ages = updated.getIds().map(id => updated.select(id)!.age)
730
- expect(ages).toEqual([30, 35])
731
- })
732
-
733
- it('sortComparer: false disables sorting', () => {
734
- const items = new Items<User>(
735
- [],
736
- { sortComparer: false }
737
- )
738
- const updated = items.insertMany([
739
- { id: 3, name: 'Charlie' },
740
- { id: 1, name: 'Alice' },
741
- { id: 2, name: 'Bob' }
742
- ])
743
-
744
- expect(updated.getIds()).toEqual([3, 1, 2])
745
- })
746
- })
747
-
748
- describe('page', () => {
749
- it('returns first page', () => {
750
- const items = new Items([
751
- { id: 1, name: 'Alice' },
752
- { id: 2, name: 'Bob' },
753
- { id: 3, name: 'Charlie' },
754
- { id: 4, name: 'Dave' },
755
- { id: 5, name: 'Eve' }
756
- ])
757
- const page = items.page(0, 2)
758
-
759
- expect(page.items).toEqual([
760
- { id: 1, name: 'Alice' },
761
- { id: 2, name: 'Bob' }
762
- ])
763
- expect(page.page).toBe(0)
764
- expect(page.pageSize).toBe(2)
765
- expect(page.hasNext).toBe(true)
766
- expect(page.hasPrevious).toBe(false)
767
- expect(page.total).toBe(5)
768
- expect(page.totalPages).toBe(3)
769
- })
770
-
771
- it('returns middle page', () => {
772
- const items = new Items([
773
- { id: 1, name: 'Alice' },
774
- { id: 2, name: 'Bob' },
775
- { id: 3, name: 'Charlie' },
776
- { id: 4, name: 'Dave' },
777
- { id: 5, name: 'Eve' }
778
- ])
779
- const page = items.page(1, 2)
780
-
781
- expect(page.items).toEqual([
782
- { id: 3, name: 'Charlie' },
783
- { id: 4, name: 'Dave' }
784
- ])
785
- expect(page.hasNext).toBe(true)
786
- expect(page.hasPrevious).toBe(true)
787
- })
788
-
789
- it('returns last page', () => {
790
- const items = new Items([
791
- { id: 1, name: 'Alice' },
792
- { id: 2, name: 'Bob' },
793
- { id: 3, name: 'Charlie' }
794
- ])
795
- const page = items.page(1, 2)
796
-
797
- expect(page.items).toEqual([{ id: 3, name: 'Charlie' }])
798
- expect(page.hasNext).toBe(false)
799
- expect(page.hasPrevious).toBe(true)
800
- })
801
- })
802
-
803
- describe('diff', () => {
804
- it('detects added entities', () => {
805
- const base = new Items([{ id: 1, name: 'Alice' }])
806
- const updated = base.insert({ id: 2, name: 'Bob' })
807
- const diff = updated.diff(base)
808
-
809
- expect(diff.added).toEqual([2])
810
- expect(diff.removed).toEqual([])
811
- expect(diff.updated).toEqual([])
812
- })
813
-
814
- it('detects removed entities', () => {
815
- const base = new Items([
816
- { id: 1, name: 'Alice' },
817
- { id: 2, name: 'Bob' }
818
- ])
819
- const updated = base.remove(2)
820
- const diff = updated.diff(base)
821
-
822
- expect(diff.added).toEqual([])
823
- expect(diff.removed).toEqual([2])
824
- expect(diff.updated).toEqual([])
825
- })
826
-
827
- it('detects updated entities', () => {
828
- const base = new Items([{ id: 1, name: 'Alice', age: 25 }])
829
- const updated = base.update(1, { age: 26 })
830
- const diff = updated.diff(base)
831
-
832
- expect(diff.added).toEqual([])
833
- expect(diff.removed).toEqual([])
834
- expect(diff.updated.length).toEqual(1)
835
- expect(diff.updated[0].id).toBe(1)
836
- expect(diff.updated[0].changes[0].key).toBe('age')
837
- expect(diff.updated[0].changes[0].type).toBe('changed')
838
- })
839
-
840
- it('detects multiple added entities', () => {
841
- const base = new Items([{ id: 1, name: 'Alice' }])
842
- const updated = base.insertMany([
843
- { id: 2, name: 'Bob' },
844
- { id: 3, name: 'Charlie' },
845
- { id: 4, name: 'Dave' }
846
- ])
847
- const diff = updated.diff(base)
848
-
849
- expect(diff.added).toEqual([2, 3, 4])
850
- expect(diff.removed).toEqual([])
851
- expect(diff.updated).toEqual([])
852
- })
853
-
854
- it('detects multiple removed entities', () => {
855
- const base = new Items([
856
- { id: 1, name: 'Alice' },
857
- { id: 2, name: 'Bob' },
858
- { id: 3, name: 'Charlie' },
859
- { id: 4, name: 'Dave' }
860
- ])
861
- const updated = base.removeMany([2, 3, 4])
862
- const diff = updated.diff(base)
863
-
864
- expect(diff.added).toEqual([])
865
- expect(diff.removed).toEqual([2, 3, 4])
866
- expect(diff.updated).toEqual([])
867
- })
868
-
869
- it('detects property addition', () => {
870
- const base = new Items<User>([{ id: 1, name: 'Alice' }])
871
- const updated = base.update(1, { age: 25 })
872
- const diff = updated.diff(base)
873
-
874
- expect(diff.added).toEqual([])
875
- expect(diff.removed).toEqual([])
876
- expect(diff.updated.length).toBe(1)
877
- expect(diff.updated[0].id).toBe(1)
878
-
879
- const ageChange = diff.updated[0].changes.find(c => c.key === 'age')
880
- expect(ageChange?.type).toBe('added')
881
- expect(ageChange?.newValue?.value).toBe(25)
882
- })
883
-
884
- it('detects property removal', () => {
885
- const base = new Items<User>([{ id: 1, name: 'Alice', age: 25 }])
886
- const updated = base.set({ id: 1, name: 'Alice' })
887
- const diff = updated.diff(base)
888
-
889
- expect(diff.added).toEqual([])
890
- expect(diff.removed).toEqual([])
891
- expect(diff.updated.length).toBe(1)
892
-
893
- const ageChange = diff.updated[0].changes.find(c => c.key === 'age')
894
- expect(ageChange?.type).toBe('removed')
895
- expect(ageChange?.oldValue?.value).toBe(25)
896
- })
897
-
898
- it('detects property value change', () => {
899
- const base = new Items([{ id: 1, name: 'Alice', age: 25 }])
900
- const updated = base.update(1, { name: 'Alicia', age: 26 })
901
- const diff = updated.diff(base)
902
-
903
- expect(diff.updated.length).toBe(1)
904
- expect(diff.updated[0].changes.length).toBe(2)
905
-
906
- const nameChange = diff.updated[0].changes.find(c => c.key === 'name')
907
- expect(nameChange?.type).toBe('changed')
908
- expect(nameChange?.oldValue?.value).toBe('Alice')
909
- expect(nameChange?.newValue?.value).toBe('Alicia')
910
-
911
- const ageChange = diff.updated[0].changes.find(c => c.key === 'age')
912
- expect(ageChange?.type).toBe('changed')
913
- expect(ageChange?.oldValue?.value).toBe(25)
914
- expect(ageChange?.newValue?.value).toBe(26)
915
- })
916
-
917
- it('detects combined add, remove, and update', () => {
918
- const base = new Items([
919
- { id: 1, name: 'Alice', age: 25 },
920
- { id: 2, name: 'Bob', age: 30 },
921
- { id: 3, name: 'Charlie', age: 35 }
922
- ])
923
-
924
- const updated = base
925
- .remove(3)
926
- .update(1, { age: 26 })
927
- .insert({ id: 4, name: 'Dave', age: 40 })
928
-
929
- const diff = updated.diff(base)
930
-
931
- expect(diff.added).toEqual([4])
932
- expect(diff.removed).toEqual([3])
933
- expect(diff.updated.length).toBe(1)
934
- expect(diff.updated[0].id).toBe(1)
935
- })
936
-
937
- it('detects no changes for identical collections', () => {
938
- const base = new Items([
939
- { id: 1, name: 'Alice', age: 25 },
940
- { id: 2, name: 'Bob', age: 30 }
941
- ])
942
- const updated = new Items([
943
- { id: 1, name: 'Alice', age: 25 },
944
- { id: 2, name: 'Bob', age: 30 }
945
- ])
946
-
947
- const diff = updated.diff(base)
948
-
949
- expect(diff.added).toEqual([])
950
- expect(diff.removed).toEqual([])
951
- expect(diff.updated).toEqual([])
952
- })
953
-
954
- it('detects changes in nested objects', () => {
955
- type UserWithAddress = {
956
- id: number
957
- name: string
958
- address: { city: string; country: string }
959
- }
960
-
961
- const base = new Items<UserWithAddress>([
962
- { id: 1, name: 'Alice', address: { city: 'NYC', country: 'USA' } }
963
- ])
964
-
965
- const updated = base.update(1, {
966
- address: { city: 'LA', country: 'USA' }
967
- })
968
-
969
- const diff = updated.diff(base)
970
-
971
- expect(diff.updated.length).toBe(1)
972
- expect(diff.updated[0].id).toBe(1)
973
-
974
- // ohash detects nested changes with multiple change objects for nested properties
975
- expect(diff.updated[0].changes.length).toBeGreaterThan(0)
976
- })
977
-
978
- it('handles empty base collection', () => {
979
- const base = new Items<User>()
980
- const updated = base.insertMany([
981
- { id: 1, name: 'Alice' },
982
- { id: 2, name: 'Bob' }
983
- ])
984
-
985
- const diff = updated.diff(base)
986
-
987
- expect(diff.added).toEqual([1, 2])
988
- expect(diff.removed).toEqual([])
989
- expect(diff.updated).toEqual([])
990
- })
991
-
992
- it('handles empty updated collection', () => {
993
- const base = new Items([
994
- { id: 1, name: 'Alice' },
995
- { id: 2, name: 'Bob' }
996
- ])
997
- const updated = base.clear()
998
-
999
- const diff = updated.diff(base)
1000
-
1001
- expect(diff.added).toEqual([])
1002
- expect(diff.removed).toEqual([1, 2])
1003
- expect(diff.updated).toEqual([])
1004
- })
1005
-
1006
- it('detects multiple property changes on multiple entities', () => {
1007
- const base = new Items<User>([
1008
- { id: 1, name: 'Alice', age: 25 },
1009
- { id: 2, name: 'Bob', age: 30 },
1010
- { id: 3, name: 'Charlie', age: 35 }
1011
- ])
1012
-
1013
- const updated = base
1014
- .update(1, { age: 26 })
1015
- .update(2, { name: 'Robert', age: 31 })
1016
-
1017
- const diff = updated.diff(base)
1018
-
1019
- expect(diff.added).toEqual([])
1020
- expect(diff.removed).toEqual([])
1021
- expect(diff.updated.length).toBe(2)
1022
-
1023
- const user1Update = diff.updated.find(u => u.id === 1)
1024
- expect(user1Update?.changes.length).toBe(1)
1025
- expect(user1Update?.changes[0].key).toBe('age')
1026
-
1027
- const user2Update = diff.updated.find(u => u.id === 2)
1028
- expect(user2Update?.changes.length).toBe(2)
1029
- })
1030
- })
1031
-
1032
- describe('iteration', () => {
1033
- it('iterates over entities', () => {
1034
- const items = new Items([
1035
- { id: 1, name: 'Alice' },
1036
- { id: 2, name: 'Bob' }
1037
- ])
1038
- const names: string[] = []
1039
-
1040
- for (const user of items) {
1041
- names.push(user.name)
1042
- }
1043
-
1044
- expect(names).toEqual(['Alice', 'Bob'])
1045
- })
1046
-
1047
- it('works with spread operator', () => {
1048
- const items = new Items<User>([
1049
- { id: 1, name: 'Alice' },
1050
- { id: 2, name: 'Bob' }
1051
- ])
1052
- const array = [...items]
1053
-
1054
- expect(array).toEqual([
1055
- { id: 1, name: 'Alice' },
1056
- { id: 2, name: 'Bob' }
1057
- ])
1058
- })
1059
- })
1060
-
1061
- describe('custom selectId with Books', () => {
1062
- it('works with string IDs', () => {
1063
- const items = new Items<Book>(
1064
- [],
1065
- { selectId: (book) => book.isbn }
1066
- )
1067
- const updated = items.insertMany([
1068
- { isbn: '978-0-1', title: 'Book A', year: 2020 },
1069
- { isbn: '978-0-2', title: 'Book B', year: 2021 }
1070
- ])
1071
-
1072
- expect(updated.getIds()).toEqual(['978-0-1', '978-0-2'])
1073
- expect(updated.select('978-0-1')?.title).toBe('Book A')
1074
- })
1075
-
1076
- it('sorts books by year', () => {
1077
- const items = new Items<Book>(
1078
- [],
1079
- {
1080
- selectId: (book) => book.isbn,
1081
- sortComparer: (a, b) => a.year - b.year
1082
- }
1083
- )
1084
- const updated = items.insertMany([
1085
- { isbn: '978-0-3', title: 'Book C', year: 2022 },
1086
- { isbn: '978-0-1', title: 'Book A', year: 2020 },
1087
- { isbn: '978-0-2', title: 'Book B', year: 2021 }
1088
- ])
1089
-
1090
- const years = updated.getIds().map(id => updated.select(id)!.year)
1091
- expect(years).toEqual([2020, 2021, 2022])
1092
- })
1093
- })
1094
- })
1095
-