@wszerad/items 0.1.0 → 0.1.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/.github/dependabot.yml +15 -0
- package/.github/workflows/publish.yml +22 -0
- package/.github/workflows/test.yml +21 -0
- package/.idea/copilot.data.migration.ask2agent.xml +6 -0
- package/.idea/items.iml +0 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/.oxlintrc.json +13 -0
- package/package.json +6 -9
- package/src/Items.ts +215 -0
- package/src/diff.ts +44 -0
- package/src/index.ts +6 -0
- package/src/selectId.ts +7 -0
- package/src/selector.ts +20 -0
- package/src/updater.ts +6 -0
- package/test/index.test.ts +1095 -0
- package/test-type-check.ts +50 -0
- package/tsconfig.json +16 -0
- package/tsdown.config.ts +13 -0
- package/vitest.config.ts +9 -0
- package/dist/index.d.mts +0 -77
- package/dist/index.mjs +0 -194
- package/dist/index.mjs.map +0 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# To get started with Dependabot version updates, you'll need to specify which
|
|
2
|
+
# package ecosystems to update and where the package manifests are located.
|
|
3
|
+
# Please see the documentation for all configuration options:
|
|
4
|
+
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
|
5
|
+
|
|
6
|
+
version: 2
|
|
7
|
+
updates:
|
|
8
|
+
- package-ecosystem: "github-actions"
|
|
9
|
+
directory: "/"
|
|
10
|
+
schedule:
|
|
11
|
+
interval: "weekly"
|
|
12
|
+
- package-ecosystem: npm
|
|
13
|
+
directory: "/"
|
|
14
|
+
schedule:
|
|
15
|
+
interval: "weekly"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [created]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish-chain:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
id-token: write
|
|
12
|
+
contents: read
|
|
13
|
+
packages: write
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v6
|
|
16
|
+
- uses: actions/setup-node@v6
|
|
17
|
+
with:
|
|
18
|
+
node-version: "24"
|
|
19
|
+
- run: npm ci
|
|
20
|
+
- run: npm test
|
|
21
|
+
- run: npm run build
|
|
22
|
+
- run: npm publish --access public
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches:
|
|
6
|
+
- master
|
|
7
|
+
push:
|
|
8
|
+
branches:
|
|
9
|
+
- master
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
name: Run tests for each push
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v6
|
|
17
|
+
- uses: actions/setup-node@v6
|
|
18
|
+
with:
|
|
19
|
+
node-version: "22"
|
|
20
|
+
- run: npm ci
|
|
21
|
+
- run: npm test
|
package/.idea/items.iml
ADDED
|
File without changes
|
package/.idea/vcs.xml
ADDED
package/.oxlintrc.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
|
|
3
|
+
"env": {
|
|
4
|
+
"es2022": true,
|
|
5
|
+
"node": true
|
|
6
|
+
},
|
|
7
|
+
"plugins": ["typescript"],
|
|
8
|
+
"rules": {
|
|
9
|
+
"eslint/no-unused-vars": "error",
|
|
10
|
+
"typescript/no-explicit-any": "warn"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
package/package.json
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wszerad/items",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
|
-
".": "./dist/index.
|
|
6
|
+
".": "./dist/index.mjs",
|
|
7
7
|
"./package.json": "./package.json"
|
|
8
8
|
},
|
|
9
|
-
"main": "./dist/index.
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"dist"
|
|
13
|
-
],
|
|
9
|
+
"main": "./dist/index.mjs",
|
|
10
|
+
"module": "./dist/index.mjs",
|
|
11
|
+
"types": "./dist/index.d.mts",
|
|
14
12
|
"engines": {
|
|
15
13
|
"node": ">=20"
|
|
16
14
|
},
|
|
@@ -47,6 +45,5 @@
|
|
|
47
45
|
"type": "git",
|
|
48
46
|
"url": "git+https://github.com/wszerad/items.git"
|
|
49
47
|
},
|
|
50
|
-
"license": "MIT"
|
|
51
|
-
"module": "./dist/index.js"
|
|
48
|
+
"license": "MIT"
|
|
52
49
|
}
|
package/src/Items.ts
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { defaultSelectId, SelectId, StrOrNum } from './selectId'
|
|
2
|
+
import { selector, Selector, SelectorFn } from './selector'
|
|
3
|
+
import { Updater } from './updater'
|
|
4
|
+
import { itemsDiff } from './diff'
|
|
5
|
+
import { update } from './updater'
|
|
6
|
+
|
|
7
|
+
export type ItemsOptions<E> = {
|
|
8
|
+
selectId?: SelectId<E>
|
|
9
|
+
sortComparer?: false | ((a: E, b: E) => number)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ItemsState<E, I> {
|
|
13
|
+
ids: I[]
|
|
14
|
+
entities: Map<I, E>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class Items<E, I extends StrOrNum = StrOrNum> {
|
|
18
|
+
private state: ItemsState<E, I>
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
items: Iterable<E> = [],
|
|
22
|
+
private options: ItemsOptions<E> = {}
|
|
23
|
+
) {
|
|
24
|
+
const entities = new Map(
|
|
25
|
+
Array
|
|
26
|
+
.from(items)
|
|
27
|
+
.map((item) => {
|
|
28
|
+
const id = this.selectId(item)
|
|
29
|
+
return [id, item]
|
|
30
|
+
})
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
this.state = {
|
|
34
|
+
ids: this.sortIds([...entities.keys()], entities),
|
|
35
|
+
entities
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getIds(): I[] {
|
|
40
|
+
return [...this.state.ids]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getEntities(): Map<I, E> {
|
|
44
|
+
return new Map(this.state.entities)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get length(): number {
|
|
48
|
+
return this.state.ids.length
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
select(id: I): E | undefined {
|
|
52
|
+
return this.state.entities.get(id)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
insert(entity: E) {
|
|
56
|
+
return this.insertMany([entity])
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
insertMany(entities: Iterable<E>) {
|
|
60
|
+
return new Items<E, I>([
|
|
61
|
+
...this,
|
|
62
|
+
...Array.from(entities).filter(entity => !this.has(this.selectId(entity)))
|
|
63
|
+
], this.options)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
upsert(entity: Partial<E>) {
|
|
67
|
+
return this.upsertMany([entity])
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
upsertMany(entities: Iterable<Partial<E>>) {
|
|
71
|
+
const clone = this.getEntities()
|
|
72
|
+
Array.from(entities).forEach(entity => {
|
|
73
|
+
const id = this.selectId(entity as E)
|
|
74
|
+
const existing = clone.get(id)
|
|
75
|
+
if (existing) {
|
|
76
|
+
clone.set(id, { ...existing, ...entity } as E)
|
|
77
|
+
} else {
|
|
78
|
+
clone.set(id, entity as E)
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
return new Items<E, I>(clone.values(), this.options)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
set(entity: E) {
|
|
85
|
+
return this.setMany([entity])
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
setMany(entities: Iterable<E>) {
|
|
89
|
+
return new Items<E, I>([
|
|
90
|
+
...this,
|
|
91
|
+
...entities
|
|
92
|
+
], this.options)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
every(check: SelectorFn<E>) {
|
|
96
|
+
return this.getIds().every(id => check(this.select(id)!))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
some(check: SelectorFn<E>) {
|
|
100
|
+
return this.getIds().some(id => check(this.select(id)!))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
has(id: I) {
|
|
104
|
+
return this.state.ids.includes(id)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
hasMany(select: Selector<E, I>) {
|
|
108
|
+
let failToFind = false
|
|
109
|
+
let result = false
|
|
110
|
+
selector(this, select, (entity) => {
|
|
111
|
+
if (entity) {
|
|
112
|
+
result = true
|
|
113
|
+
} else {
|
|
114
|
+
failToFind = true
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
return !failToFind && result
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
update(id: I, updater: Updater<E>) {
|
|
121
|
+
const entity = this.select(id)
|
|
122
|
+
if (!entity) {
|
|
123
|
+
return this
|
|
124
|
+
}
|
|
125
|
+
const clone = this.getEntities()
|
|
126
|
+
clone.set(id, update(entity, updater))
|
|
127
|
+
return new Items<E, I>(clone.values(), this.options)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
updateMany(select: Selector<E, I>, updater: Updater<E>) {
|
|
131
|
+
const clone = this.getEntities()
|
|
132
|
+
selector(this, select, (entity, id) => {
|
|
133
|
+
if (entity) {
|
|
134
|
+
clone.set(id, update(entity, updater))
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
return new Items<E, I>(clone.values(), this.options)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
remove(id: I) {
|
|
141
|
+
const clone = this.getEntities()
|
|
142
|
+
clone.delete(id)
|
|
143
|
+
return new Items<E, I>(clone.values(), this.options)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
removeMany(select: Selector<E, I>) {
|
|
147
|
+
const clone = this.getEntities()
|
|
148
|
+
selector(this, select, (_, id) => {
|
|
149
|
+
clone.delete(id)
|
|
150
|
+
})
|
|
151
|
+
return new Items<E, I>(clone.values(), this.options)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
clear() {
|
|
155
|
+
return new Items<E, I>([], this.options)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
filter(select: Selector<E, I>) {
|
|
159
|
+
const clone = new Map<I, E>()
|
|
160
|
+
|
|
161
|
+
selector(this, select, (entity, id) => {
|
|
162
|
+
if (entity) {
|
|
163
|
+
clone.set(id, entity)
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
return new Items<E, I>(clone.values(), this.options)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
page(page: number, pageSize: number) {
|
|
171
|
+
const totalPages = Math.ceil(this.length / pageSize)
|
|
172
|
+
return {
|
|
173
|
+
items: this.getIds()
|
|
174
|
+
.slice(page * pageSize, (page + 1) * pageSize)
|
|
175
|
+
.map(id => this.select(id)!),
|
|
176
|
+
page,
|
|
177
|
+
pageSize,
|
|
178
|
+
hasNext: page < totalPages - 1,
|
|
179
|
+
hasPrevious: page > 0,
|
|
180
|
+
total: this.length,
|
|
181
|
+
totalPages
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
diff(base: Items<E, I>) {
|
|
186
|
+
return itemsDiff(base, this)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private sortIds(
|
|
190
|
+
ids: I[],
|
|
191
|
+
entities: Map<I, E>
|
|
192
|
+
): Array<I> {
|
|
193
|
+
if (this.sortComparer === false) {
|
|
194
|
+
return ids
|
|
195
|
+
}
|
|
196
|
+
const sorter = this.sortComparer
|
|
197
|
+
return [...ids].sort((aId, bId) => {
|
|
198
|
+
const a = entities.get(aId)!
|
|
199
|
+
const b = entities.get(bId)!
|
|
200
|
+
return sorter(a, b)
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private selectId(entity: E): I {
|
|
205
|
+
return this.options?.selectId?.(entity) as undefined || defaultSelectId(entity as E & { id: I })
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private get sortComparer() {
|
|
209
|
+
return this.options.sortComparer || false
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
[Symbol.iterator](): Iterator<E> {
|
|
213
|
+
return this.state.entities.values()
|
|
214
|
+
}
|
|
215
|
+
}
|
package/src/diff.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { diff } from 'ohash/utils'
|
|
2
|
+
import { Items } from './Items'
|
|
3
|
+
import { StrOrNum } from './selectId'
|
|
4
|
+
|
|
5
|
+
export interface ItemDiff<I> {
|
|
6
|
+
id: I
|
|
7
|
+
changes: ReturnType<typeof diff>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ItemsDiff<I> {
|
|
11
|
+
added: I[]
|
|
12
|
+
removed: I[]
|
|
13
|
+
updated: ItemDiff<I>[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function itemsDiff<E, I extends StrOrNum>(fromItems: Items<E, I>, toItems: Items<E, I>): ItemsDiff<I> {
|
|
17
|
+
const ids = new Set(toItems.getIds())
|
|
18
|
+
const baseIds = new Set(fromItems.getIds())
|
|
19
|
+
|
|
20
|
+
const added: I[] = []
|
|
21
|
+
const updated: ItemDiff<I>[] = []
|
|
22
|
+
|
|
23
|
+
ids.forEach(id => {
|
|
24
|
+
if (!baseIds.has(id)) {
|
|
25
|
+
added.push(id)
|
|
26
|
+
} else {
|
|
27
|
+
const changes = diff(fromItems.select(id), toItems.select(id))
|
|
28
|
+
|
|
29
|
+
if (changes.length > 0) {
|
|
30
|
+
updated.push({ id, changes })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
baseIds.delete(id)
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const removed = [...baseIds]
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
added,
|
|
41
|
+
removed,
|
|
42
|
+
updated
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { Items } from './Items'
|
|
2
|
+
export type { ItemsOptions, ItemsState } from './Items'
|
|
3
|
+
export type { SelectId, StrOrNum } from './selectId'
|
|
4
|
+
export type { Selector, SelectorFn, Operation } from './selector'
|
|
5
|
+
export type { Updater, UpdateFn } from './updater'
|
|
6
|
+
export type { ItemDiff, ItemsDiff } from './diff'
|
package/src/selectId.ts
ADDED
package/src/selector.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Items } from './Items'
|
|
2
|
+
import { StrOrNum } from './selectId'
|
|
3
|
+
|
|
4
|
+
export type SelectorFn<E> = (entity: E) => boolean
|
|
5
|
+
export type Selector<E, I> = Iterable<I> | SelectorFn<E>
|
|
6
|
+
export type Operation<E, I> = (entity: E | undefined, id: I) => void
|
|
7
|
+
|
|
8
|
+
export function selector<E, I extends StrOrNum>(items: Items<E, I>, selector: Selector<E, I>, operation: Operation<E, I>) {
|
|
9
|
+
if (typeof selector === 'function') {
|
|
10
|
+
items.getEntities().forEach((entity, id) => {
|
|
11
|
+
if ((selector as SelectorFn<E>)(entity)) {
|
|
12
|
+
operation(entity, id)
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
} else {
|
|
16
|
+
Array
|
|
17
|
+
.from(selector as Iterable<I>)
|
|
18
|
+
.forEach(id => operation(items.select(id), id))
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/updater.ts
ADDED