capitalsix-data-table 0.1.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 +253 -0
- package/package.json +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# DataTable
|
|
2
|
+
|
|
3
|
+
A lightweight, framework-agnostic in-memory data table with fully typed CRUD operations. Use it anywhere you need to manage a collection of objects: plain TypeScript, Node.js, React state handlers, Svelte stores, etc.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Typed rows** — generic over any object type `T`
|
|
8
|
+
- **Insert** with optional duplicate-key guard
|
|
9
|
+
- **Update** by predicate — via mutation function or replacement object
|
|
10
|
+
- **Upsert** — insert new rows or update existing ones in a single call
|
|
11
|
+
- **Delete** by predicate, returns deleted rows
|
|
12
|
+
- **Sort** in-place with a custom comparator
|
|
13
|
+
- **Query** all rows or filter with a predicate
|
|
14
|
+
- **Bulk replace** via `setRows` for full re-initialisation
|
|
15
|
+
- Zero runtime dependencies
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install capitalsix-data-table
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { DataTable } from 'capitalsix-data-table';
|
|
27
|
+
|
|
28
|
+
type User = { id: number; name: string; age: number };
|
|
29
|
+
|
|
30
|
+
const table = new DataTable<User>([
|
|
31
|
+
{ id: 1, name: 'Alice', age: 30 },
|
|
32
|
+
{ id: 2, name: 'Bob', age: 25 },
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const users = table.getRows();
|
|
36
|
+
// [{ id: 1, name: 'Alice', age: 30 }, { id: 2, name: 'Bob', age: 25 }]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## API
|
|
40
|
+
|
|
41
|
+
### `constructor(initialRows?: T[])`
|
|
42
|
+
|
|
43
|
+
Creates a new table, optionally pre-populated with rows.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
const empty = new DataTable<User>();
|
|
47
|
+
const seeded = new DataTable<User>([{ id: 1, name: 'Alice', age: 30 }]);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
### `getRows(predicate?: (row: T) => boolean): T[]`
|
|
53
|
+
|
|
54
|
+
Returns all rows, or only the rows matching the predicate.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
const all = table.getRows();
|
|
58
|
+
const young = table.getRows(u => u.age < 30);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
### `setRows(rows: T[]): void`
|
|
64
|
+
|
|
65
|
+
Replaces **all** current rows with the provided array. Useful for bulk re-initialisation (e.g. after loading fresh data from an API).
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
table.setRows([
|
|
69
|
+
{ id: 10, name: 'Carol', age: 28 },
|
|
70
|
+
{ id: 11, name: 'Dave', age: 35 },
|
|
71
|
+
]);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
### `insertRows(rows: T[], primaryKey?: keyof T): void`
|
|
77
|
+
|
|
78
|
+
Appends rows to the table. When `primaryKey` is provided, the method throws if any incoming key already exists, preventing accidental duplicates.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// Insert without key guard
|
|
82
|
+
table.insertRows([{ id: 3, name: 'Eve', age: 22 }]);
|
|
83
|
+
|
|
84
|
+
// Insert with duplicate-key guard
|
|
85
|
+
table.insertRows([{ id: 4, name: 'Frank', age: 40 }], 'id');
|
|
86
|
+
|
|
87
|
+
// Throws: "Duplicate primary key value found in existing rows for key: id"
|
|
88
|
+
table.insertRows([{ id: 1, name: 'Alice again', age: 99 }], 'id');
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
### `updateRows(predicate, update, primaryKey?): T[]`
|
|
94
|
+
|
|
95
|
+
Updates all rows matching `predicate` and returns them (already reflecting the changes).
|
|
96
|
+
|
|
97
|
+
**Mutation-function style** — receives each matched row and mutates it directly:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
table.updateRows(
|
|
101
|
+
u => u.id === 1,
|
|
102
|
+
u => { u.age += 1; },
|
|
103
|
+
);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Replacement-object style** — merges the object onto each matched row while preserving the primary key:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
table.updateRows(
|
|
110
|
+
u => u.id === 2,
|
|
111
|
+
{ id: 0, name: 'Bob Updated', age: 26 },
|
|
112
|
+
'id', // primaryKey is required; its value is preserved from the original row
|
|
113
|
+
);
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
### `upsertRows(rows: T[], primaryKey: keyof T): DataTableUpsertResult<T>`
|
|
119
|
+
|
|
120
|
+
Inserts rows whose key does not yet exist; updates rows whose key already exists. Returns an object `{ inserted, updated }` so callers can react to each outcome.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
const result = table.upsertRows(
|
|
124
|
+
[
|
|
125
|
+
{ id: 1, name: 'Alice', age: 31 }, // key exists -> updated
|
|
126
|
+
{ id: 5, name: 'Grace', age: 27 }, // new key -> inserted
|
|
127
|
+
],
|
|
128
|
+
'id',
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
console.log(result.updated.length); // 1
|
|
132
|
+
console.log(result.inserted.length); // 1
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
### `deleteRows(predicate: (row: T) => boolean): T[]`
|
|
138
|
+
|
|
139
|
+
Removes all rows that match the predicate and returns them.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
const removed = table.deleteRows(u => u.age < 25);
|
|
143
|
+
console.log(removed); // rows that were deleted
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
### `sortRows(compareFn: (a: T, b: T) => number): void`
|
|
149
|
+
|
|
150
|
+
Sorts rows in place using the provided comparator (same contract as `Array.prototype.sort`).
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
// Sort by age ascending
|
|
154
|
+
table.sortRows((a, b) => a.age - b.age);
|
|
155
|
+
|
|
156
|
+
// Sort by name alphabetically
|
|
157
|
+
table.sortRows((a, b) => a.name.localeCompare(b.name));
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Examples
|
|
163
|
+
|
|
164
|
+
### Build a simple in-memory cache
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { DataTable } from 'capitalsix-data-table';
|
|
168
|
+
|
|
169
|
+
type Product = { sku: string; name: string; stock: number };
|
|
170
|
+
|
|
171
|
+
const cache = new DataTable<Product>();
|
|
172
|
+
|
|
173
|
+
// Load initial catalogue
|
|
174
|
+
cache.setRows(await fetchProducts());
|
|
175
|
+
|
|
176
|
+
// Reduce stock for a purchased item
|
|
177
|
+
cache.updateRows(
|
|
178
|
+
p => p.sku === 'ABC-123',
|
|
179
|
+
p => { p.stock -= 1; },
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// Sync with latest server data, inserting new products and updating changed ones
|
|
183
|
+
const { inserted, updated } = cache.upsertRows(await fetchProducts(), 'sku');
|
|
184
|
+
console.log(`Synced: ${inserted.length} new, ${updated.length} updated`);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
### Filter and sort for display
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
const inStock = table.getRows(p => p.stock > 0);
|
|
193
|
+
|
|
194
|
+
// Clone before sorting if you do not want to mutate the table order.
|
|
195
|
+
// Use sortRows() when you *do* want the table itself sorted.
|
|
196
|
+
const sorted = [...inStock].sort((a, b) => a.name.localeCompare(b.name));
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
### Use inside a React reducer
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import { DataTable } from 'capitalsix-data-table';
|
|
205
|
+
|
|
206
|
+
type State = { table: DataTable<User> };
|
|
207
|
+
|
|
208
|
+
function reducer(state: State, action: Action): State {
|
|
209
|
+
switch (action.type) {
|
|
210
|
+
case 'UPSERT_USERS':
|
|
211
|
+
state.table.upsertRows(action.payload, 'id');
|
|
212
|
+
return { ...state }; // shallow clone to trigger re-render
|
|
213
|
+
|
|
214
|
+
case 'DELETE_USER':
|
|
215
|
+
state.table.deleteRows(u => u.id === action.id);
|
|
216
|
+
return { ...state };
|
|
217
|
+
|
|
218
|
+
default:
|
|
219
|
+
return state;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
### Typed interfaces for dependency injection
|
|
227
|
+
|
|
228
|
+
`DataTable` implements two interfaces you can use for narrowing in function signatures:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
import type { IDataTable, IDataTableInitial } from 'capitalsix-data-table';
|
|
232
|
+
|
|
233
|
+
// Read/write CRUD — no bulk replace
|
|
234
|
+
function processUsers(table: IDataTable<User>) {
|
|
235
|
+
table.insertRows([{ id: 99, name: 'Test', age: 0 }], 'id');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Bulk replace only — used during initialisation
|
|
239
|
+
function loadUsers(table: IDataTableInitial<User>, users: User[]) {
|
|
240
|
+
table.setRows(users);
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Development
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
npm install
|
|
250
|
+
npm test
|
|
251
|
+
npm run typecheck
|
|
252
|
+
npm run build
|
|
253
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "capitalsix-data-table",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A lightweight, framework-agnostic in-memory data table with typed CRUD operations: insert, update, upsert, delete, sort, and query rows.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.cjs",
|
|
8
|
+
"module": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js",
|
|
18
|
+
"require": "./dist/index.cjs"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"author": {
|
|
22
|
+
"name": "Mike Wijnen",
|
|
23
|
+
"email": "mike@capitalsix.nl",
|
|
24
|
+
"url": "https://capitalsix.nl"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"url": "https://github.com/capital-six/data-table"
|
|
28
|
+
},
|
|
29
|
+
"ignore": [
|
|
30
|
+
"src/",
|
|
31
|
+
"node_modules/",
|
|
32
|
+
"PUBLISH.md"
|
|
33
|
+
],
|
|
34
|
+
"sideEffects": false,
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:watch": "vitest",
|
|
39
|
+
"typecheck": "tsc --noEmit",
|
|
40
|
+
"prepack": "npmignore --auto"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"react": "^18.3.1",
|
|
44
|
+
"react-dom": "^18.3.1"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@testing-library/jest-dom": "^6.4.8",
|
|
48
|
+
"@testing-library/react": "^16.0.1",
|
|
49
|
+
"@types/node": "^22.10.2",
|
|
50
|
+
"@types/react": "^18.3.1",
|
|
51
|
+
"@types/react-dom": "^18.3.1",
|
|
52
|
+
"@vitejs/plugin-react": "^5.0.0",
|
|
53
|
+
"jsdom": "^25.0.1",
|
|
54
|
+
"tsup": "^8.3.5",
|
|
55
|
+
"typescript": "^5.7.3",
|
|
56
|
+
"vite": "^6.0.5",
|
|
57
|
+
"vitest": "^2.1.8"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"npmignore": "^0.3.5"
|
|
61
|
+
}
|
|
62
|
+
}
|