@wszerad/items 0.0.6 → 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.
@@ -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
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="Ask2AgentMigrationStateService">
4
+ <option name="migrationStatus" value="COMPLETED" />
5
+ </component>
6
+ </project>
File without changes
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/items.iml" filepath="$PROJECT_DIR$/.idea/items.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
package/.idea/vcs.xml ADDED
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="" vcs="Git" />
5
+ </component>
6
+ </project>
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/README.md CHANGED
@@ -4,7 +4,7 @@ Lightweight, immutable collection manager inspired by NgRx Entity Adapter.
4
4
 
5
5
  ## Features
6
6
 
7
- - ✅ Immutable operations (insert/insertMany, upsert/upsertMany, set/setMany, update, remove, filter, etc.)
7
+ - ✅ Immutable operations (insert/insertMany, upsert/upsertMany, set/setMany, update, remove/removeMany, filter, etc.)
8
8
  - ✅ Single entity and batch operations
9
9
  - ✅ Custom ID selection (`selectId`)
10
10
  - ✅ Optional sorting (`sortComparer`)
@@ -19,7 +19,7 @@ Lightweight, immutable collection manager inspired by NgRx Entity Adapter.
19
19
  ## Installation
20
20
 
21
21
  ```bash
22
- npm install items
22
+ npm install @wszerad/items
23
23
  ```
24
24
 
25
25
  ## Quick Start
@@ -722,40 +722,45 @@ users = users.setMany([
722
722
  ### Checking Entity Existence
723
723
 
724
724
  ```typescript
725
- import { Items } from 'items'
725
+ interface Product {
726
+ sku: string
727
+ name: string
728
+ price: number
729
+ }
726
730
 
727
- const users = new Items<number, User>([
728
- { id: 1, name: 'Alice', age: 25 },
729
- { id: 2, name: 'Bob', age: 30 },
730
- { id: 3, name: 'Charlie', age: 35 }
731
+ const products = new Items<string, Product>([], {
732
+ selectId: (product) => product.sku
733
+ })
734
+
735
+ const updated = products.insert([
736
+ { sku: 'ABC-123', name: 'Widget', price: 19.99 }
731
737
  ])
732
738
 
733
- // Check single entity exists
734
- if (users.has(1)) {
735
- console.log('User 1 exists')
736
- }
739
+ console.log(updated.select('ABC-123'))
740
+ ```
737
741
 
738
- if (!users.has(99)) {
739
- console.log('User 99 does not exist')
740
- }
742
+ ### With Sorting
741
743
 
742
- // Check multiple entities exist (ALL must exist)
743
- if (users.hasMany([1, 2])) {
744
- console.log('Users 1 and 2 both exist')
745
- }
744
+ ```typescript
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
+ )
746
753
 
747
- if (!users.hasMany([1, 99])) {
748
- console.log('Not all users exist')
749
- }
754
+ console.log(items.getIds()) // [1, 2, 3] - sorted by name
755
+ ```
750
756
 
751
- // Check if any entity matches condition
752
- if (users.hasMany(user => user.age >= 30)) {
753
- console.log('At least one user is 30 or older')
754
- }
757
+ ---
758
+
759
+ ## Development
755
760
 
756
- // Use with conditionals
757
- const userExists = users.has(1)
758
- const allExist = users.hasMany([1, 2, 3])
759
- const hasAdults = users.hasMany(user => user.age >= 18)
761
+ ### Scripts
760
762
 
761
- // Combine with ot
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 (
package/package.json CHANGED
@@ -1,16 +1,14 @@
1
1
  {
2
2
  "name": "@wszerad/items",
3
- "version": "0.0.6",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "exports": {
6
- ".": "./dist/index.js",
6
+ ".": "./dist/index.mjs",
7
7
  "./package.json": "./package.json"
8
8
  },
9
- "main": "./dist/index.js",
10
- "types": "./dist/index.d.ts",
11
- "files": [
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'
@@ -0,0 +1,7 @@
1
+ export type StrOrNum = string | number
2
+
3
+ export type SelectId<E> = (entity: E) => StrOrNum
4
+
5
+ export function defaultSelectId<E extends { id: I }, I = E['id']>(entity: E) {
6
+ return entity.id
7
+ }
@@ -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
@@ -0,0 +1,6 @@
1
+ export type UpdateFn<E> = (entity: E) => E
2
+ export type Updater<E> = UpdateFn<E> | Partial<E>
3
+
4
+ export function update<E>(entity: E, updater: Updater<E>) {
5
+ return { ...entity, ...(typeof updater === 'function' ? updater(entity) : updater) }
6
+ }