@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.
- package/README.md +377 -547
- package/dist/index.d.mts +87 -0
- package/dist/index.mjs +193 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +4 -1
- package/.github/dependabot.yml +0 -15
- package/.github/workflows/publish.yml +0 -22
- package/.github/workflows/test.yml +0 -21
- package/.idea/copilot.data.migration.ask2agent.xml +0 -6
- package/.idea/items.iml +0 -0
- package/.idea/modules.xml +0 -8
- package/.idea/vcs.xml +0 -6
- package/.oxlintrc.json +0 -13
- package/src/Items.ts +0 -215
- package/src/diff.ts +0 -44
- package/src/index.ts +0 -6
- package/src/selectId.ts +0 -7
- package/src/selector.ts +0 -20
- package/src/updater.ts +0 -6
- package/test/index.test.ts +0 -1095
- package/test-type-check.ts +0 -50
- package/tsconfig.json +0 -16
- package/tsdown.config.ts +0 -13
- package/vitest.config.ts +0 -9
package/README.md
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
|
-
# items
|
|
1
|
+
# @wszerad/items
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An immutable collection library for managing entities with built-in selection, filtering, and diffing capabilities.
|
|
4
|
+
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://badge.fury.io/js/@wszerad%2Fitems)
|
|
4
7
|
|
|
5
8
|
## Features
|
|
6
9
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
- ✅ Tree-shakeable ESM build
|
|
14
|
-
- ✅ Flexible selectors (ID, array of IDs, or predicate function)
|
|
15
|
-
- ✅ Built-in pagination support
|
|
16
|
-
- ✅ Diff detection between collections
|
|
17
|
-
- ✅ `every()` and `some()` collection validators
|
|
10
|
+
- 🔒 **Immutable**: All operations return new instances without mutating the original
|
|
11
|
+
- 🎯 **Type-safe**: Full TypeScript support with generic types
|
|
12
|
+
- 🔍 **Powerful Selection**: Query and filter items with a fluent API
|
|
13
|
+
- 📊 **Diff Tracking**: Compare collections and track changes
|
|
14
|
+
- ⚡ **Performance**: Efficient internal storage using Maps
|
|
15
|
+
- 🎨 **Flexible**: Custom ID selectors and sorting comparers
|
|
18
16
|
|
|
19
17
|
## Installation
|
|
20
18
|
|
|
@@ -25,7 +23,7 @@ npm install @wszerad/items
|
|
|
25
23
|
## Quick Start
|
|
26
24
|
|
|
27
25
|
```typescript
|
|
28
|
-
import { Items } from 'items'
|
|
26
|
+
import { Items } from '@wszerad/items'
|
|
29
27
|
|
|
30
28
|
interface User {
|
|
31
29
|
id: number
|
|
@@ -33,734 +31,566 @@ interface User {
|
|
|
33
31
|
age: number
|
|
34
32
|
}
|
|
35
33
|
|
|
36
|
-
// Create collection
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
// Create a collection
|
|
35
|
+
const users = new Items<User>([
|
|
36
|
+
{ id: 1, name: 'Alice', age: 30 },
|
|
37
|
+
{ id: 2, name: 'Bob', age: 25 },
|
|
38
|
+
{ id: 3, name: 'Charlie', age: 35 }
|
|
39
|
+
])
|
|
41
40
|
|
|
42
|
-
// Add
|
|
43
|
-
const
|
|
44
|
-
{ id:
|
|
45
|
-
{ id: 2, name: 'Bob', age: 30 }
|
|
41
|
+
// Add items
|
|
42
|
+
const updated = users.add([
|
|
43
|
+
{ id: 4, name: 'David', age: 40 }
|
|
46
44
|
])
|
|
47
45
|
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
// Update items
|
|
47
|
+
const older = users.update([1, 2], (user) => ({
|
|
48
|
+
...user!,
|
|
49
|
+
age: user!.age + 1
|
|
50
|
+
}))
|
|
51
|
+
|
|
52
|
+
// Select and filter - returns array
|
|
53
|
+
const adults = users.select((s) =>
|
|
54
|
+
s.filter((user) => user.age >= 30)
|
|
55
|
+
.sort((a, b) => a.age - b.age)
|
|
56
|
+
)
|
|
57
|
+
console.log(adults) // [{ id: 1, name: 'Alice', age: 30 }, { id: 3, name: 'Charlie', age: 35 }]
|
|
52
58
|
|
|
53
|
-
//
|
|
54
|
-
const
|
|
59
|
+
// Select single item using at() - returns single item or undefined
|
|
60
|
+
const oldestUser = users.select((s) =>
|
|
61
|
+
s.sort((a, b) => b.age - a.age).at(0)
|
|
62
|
+
)
|
|
63
|
+
console.log(oldestUser) // { id: 3, name: 'Charlie', age: 35 }
|
|
55
64
|
|
|
56
|
-
//
|
|
57
|
-
const
|
|
65
|
+
// Get IDs instead of items
|
|
66
|
+
const adultIds = users.selectId((s) =>
|
|
67
|
+
s.filter((user) => user.age >= 30)
|
|
68
|
+
)
|
|
69
|
+
console.log(adultIds) // [1, 3]
|
|
58
70
|
```
|
|
59
71
|
|
|
60
72
|
## API Reference
|
|
61
73
|
|
|
62
|
-
###
|
|
63
|
-
|
|
64
|
-
#### `new Items<I, E>(items?, options?)`
|
|
74
|
+
### Items Class
|
|
65
75
|
|
|
66
|
-
|
|
76
|
+
#### Constructor
|
|
67
77
|
|
|
68
78
|
```typescript
|
|
69
|
-
|
|
70
|
-
|
|
79
|
+
new Items<E>(items?: Iterable<E>, options?: ItemsOptions<E>)
|
|
80
|
+
```
|
|
71
81
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
{ id: 2, name: 'Bob' }
|
|
76
|
-
])
|
|
82
|
+
**Options:**
|
|
83
|
+
- `selectId?: (entity: E) => ItemId` - Custom ID selector (defaults to `entity.id`)
|
|
84
|
+
- `sortComparer?: false | ((a: E, b: E) => number)` - Sorting comparator for maintaining order
|
|
77
85
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
86
|
+
**Example:**
|
|
87
|
+
```typescript
|
|
88
|
+
const items = new Items(users, {
|
|
89
|
+
selectId: (user) => user.userId,
|
|
90
|
+
sortComparer: (a, b) => a.name.localeCompare(b.name)
|
|
82
91
|
})
|
|
83
92
|
```
|
|
84
93
|
|
|
85
|
-
|
|
94
|
+
#### Methods
|
|
95
|
+
|
|
96
|
+
##### `add(items: Iterable<E>): Items<E>`
|
|
86
97
|
|
|
87
|
-
|
|
98
|
+
Adds new items to the collection. Items with existing IDs are ignored.
|
|
88
99
|
|
|
89
100
|
```typescript
|
|
90
|
-
|
|
101
|
+
const updated = users.add([
|
|
102
|
+
{ id: 4, name: 'David', age: 40 }
|
|
103
|
+
])
|
|
91
104
|
```
|
|
92
105
|
|
|
93
|
-
|
|
106
|
+
##### `update(selector: Selector<E>, updater: Updater<E>): Items<E>`
|
|
94
107
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
- **`insert(entity)`** – Adds a single entity (skips if already exists)
|
|
98
|
-
- **`insertMany(entities)`** – Adds multiple entities (skips duplicates). Accepts an `Iterable<E>` (array, Set, etc.)
|
|
108
|
+
Updates selected items with partial data or a function. Can create new items if using a function updater.
|
|
99
109
|
|
|
100
110
|
```typescript
|
|
101
|
-
//
|
|
102
|
-
|
|
111
|
+
// Update with partial data
|
|
112
|
+
const updated = users.update(1, { age: 31 })
|
|
113
|
+
|
|
114
|
+
// Update with function
|
|
115
|
+
const updated = users.update([1, 2], (user) => ({
|
|
116
|
+
...user!,
|
|
117
|
+
age: user!.age + 1
|
|
118
|
+
}))
|
|
119
|
+
|
|
120
|
+
// Update with selector function
|
|
121
|
+
const updated = users.update(
|
|
122
|
+
(s) => s.filter((u) => u.age > 25),
|
|
123
|
+
{ active: true }
|
|
124
|
+
)
|
|
125
|
+
```
|
|
103
126
|
|
|
104
|
-
|
|
105
|
-
items.insertMany([
|
|
106
|
-
{ id: 1, name: 'Alice' },
|
|
107
|
-
{ id: 2, name: 'Bob' }
|
|
108
|
-
])
|
|
127
|
+
##### `merge(items: Iterable<E>): Items<E>`
|
|
109
128
|
|
|
110
|
-
|
|
111
|
-
const usersSet = new Set([
|
|
112
|
-
{ id: 1, name: 'Alice' },
|
|
113
|
-
{ id: 2, name: 'Bob' }
|
|
114
|
-
])
|
|
115
|
-
items.insertMany(usersSet)
|
|
129
|
+
Merges items into the collection. Adds new items and overwrites existing ones.
|
|
116
130
|
|
|
117
|
-
|
|
118
|
-
|
|
131
|
+
```typescript
|
|
132
|
+
const merged = users.merge([
|
|
133
|
+
{ id: 1, name: 'Alice', age: 31 }, // Updates existing
|
|
134
|
+
{ id: 5, name: 'Eve', age: 28 } // Adds new
|
|
135
|
+
])
|
|
119
136
|
```
|
|
120
137
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
- **`upsert(entity)`** – Adds or **merges** a single entity (extends existing properties)
|
|
124
|
-
- **`upsertMany(entities)`** – Adds or **merges** multiple entities. Accepts an `Iterable<E>` (array, Set, etc.)
|
|
138
|
+
##### `remove(selector: Selector<E>): Items<E>`
|
|
125
139
|
|
|
126
|
-
|
|
140
|
+
Removes selected items from the collection.
|
|
127
141
|
|
|
128
142
|
```typescript
|
|
129
|
-
//
|
|
130
|
-
|
|
143
|
+
// Remove by ID
|
|
144
|
+
const removed = users.remove(1)
|
|
131
145
|
|
|
132
|
-
//
|
|
133
|
-
const
|
|
134
|
-
const updated = items.upsert({ id: 1, name: 'Alice Updated' })
|
|
135
|
-
// Result: { id: 1, name: 'Alice Updated', age: 25 }
|
|
136
|
-
// Note: age is preserved!
|
|
146
|
+
// Remove by IDs
|
|
147
|
+
const removed = users.remove([1, 2])
|
|
137
148
|
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
{ id: 1, name: 'Alice' },
|
|
141
|
-
{ id: 2, name: 'Bob' }
|
|
142
|
-
])
|
|
143
|
-
|
|
144
|
-
// Adding new properties
|
|
145
|
-
const items = new Items([{ id: 1, name: 'Alice' }])
|
|
146
|
-
const updated = items.upsert({ id: 1, age: 25 })
|
|
147
|
-
// Result: { id: 1, name: 'Alice', age: 25 }
|
|
148
|
-
// Note: name is preserved, age is added
|
|
149
|
+
// Remove by function
|
|
150
|
+
const removed = users.remove((s) => s.filter((u) => u.age < 30))
|
|
149
151
|
```
|
|
150
152
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
- **`set(entity)`** – Adds or **completely replaces** a single entity
|
|
154
|
-
- **`setMany(entities)`** – Adds or **completely replaces** multiple entities. Accepts an `Iterable<E>` (array, Set, etc.)
|
|
153
|
+
##### `pick(selector: Selector<E>): Items<E>`
|
|
155
154
|
|
|
156
|
-
|
|
155
|
+
Returns a new Items instance containing only the selected items.
|
|
157
156
|
|
|
158
157
|
```typescript
|
|
159
|
-
|
|
160
|
-
items.set({ id: 1, name: 'Alice' })
|
|
161
|
-
|
|
162
|
-
// Set existing entity - REPLACES completely
|
|
163
|
-
const items = new Items([{ id: 1, name: 'Alice', age: 25 }])
|
|
164
|
-
const updated = items.set({ id: 1, name: 'Alice Updated' })
|
|
165
|
-
// Result: { id: 1, name: 'Alice Updated' }
|
|
166
|
-
// Note: age is removed!
|
|
167
|
-
|
|
168
|
-
// Set multiple entities
|
|
169
|
-
items.setMany([
|
|
170
|
-
{ id: 1, name: 'Alice' },
|
|
171
|
-
{ id: 2, name: 'Bob' }
|
|
172
|
-
])
|
|
158
|
+
const picked = users.pick((s) => s.filter((u) => u.age >= 30))
|
|
173
159
|
```
|
|
174
160
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
- **`update(id, updater)`** – Updates a single entity by ID
|
|
178
|
-
- **`updateMany(selector, updater)`** – Updates multiple entities matching selector
|
|
161
|
+
##### `select(selector: Selector<E>): E[] | E | undefined`
|
|
179
162
|
|
|
180
|
-
|
|
163
|
+
Selects items and returns them. Returns an array for multiple selections, or a single item (or undefined) when using SingleSelect methods like `at()` or `on()`.
|
|
181
164
|
|
|
182
165
|
```typescript
|
|
183
|
-
//
|
|
184
|
-
items.
|
|
166
|
+
// Select by ID - returns single item or undefined
|
|
167
|
+
const user = items.select(1)
|
|
185
168
|
|
|
186
|
-
//
|
|
187
|
-
items.
|
|
169
|
+
// Select by IDs - returns array
|
|
170
|
+
const users = items.select([1, 2])
|
|
188
171
|
|
|
189
|
-
//
|
|
190
|
-
items.
|
|
172
|
+
// Select with function returning Select - returns array
|
|
173
|
+
const adults = items.select((s) => s.filter((u) => u.age >= 30))
|
|
191
174
|
|
|
192
|
-
//
|
|
193
|
-
items.
|
|
194
|
-
|
|
195
|
-
{ age: 26 }
|
|
196
|
-
)
|
|
175
|
+
// Select with function returning SingleSelect - returns single item or undefined
|
|
176
|
+
const firstUser = items.select((s) => s.at(0))
|
|
177
|
+
const oldest = items.select((s) => s.sort((a, b) => b.age - a.age).at(0))
|
|
197
178
|
```
|
|
198
179
|
|
|
199
|
-
|
|
180
|
+
##### `selectId(selector: Selector<E>): ItemId[] | ItemId | undefined`
|
|
200
181
|
|
|
201
|
-
|
|
202
|
-
- **`removeMany(selector)`** – Removes multiple entities matching selector
|
|
182
|
+
Selects item IDs instead of items. Returns an array of IDs for multiple selections, or a single ID (or undefined) when using SingleSelect methods.
|
|
203
183
|
|
|
204
184
|
```typescript
|
|
205
|
-
//
|
|
206
|
-
items.
|
|
185
|
+
// Get IDs for filtered items - returns array
|
|
186
|
+
const adultIds = items.selectId((s) => s.filter((u) => u.age >= 30))
|
|
207
187
|
|
|
208
|
-
//
|
|
209
|
-
items.
|
|
188
|
+
// Get ID of first item - returns ItemId or undefined
|
|
189
|
+
const firstId = items.selectId((s) => s.at(0))
|
|
210
190
|
|
|
211
|
-
//
|
|
212
|
-
items.
|
|
191
|
+
// Get ID of oldest user - returns ItemId or undefined
|
|
192
|
+
const oldestId = items.selectId((s) => s.sort((a, b) => b.age - a.age).at(0))
|
|
213
193
|
```
|
|
214
194
|
|
|
215
|
-
|
|
195
|
+
##### `extractId(entity: E): ItemId`
|
|
196
|
+
|
|
197
|
+
Extracts the ID from an entity using the configured ID selector.
|
|
216
198
|
|
|
217
199
|
```typescript
|
|
218
|
-
|
|
200
|
+
const user = { id: 5, name: 'Eve', age: 28 }
|
|
201
|
+
const id = items.extractId(user) // Returns: 5
|
|
202
|
+
|
|
203
|
+
// With custom selector
|
|
204
|
+
const products = new Items(items, {
|
|
205
|
+
selectId: (p) => p.sku
|
|
206
|
+
})
|
|
207
|
+
const product = { sku: 'ABC123', name: 'Widget' }
|
|
208
|
+
const sku = products.extractId(product) // Returns: 'ABC123'
|
|
219
209
|
```
|
|
220
210
|
|
|
221
|
-
|
|
211
|
+
##### `clear(): Items<E>`
|
|
222
212
|
|
|
223
|
-
|
|
213
|
+
Returns an empty Items instance with the same options.
|
|
224
214
|
|
|
225
215
|
```typescript
|
|
226
|
-
|
|
227
|
-
items.filter(1)
|
|
228
|
-
|
|
229
|
-
// Filter by multiple IDs
|
|
230
|
-
items.filter([1, 2])
|
|
231
|
-
|
|
232
|
-
// Filter by predicate
|
|
233
|
-
items.filter(user => user.age >= 30)
|
|
216
|
+
const empty = users.clear()
|
|
234
217
|
```
|
|
235
218
|
|
|
236
|
-
|
|
219
|
+
##### `every(check: (entity: E) => boolean): boolean`
|
|
237
220
|
|
|
238
|
-
|
|
239
|
-
- **`getEntities()`** – Returns Map of entities
|
|
240
|
-
- **`select(id)`** – Returns entity by ID or `undefined`
|
|
221
|
+
Tests whether all items pass the provided function.
|
|
241
222
|
|
|
242
223
|
```typescript
|
|
243
|
-
|
|
244
|
-
items.getEntities() // Map { 1 => {...}, 2 => {...} }
|
|
245
|
-
items.select(1) // { id: 1, name: 'Alice' }
|
|
224
|
+
const allAdults = users.every((u) => u.age >= 18)
|
|
246
225
|
```
|
|
247
226
|
|
|
248
|
-
|
|
227
|
+
##### `some(check: (entity: E) => boolean): boolean`
|
|
249
228
|
|
|
250
|
-
|
|
251
|
-
- **`hasMany(selector)`** – Checks if entities exist matching selector
|
|
229
|
+
Tests whether at least one item passes the provided function.
|
|
252
230
|
|
|
253
231
|
```typescript
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
items.has(99) // false
|
|
232
|
+
const hasSenior = users.some((u) => u.age >= 65)
|
|
233
|
+
```
|
|
257
234
|
|
|
258
|
-
|
|
259
|
-
items.hasMany([1, 2]) // true
|
|
260
|
-
items.hasMany([1, 99]) // false
|
|
235
|
+
##### `has(id: ItemId): boolean`
|
|
261
236
|
|
|
262
|
-
|
|
263
|
-
items.hasMany(user => user.age >= 30) // true
|
|
264
|
-
items.hasMany(user => user.age >= 100) // false
|
|
237
|
+
Checks if an item with the given ID exists.
|
|
265
238
|
|
|
266
|
-
|
|
267
|
-
|
|
239
|
+
```typescript
|
|
240
|
+
const exists = users.has(1)
|
|
268
241
|
```
|
|
269
242
|
|
|
270
|
-
|
|
243
|
+
##### `get(id: ItemId): E | undefined`
|
|
244
|
+
|
|
245
|
+
Gets an item by ID.
|
|
271
246
|
|
|
272
247
|
```typescript
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
{ id: 2, name: 'Bob', age: 30 },
|
|
276
|
-
{ id: 3, name: 'Charlie', age: 35 }
|
|
277
|
-
])
|
|
248
|
+
const user = users.get(1)
|
|
249
|
+
```
|
|
278
250
|
|
|
279
|
-
|
|
280
|
-
items.every(user => user.age >= 18) // true
|
|
251
|
+
##### `getIds(): ItemId[]`
|
|
281
252
|
|
|
282
|
-
|
|
283
|
-
items.every(user => user.age >= 65) // false
|
|
253
|
+
Returns an array of all item IDs.
|
|
284
254
|
|
|
285
|
-
|
|
286
|
-
|
|
255
|
+
```typescript
|
|
256
|
+
const ids = users.getIds() // [1, 2, 3]
|
|
287
257
|
```
|
|
288
258
|
|
|
289
|
-
|
|
259
|
+
##### `getEntities(): E[]`
|
|
260
|
+
|
|
261
|
+
Returns an array of all items.
|
|
290
262
|
|
|
291
263
|
```typescript
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
{ id: 2, name: 'Bob', age: 30 },
|
|
295
|
-
{ id: 3, name: 'Charlie', age: 35 }
|
|
296
|
-
])
|
|
264
|
+
const allUsers = users.getEntities()
|
|
265
|
+
```
|
|
297
266
|
|
|
298
|
-
|
|
299
|
-
items.some(user => user.age < 30) // true
|
|
267
|
+
##### `length: number`
|
|
300
268
|
|
|
301
|
-
|
|
302
|
-
items.some(user => user.name === 'Dave') // false
|
|
269
|
+
Gets the number of items in the collection.
|
|
303
270
|
|
|
304
|
-
|
|
305
|
-
|
|
271
|
+
```typescript
|
|
272
|
+
console.log(users.length) // 3
|
|
306
273
|
```
|
|
307
274
|
|
|
308
|
-
#### Pagination
|
|
309
275
|
|
|
310
|
-
|
|
276
|
+
#### Static Methods
|
|
277
|
+
|
|
278
|
+
##### `Items.compare<E>(base: Items<E>, to: Items<E>): ItemsDiff`
|
|
279
|
+
|
|
280
|
+
Compares two Items instances and returns the differences.
|
|
311
281
|
|
|
312
282
|
```typescript
|
|
313
|
-
const
|
|
283
|
+
const before = new Items(users)
|
|
284
|
+
const after = before.update(1, { age: 31 })
|
|
285
|
+
|
|
286
|
+
const diff = Items.compare(before, after)
|
|
287
|
+
console.log(diff)
|
|
314
288
|
// {
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
// hasNext: true,
|
|
319
|
-
// hasPrevious: false,
|
|
320
|
-
// total: 25,
|
|
321
|
-
// totalPages: 3
|
|
289
|
+
// added: [],
|
|
290
|
+
// removed: [],
|
|
291
|
+
// updated: [{ id: 1, changes: [...] }]
|
|
322
292
|
// }
|
|
323
293
|
```
|
|
324
294
|
|
|
325
|
-
|
|
295
|
+
### Select Class
|
|
326
296
|
|
|
327
|
-
|
|
297
|
+
The Select class provides a fluent API for querying and transforming item collections.
|
|
328
298
|
|
|
329
|
-
|
|
330
|
-
- `added`: IDs of entities that exist in the new collection but not in the base
|
|
331
|
-
- `removed`: IDs of entities that exist in the base but not in the new collection
|
|
332
|
-
- `updated`: Array of `ItemDiff` objects containing the ID and detailed property changes
|
|
299
|
+
#### Methods
|
|
333
300
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
- `oldValue`: The old value (wrapped in a `DiffHashedObject` with a `value` property)
|
|
338
|
-
- `newValue`: The new value (wrapped in a `DiffHashedObject` with a `value` property)
|
|
301
|
+
##### `take(len: number): Select<E>`
|
|
302
|
+
|
|
303
|
+
Takes the first `n` items.
|
|
339
304
|
|
|
340
305
|
```typescript
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const updated = base.insert([{ id: 2, name: 'Bob' }])
|
|
344
|
-
const diff = updated.diff(base)
|
|
345
|
-
// {
|
|
346
|
-
// added: [2],
|
|
347
|
-
// removed: [],
|
|
348
|
-
// updated: []
|
|
349
|
-
// }
|
|
306
|
+
users.select((s) => s.take(2))
|
|
307
|
+
```
|
|
350
308
|
|
|
351
|
-
|
|
352
|
-
const base = new Items([
|
|
353
|
-
{ id: 1, name: 'Alice' },
|
|
354
|
-
{ id: 2, name: 'Bob' }
|
|
355
|
-
])
|
|
356
|
-
const updated = base.remove(2)
|
|
357
|
-
const diff = updated.diff(base)
|
|
358
|
-
// {
|
|
359
|
-
// added: [],
|
|
360
|
-
// removed: [2],
|
|
361
|
-
// updated: []
|
|
362
|
-
// }
|
|
309
|
+
##### `skip(len: number): Select<E>`
|
|
363
310
|
|
|
364
|
-
|
|
365
|
-
const base = new Items([{ id: 1, name: 'Alice', age: 25 }])
|
|
366
|
-
const updated = base.update(1, { age: 26 })
|
|
367
|
-
const diff = updated.diff(base)
|
|
368
|
-
// {
|
|
369
|
-
// added: [],
|
|
370
|
-
// removed: [],
|
|
371
|
-
// updated: [
|
|
372
|
-
// {
|
|
373
|
-
// id: 1,
|
|
374
|
-
// changes: [
|
|
375
|
-
// {
|
|
376
|
-
// key: 'age',
|
|
377
|
-
// type: 'changed',
|
|
378
|
-
// oldValue: { value: 25, ... },
|
|
379
|
-
// newValue: { value: 26, ... }
|
|
380
|
-
// }
|
|
381
|
-
// ]
|
|
382
|
-
// }
|
|
383
|
-
// ]
|
|
384
|
-
// }
|
|
311
|
+
Skips the first `n` items.
|
|
385
312
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const diff = updated.diff(base)
|
|
390
|
-
// {
|
|
391
|
-
// added: [],
|
|
392
|
-
// removed: [],
|
|
393
|
-
// updated: [
|
|
394
|
-
// {
|
|
395
|
-
// id: 1,
|
|
396
|
-
// changes: [
|
|
397
|
-
// { key: 'age', type: 'added', newValue: { value: 25, ... } }
|
|
398
|
-
// ]
|
|
399
|
-
// }
|
|
400
|
-
// ]
|
|
401
|
-
// }
|
|
313
|
+
```typescript
|
|
314
|
+
users.select((s) => s.skip(1))
|
|
315
|
+
```
|
|
402
316
|
|
|
403
|
-
|
|
404
|
-
const base = new Items([{ id: 1, name: 'Alice', age: 25 }])
|
|
405
|
-
const updated = base.set([{ id: 1, name: 'Alice' }])
|
|
406
|
-
const diff = updated.diff(base)
|
|
407
|
-
// {
|
|
408
|
-
// added: [],
|
|
409
|
-
// removed: [],
|
|
410
|
-
// updated: [
|
|
411
|
-
// {
|
|
412
|
-
// id: 1,
|
|
413
|
-
// changes: [
|
|
414
|
-
// { key: 'age', type: 'removed', oldValue: { value: 25, ... } }
|
|
415
|
-
// ]
|
|
416
|
-
// }
|
|
417
|
-
// ]
|
|
418
|
-
// }
|
|
317
|
+
##### `filter(testFn: (entry: E, id: ItemId, index: number) => boolean): Select<E>`
|
|
419
318
|
|
|
420
|
-
|
|
421
|
-
const base = new Items([
|
|
422
|
-
{ id: 1, name: 'Alice', age: 25 },
|
|
423
|
-
{ id: 2, name: 'Bob', age: 30 },
|
|
424
|
-
{ id: 3, name: 'Charlie', age: 35 }
|
|
425
|
-
])
|
|
426
|
-
const updated = base
|
|
427
|
-
.remove(3) // Remove Charlie
|
|
428
|
-
.update(1, { age: 26 }) // Update Alice's age
|
|
429
|
-
.insert([{ id: 4, name: 'Dave', age: 40 }]) // Add Dave
|
|
430
|
-
|
|
431
|
-
const diff = updated.diff(base)
|
|
432
|
-
// {
|
|
433
|
-
// added: [4],
|
|
434
|
-
// removed: [3],
|
|
435
|
-
// updated: [
|
|
436
|
-
// {
|
|
437
|
-
// id: 1,
|
|
438
|
-
// changes: [
|
|
439
|
-
// {
|
|
440
|
-
// key: 'age',
|
|
441
|
-
// type: 'changed',
|
|
442
|
-
// oldValue: { value: 25, ... },
|
|
443
|
-
// newValue: { value: 26, ... }
|
|
444
|
-
// }
|
|
445
|
-
// ]
|
|
446
|
-
// }
|
|
447
|
-
// ]
|
|
448
|
-
// }
|
|
319
|
+
Filters items based on a predicate function.
|
|
449
320
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
// Access changed values
|
|
454
|
-
diff.updated.forEach(item => {
|
|
455
|
-
console.log(`Entity ${item.id} was updated`)
|
|
456
|
-
item.changes.forEach(change => {
|
|
457
|
-
if (change.type === 'changed') {
|
|
458
|
-
console.log(` ${change.key}: ${change.oldValue?.value} -> ${change.newValue?.value}`)
|
|
459
|
-
} else if (change.type === 'added') {
|
|
460
|
-
console.log(` ${change.key}: added with value ${change.newValue?.value}`)
|
|
461
|
-
} else if (change.type === 'removed') {
|
|
462
|
-
console.log(` ${change.key}: removed (was ${change.oldValue?.value})`)
|
|
463
|
-
}
|
|
464
|
-
})
|
|
465
|
-
})
|
|
321
|
+
```typescript
|
|
322
|
+
users.select((s) => s.filter((user, id, index) => user.age > 25))
|
|
323
|
+
```
|
|
466
324
|
|
|
467
|
-
|
|
468
|
-
interface UserWithAddress {
|
|
469
|
-
id: number
|
|
470
|
-
name: string
|
|
471
|
-
address: { city: string; country: string }
|
|
472
|
-
}
|
|
325
|
+
##### `revert(): Select<E>`
|
|
473
326
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
address: { city: 'LA', country: 'USA' }
|
|
479
|
-
})
|
|
480
|
-
const diff = updated.diff(base)
|
|
481
|
-
// Detects changes in nested object properties
|
|
327
|
+
Reverses the order of items.
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
users.select((s) => s.revert())
|
|
482
331
|
```
|
|
483
332
|
|
|
484
|
-
|
|
333
|
+
##### `sort(sortFn: (a: E, b: E) => number): Select<E>`
|
|
485
334
|
|
|
486
|
-
|
|
335
|
+
Sorts items using a comparator function.
|
|
487
336
|
|
|
488
337
|
```typescript
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
338
|
+
users.select((s) => s.sort((a, b) => a.age - b.age))
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
##### `at(index: number): SingleSelect<E>`
|
|
342
|
+
|
|
343
|
+
Returns a SingleSelect for the item at the given index.
|
|
492
344
|
|
|
493
|
-
|
|
494
|
-
const
|
|
345
|
+
```typescript
|
|
346
|
+
const select = new Select(users.getIds(), users)
|
|
347
|
+
const single = select.at(0)
|
|
495
348
|
```
|
|
496
349
|
|
|
497
|
-
|
|
350
|
+
##### `from(entities: Iterable<E>): Select<E>`
|
|
351
|
+
|
|
352
|
+
Creates a new Select from the given entities.
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
const select = new Select(users.getIds(), users)
|
|
356
|
+
const newSelect = select.from([
|
|
357
|
+
{ id: 2, name: 'Bob', age: 25 }
|
|
358
|
+
])
|
|
359
|
+
```
|
|
498
360
|
|
|
499
|
-
|
|
361
|
+
##### `on(entry: E): SingleSelect<E>`
|
|
500
362
|
|
|
501
|
-
|
|
363
|
+
Creates a SingleSelect for the given entity.
|
|
502
364
|
|
|
503
365
|
```typescript
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
}
|
|
366
|
+
const select = new Select(users.getIds(), users)
|
|
367
|
+
const single = select.on({ id: 1, name: 'Alice', age: 30 })
|
|
368
|
+
```
|
|
508
369
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
370
|
+
### Chaining Selectors
|
|
371
|
+
|
|
372
|
+
Selectors can be chained for powerful queries:
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
const result = users.select((s) =>
|
|
376
|
+
s.filter((u) => u.age >= 25)
|
|
377
|
+
.sort((a, b) => a.age - b.age)
|
|
378
|
+
.skip(1)
|
|
379
|
+
.take(2)
|
|
380
|
+
)
|
|
512
381
|
```
|
|
513
382
|
|
|
514
|
-
|
|
383
|
+
## Diff Tracking
|
|
515
384
|
|
|
516
|
-
|
|
385
|
+
Track changes between two Items instances:
|
|
517
386
|
|
|
518
387
|
```typescript
|
|
519
|
-
|
|
520
|
-
const items = new Items<number, User>([], {
|
|
521
|
-
sortComparer: (a, b) => a.name.localeCompare(b.name)
|
|
522
|
-
})
|
|
388
|
+
import { Items, itemsDiff } from '@wszerad/items'
|
|
523
389
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
390
|
+
const before = new Items([
|
|
391
|
+
{ id: 1, name: 'Alice', age: 30 },
|
|
392
|
+
{ id: 2, name: 'Bob', age: 25 }
|
|
393
|
+
])
|
|
394
|
+
|
|
395
|
+
const after = before
|
|
396
|
+
.update(1, { age: 31 })
|
|
397
|
+
.add([{ id: 3, name: 'Charlie', age: 35 }])
|
|
398
|
+
.remove(2)
|
|
399
|
+
|
|
400
|
+
const diff = itemsDiff(before, after)
|
|
401
|
+
console.log(diff)
|
|
402
|
+
// {
|
|
403
|
+
// added: [3],
|
|
404
|
+
// removed: [2],
|
|
405
|
+
// updated: [{ id: 1, changes: [...] }]
|
|
406
|
+
// }
|
|
528
407
|
```
|
|
529
408
|
|
|
530
|
-
|
|
409
|
+
## Advanced Usage
|
|
410
|
+
|
|
411
|
+
### Custom ID Selector
|
|
412
|
+
|
|
413
|
+
Use a custom field as the ID:
|
|
531
414
|
|
|
532
415
|
```typescript
|
|
533
416
|
interface Product {
|
|
534
|
-
|
|
417
|
+
sku: string
|
|
535
418
|
name: string
|
|
536
419
|
price: number
|
|
537
|
-
inStock: boolean
|
|
538
420
|
}
|
|
539
421
|
|
|
540
|
-
const products = new Items<
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
]
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
const allInStock = products.every(p => p.inStock) // false
|
|
422
|
+
const products = new Items<Product>(
|
|
423
|
+
[
|
|
424
|
+
{ sku: 'ABC123', name: 'Widget', price: 9.99 },
|
|
425
|
+
{ sku: 'XYZ789', name: 'Gadget', price: 19.99 }
|
|
426
|
+
],
|
|
427
|
+
{ selectId: (product) => product.sku }
|
|
428
|
+
)
|
|
548
429
|
|
|
549
|
-
|
|
550
|
-
|
|
430
|
+
const widget = products.get('ABC123')
|
|
431
|
+
```
|
|
551
432
|
|
|
552
|
-
|
|
553
|
-
const allNamed = products.every(p => p.name.length > 0) // true
|
|
433
|
+
### Automatic Sorting
|
|
554
434
|
|
|
555
|
-
|
|
556
|
-
const hasCheap = products.some(p => p.price < 30) // true
|
|
435
|
+
Keep items sorted automatically:
|
|
557
436
|
|
|
558
|
-
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
errors.push('All products must have names')
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
if (items.some(p => p.price > 10000)) {
|
|
571
|
-
errors.push('Warning: Some products are very expensive')
|
|
437
|
+
```typescript
|
|
438
|
+
const users = new Items(
|
|
439
|
+
[
|
|
440
|
+
{ id: 3, name: 'Charlie', age: 35 },
|
|
441
|
+
{ id: 1, name: 'Alice', age: 30 },
|
|
442
|
+
{ id: 2, name: 'Bob', age: 25 }
|
|
443
|
+
],
|
|
444
|
+
{
|
|
445
|
+
sortComparer: (a, b) => a.age - b.age
|
|
572
446
|
}
|
|
573
|
-
|
|
574
|
-
return errors
|
|
575
|
-
}
|
|
576
|
-
```
|
|
577
|
-
|
|
578
|
-
## Selectors
|
|
447
|
+
)
|
|
579
448
|
|
|
580
|
-
|
|
449
|
+
console.log(users.getIds()) // [2, 1, 3] - sorted by age
|
|
450
|
+
```
|
|
581
451
|
|
|
582
|
-
###
|
|
583
|
-
Accept a single ID directly:
|
|
584
|
-
- `insert(entity)`, `upsert(entity)`, `set(entity)`
|
|
585
|
-
- `update(id, updater)` – `items.update(1, { age: 26 })`
|
|
586
|
-
- `remove(id)` – `items.remove(1)`
|
|
587
|
-
- `has(id)` – `items.has(1)`
|
|
452
|
+
### Complex Updates
|
|
588
453
|
|
|
589
|
-
|
|
590
|
-
Accept a `Selector` parameter that can be:
|
|
591
|
-
1. **Array of IDs** – `items.removeMany([1, 2, 3])`
|
|
592
|
-
2. **Predicate function** – `items.filter(user => user.age >= 30)`
|
|
454
|
+
Perform complex transformations:
|
|
593
455
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
456
|
+
```typescript
|
|
457
|
+
// Increment age for all users over 25
|
|
458
|
+
const updated = users.update(
|
|
459
|
+
(s) => s.filter((u) => u.age > 25),
|
|
460
|
+
(user) => ({
|
|
461
|
+
...user!,
|
|
462
|
+
age: user!.age + 1,
|
|
463
|
+
senior: user!.age >= 65
|
|
464
|
+
})
|
|
465
|
+
)
|
|
466
|
+
```
|
|
599
467
|
|
|
600
|
-
|
|
468
|
+
### Immutability
|
|
601
469
|
|
|
602
|
-
All operations
|
|
470
|
+
All operations are immutable:
|
|
603
471
|
|
|
604
472
|
```typescript
|
|
605
|
-
const
|
|
606
|
-
|
|
473
|
+
const original = new Items([
|
|
474
|
+
{ id: 1, name: 'Alice', age: 30 }
|
|
475
|
+
])
|
|
476
|
+
|
|
477
|
+
const updated = original.update(1, { age: 31 })
|
|
607
478
|
|
|
608
|
-
console.log(
|
|
609
|
-
console.log(
|
|
479
|
+
console.log(original.get(1)?.age) // 30 - unchanged
|
|
480
|
+
console.log(updated.get(1)?.age) // 31 - new instance
|
|
610
481
|
```
|
|
611
482
|
|
|
612
|
-
|
|
483
|
+
### Iteration
|
|
613
484
|
|
|
614
|
-
|
|
485
|
+
Items are iterable:
|
|
615
486
|
|
|
616
487
|
```typescript
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
name: string
|
|
620
|
-
age: number
|
|
488
|
+
for (const user of users) {
|
|
489
|
+
console.log(user.name)
|
|
621
490
|
}
|
|
622
491
|
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
// ✅ Type-safe
|
|
626
|
-
items.insert({ id: 1, name: 'Alice', age: 25 })
|
|
627
|
-
|
|
628
|
-
// ❌ Type error
|
|
629
|
-
items.insert({ id: 1, name: 'Alice' }) // Missing 'age'
|
|
492
|
+
const array = Array.from(users)
|
|
493
|
+
const names = [...users].map(u => u.name)
|
|
630
494
|
```
|
|
631
495
|
|
|
632
|
-
|
|
496
|
+
### Single Item Selection
|
|
633
497
|
|
|
634
|
-
|
|
498
|
+
The `select()` method automatically returns a single item (or undefined) when using `at()` or `on()` methods:
|
|
635
499
|
|
|
636
500
|
```typescript
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
id:
|
|
641
|
-
|
|
642
|
-
completed: boolean
|
|
643
|
-
}
|
|
501
|
+
const users = new Items<User>([
|
|
502
|
+
{ id: 1, name: 'Alice', age: 30 },
|
|
503
|
+
{ id: 2, name: 'Bob', age: 25 },
|
|
504
|
+
{ id: 3, name: 'Charlie', age: 35 }
|
|
505
|
+
])
|
|
644
506
|
|
|
645
|
-
|
|
507
|
+
// Select by index - returns single User or undefined
|
|
508
|
+
const firstUser = users.select((s) => s.at(0))
|
|
509
|
+
console.log(firstUser?.name) // 'Alice'
|
|
646
510
|
|
|
647
|
-
|
|
648
|
-
|
|
511
|
+
const secondUser = users.select((s) => s.at(1))
|
|
512
|
+
console.log(secondUser?.name) // 'Bob'
|
|
649
513
|
|
|
650
|
-
//
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
{ id: 3, text: 'Deploy', completed: false }
|
|
654
|
-
])
|
|
514
|
+
// Select by ID - returns single User or undefined
|
|
515
|
+
const user = users.select(2)
|
|
516
|
+
console.log(user?.name) // 'Bob'
|
|
655
517
|
|
|
656
|
-
//
|
|
657
|
-
|
|
518
|
+
// Out of bounds returns undefined
|
|
519
|
+
const notFound = users.select((s) => s.at(10))
|
|
520
|
+
console.log(notFound) // undefined
|
|
658
521
|
|
|
659
|
-
//
|
|
660
|
-
const
|
|
522
|
+
// Chain operations before selecting single item
|
|
523
|
+
const oldestUser = users.select((s) =>
|
|
524
|
+
s.sort((a, b) => b.age - a.age).at(0)
|
|
525
|
+
)
|
|
526
|
+
console.log(oldestUser?.name) // 'Charlie' (age 35)
|
|
661
527
|
|
|
662
|
-
//
|
|
663
|
-
|
|
528
|
+
// Get just the ID instead of the full item
|
|
529
|
+
const oldestId = users.selectId((s) =>
|
|
530
|
+
s.sort((a, b) => b.age - a.age).at(0)
|
|
531
|
+
)
|
|
532
|
+
console.log(oldestId) // 3
|
|
664
533
|
```
|
|
665
534
|
|
|
666
|
-
|
|
535
|
+
## TypeScript Support
|
|
667
536
|
|
|
668
|
-
|
|
669
|
-
import { Items } from 'items'
|
|
537
|
+
Full TypeScript support with generics:
|
|
670
538
|
|
|
539
|
+
```typescript
|
|
671
540
|
interface User {
|
|
672
541
|
id: number
|
|
673
542
|
name: string
|
|
674
|
-
|
|
675
|
-
age?: number
|
|
543
|
+
age: number
|
|
676
544
|
}
|
|
677
545
|
|
|
678
|
-
|
|
679
|
-
let users = new Items<number, User>([
|
|
680
|
-
{ id: 1, name: 'Alice', email: 'alice@example.com', age: 25 }
|
|
681
|
-
])
|
|
546
|
+
const users = new Items<User>() // Fully typed
|
|
682
547
|
|
|
683
|
-
//
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
users = users.insert({ id: 2, name: 'Bob' })
|
|
689
|
-
// Result: Adds Bob with id: 2 since it doesn't exist
|
|
690
|
-
|
|
691
|
-
// UPSERT - merges properties (adds new, extends existing)
|
|
692
|
-
users = users.upsert({ id: 1, name: 'Alicia', age: 26 })
|
|
693
|
-
// Result: { id: 1, name: 'Alicia', email: 'alice@example.com', age: 26 }
|
|
694
|
-
// Note: name and age updated, email preserved!
|
|
695
|
-
|
|
696
|
-
// SET - completely replaces entity
|
|
697
|
-
users = users.set({ id: 1, name: 'Alice' })
|
|
698
|
-
// Result: { id: 1, name: 'Alice' }
|
|
699
|
-
// Note: email and age are removed!
|
|
700
|
-
|
|
701
|
-
// UPDATE - merges partial update with existing entity
|
|
702
|
-
users = users.update(1, { age: 27 })
|
|
703
|
-
// Result: { id: 1, name: 'Alice', age: 27 }
|
|
704
|
-
// Note: age added, name preserved
|
|
705
|
-
|
|
706
|
-
// Batch operations with *Many methods
|
|
707
|
-
users = users.insertMany([
|
|
708
|
-
{ id: 3, name: 'Charlie' },
|
|
709
|
-
{ id: 4, name: 'Dave' }
|
|
710
|
-
])
|
|
711
|
-
|
|
712
|
-
users = users.upsertMany([
|
|
713
|
-
{ id: 1, age: 28 },
|
|
714
|
-
{ id: 3, email: 'charlie@example.com' }
|
|
715
|
-
])
|
|
716
|
-
|
|
717
|
-
users = users.setMany([
|
|
718
|
-
{ id: 2, name: 'Robert', age: 35 }
|
|
719
|
-
])
|
|
548
|
+
// Type inference works automatically
|
|
549
|
+
const names: string[] = users
|
|
550
|
+
.select((s) => s.filter((u) => u.age >= 30))
|
|
551
|
+
.map(u => u.name)
|
|
720
552
|
```
|
|
721
553
|
|
|
722
|
-
|
|
554
|
+
## Types
|
|
723
555
|
|
|
724
556
|
```typescript
|
|
725
|
-
|
|
726
|
-
sku: string
|
|
727
|
-
name: string
|
|
728
|
-
price: number
|
|
729
|
-
}
|
|
557
|
+
type ItemId = string | number
|
|
730
558
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
559
|
+
type Selector<E> =
|
|
560
|
+
| ((selector: BaseSelect<E>) => BaseSelect<E>)
|
|
561
|
+
| ItemId
|
|
562
|
+
| Iterable<ItemId>
|
|
734
563
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
564
|
+
type Updater<E> =
|
|
565
|
+
| ((entity: E | undefined) => E)
|
|
566
|
+
| Partial<E>
|
|
738
567
|
|
|
739
|
-
|
|
568
|
+
type ItemsOptions<E> = {
|
|
569
|
+
selectId?: (entity: E) => ItemId
|
|
570
|
+
sortComparer?: false | ((a: E, b: E) => number)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
interface ItemsDiff {
|
|
574
|
+
added: ItemId[]
|
|
575
|
+
removed: ItemId[]
|
|
576
|
+
updated: ItemDiff[]
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
interface ItemDiff {
|
|
580
|
+
id: ItemId
|
|
581
|
+
changes: any[]
|
|
582
|
+
}
|
|
740
583
|
```
|
|
741
584
|
|
|
742
|
-
|
|
585
|
+
## License
|
|
743
586
|
|
|
744
|
-
|
|
745
|
-
const items = new Items<number, User>(
|
|
746
|
-
[
|
|
747
|
-
{ id: 3, name: 'Charlie', age: 35 },
|
|
748
|
-
{ id: 1, name: 'Alice', age: 25 },
|
|
749
|
-
{ id: 2, name: 'Bob', age: 30 }
|
|
750
|
-
],
|
|
751
|
-
{ sortComparer: (a, b) => a.name.localeCompare(b.name) }
|
|
752
|
-
)
|
|
587
|
+
MIT © Wszerad Martynowski
|
|
753
588
|
|
|
754
|
-
|
|
755
|
-
```
|
|
589
|
+
## Contributing
|
|
756
590
|
|
|
757
|
-
|
|
591
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
758
592
|
|
|
759
|
-
##
|
|
593
|
+
## Repository
|
|
760
594
|
|
|
761
|
-
|
|
595
|
+
https://github.com/wszerad/items
|
|
762
596
|
|
|
763
|
-
- `npm test` – runs tests (Vitest)
|
|
764
|
-
- `npm run test:watch` – watch mode
|
|
765
|
-
- `npm run build` – typecheck + bundling (tsc + tsdown)
|
|
766
|
-
- `npm run typecheck` – TypeScript type checking (
|