@jsonkit/db 1.0.1 → 3.0.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/README.md +247 -1
- package/dist/cjs/index.cjs +219 -84
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +218 -85
- package/dist/esm/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/common/index.d.ts +2 -0
- package/dist/types/common/multiEntryDb.d.ts +19 -0
- package/dist/types/{types.d.ts → common/types.d.ts} +9 -0
- package/dist/types/{files.d.ts → file/files.d.ts} +2 -2
- package/dist/types/file/multiEntryFileDb.d.ts +20 -0
- package/dist/types/{singleEntryFileDb.d.ts → file/singleEntryFileDb.d.ts} +3 -3
- package/dist/types/index.d.ts +5 -3
- package/dist/types/memory/__tests__/multiEntryMemDb.test.d.ts +1 -0
- package/dist/types/memory/__tests__/singleEntryMemDb.test.d.ts +1 -0
- package/dist/types/memory/multiEntryMemDb.d.ts +11 -0
- package/dist/types/memory/singleEntryMemDb.d.ts +8 -0
- package/package.json +5 -2
- package/dist/types/multiEntryFileDb.d.ts +0 -26
- /package/dist/types/{__tests__ → file/__tests__}/multiEntryFileDb.test.d.ts +0 -0
- /package/dist/types/{__tests__ → file/__tests__}/singleEntryFileDb.test.d.ts +0 -0
package/README.md
CHANGED
|
@@ -1,3 +1,249 @@
|
|
|
1
1
|
# @jsonkit/db
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@jsonkit/db` is a lightweight, zero-dependency database abstraction for rapid prototyping. It provides simple **file-based** and **in-memory** databases with consistent APIs, suitable for small applications, tooling, tests, and early-stage prototypes where setting up a full database would be unnecessary overhead.
|
|
4
|
+
|
|
5
|
+
The package exposes two core concepts:
|
|
6
|
+
|
|
7
|
+
* **Single-entry databases** – manage exactly one JSON-serializable object.
|
|
8
|
+
* **Multi-entry databases** – manage collections of identifiable records keyed by an `id`.
|
|
9
|
+
|
|
10
|
+
Both concepts are available in **file-backed** and **in-memory** variants.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @jsonkit/db
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Multi-entry Databases
|
|
23
|
+
|
|
24
|
+
Multi-entry databases manage collections of entries keyed by `id`.
|
|
25
|
+
|
|
26
|
+
### Common API (`MultiEntryDb<T>`)
|
|
27
|
+
|
|
28
|
+
All multi-entry implementations expose the same methods:
|
|
29
|
+
|
|
30
|
+
* `create(entry)`
|
|
31
|
+
* `getById(id)`
|
|
32
|
+
* `getByIdOrThrow(id)`
|
|
33
|
+
* `getWhere(predicate, pagination?)`
|
|
34
|
+
* `getAll(ids?)`
|
|
35
|
+
* `getAllIds()`
|
|
36
|
+
* `update(id, updater)`
|
|
37
|
+
* `deleteById(id)`
|
|
38
|
+
* `deleteByIds(ids)`
|
|
39
|
+
* `deleteWhere(predicate)`
|
|
40
|
+
* `exists(id)`
|
|
41
|
+
* `countAll()`
|
|
42
|
+
* `countWhere(predicate)`
|
|
43
|
+
* `destroy()`
|
|
44
|
+
|
|
45
|
+
Updates are **partial merges**, and changing an entry’s `id` during an update is supported.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
### `MultiEntryFileDb<T extends Identifiable>`
|
|
50
|
+
|
|
51
|
+
A file-backed database where **each entry is stored as its own JSON file**.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { MultiEntryFileDb } from '@jsonkit/db'
|
|
55
|
+
|
|
56
|
+
const db = new MultiEntryFileDb<User>('./data/users')
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
#### Behavior
|
|
60
|
+
|
|
61
|
+
* Each entry is stored as `<id>.json` in the provided directory.
|
|
62
|
+
* The directory is created implicitly as files are written.
|
|
63
|
+
* IDs are validated to prevent path traversal by default.
|
|
64
|
+
|
|
65
|
+
#### Constructor
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
new MultiEntryFileDb<T>(dirpath, options?)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Options**
|
|
72
|
+
|
|
73
|
+
| Option | Description | Default |
|
|
74
|
+
| --------------- | -------------------------------------- | ------- |
|
|
75
|
+
| `noPathlikeIds` | Reject IDs containing `/` or `\` | `true` |
|
|
76
|
+
| `parser` | Custom JSON parser (`{ parse(text) }`) | `JSON` |
|
|
77
|
+
|
|
78
|
+
#### Notes
|
|
79
|
+
|
|
80
|
+
* Failed reads (missing file or invalid JSON) return `null`.
|
|
81
|
+
* `destroy()` deletes the entire directory.
|
|
82
|
+
* Intended for development, prototyping, and small datasets.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
### `MultiEntryMemDb<T extends Identifiable>`
|
|
87
|
+
|
|
88
|
+
An in-memory implementation backed by a `Map`.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { MultiEntryMemDb } from '@jsonkit/db'
|
|
92
|
+
|
|
93
|
+
const db = new MultiEntryMemDb<User>()
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### Behavior
|
|
97
|
+
|
|
98
|
+
* Fast, ephemeral storage.
|
|
99
|
+
* Ideal for tests and short-lived processes.
|
|
100
|
+
* `destroy()` clears all entries.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Single-entry Databases
|
|
105
|
+
|
|
106
|
+
Single-entry databases manage **exactly one value**, often used for configuration or application state.
|
|
107
|
+
|
|
108
|
+
### Common API (`SingleEntryDb<T>`)
|
|
109
|
+
|
|
110
|
+
* `isInited()`
|
|
111
|
+
* `read()`
|
|
112
|
+
* `write(entry | updater)`
|
|
113
|
+
* `delete()`
|
|
114
|
+
|
|
115
|
+
`write` supports either replacing the entry or partially updating it via an updater function.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
### `SingleEntryFileDb<T>`
|
|
120
|
+
|
|
121
|
+
Stores a single JSON object in a file.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { SingleEntryFileDb } from '@jsonkit/db'
|
|
125
|
+
|
|
126
|
+
const db = new SingleEntryFileDb<AppConfig>('./config.json')
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### Behavior
|
|
130
|
+
|
|
131
|
+
* Reads and writes a single JSON file.
|
|
132
|
+
* `isInited()` checks file existence.
|
|
133
|
+
* `read()` throws if the file does not exist.
|
|
134
|
+
|
|
135
|
+
#### Constructor
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
new SingleEntryFileDb<T>(filepath, parser?)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
* `parser` defaults to `JSON`.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
### `SingleEntryMemDb<T>`
|
|
146
|
+
|
|
147
|
+
An in-memory single-value database.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import { SingleEntryMemDb } from '@jsonkit/db'
|
|
151
|
+
|
|
152
|
+
const db = new SingleEntryMemDb<AppConfig>()
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
#### Behavior
|
|
156
|
+
|
|
157
|
+
* Optional initial value.
|
|
158
|
+
* `read()` throws if uninitialized.
|
|
159
|
+
* `delete()` resets the entry to `null`.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Example
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
type User = { id: string; name: string }
|
|
167
|
+
|
|
168
|
+
const users = new MultiEntryFileDb<User>('./users')
|
|
169
|
+
|
|
170
|
+
await users.create({ id: 'u1', name: 'Alice' })
|
|
171
|
+
|
|
172
|
+
await users.update('u1', (u) => ({ name: 'Alice Smith' }))
|
|
173
|
+
|
|
174
|
+
const allUsers = await users.getAll()
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Use Cases
|
|
180
|
+
|
|
181
|
+
* Rapid application prototyping
|
|
182
|
+
* CLI tools
|
|
183
|
+
* Small internal services
|
|
184
|
+
* Tests and mocks
|
|
185
|
+
* Configuration and state persistence
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Core Types
|
|
190
|
+
|
|
191
|
+
### `Identifiable`
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
type Identifiable = { id: string }
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
All multi-entry databases require entries to have a string `id`.
|
|
198
|
+
|
|
199
|
+
### `Promisable<T>`
|
|
200
|
+
|
|
201
|
+
A value or a promise of a value.
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
type Promisable<T> = T | Promise<T>
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### `PredicateFn<T>`
|
|
208
|
+
|
|
209
|
+
Used for filtering entries.
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
type PredicateFn<T extends Identifiable> = (entry: T) => boolean
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### `PaginationInput`
|
|
216
|
+
|
|
217
|
+
Used for filtering entries.
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
type PredicateFn<T extends Identifiable> = (entry: T) => boolean
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### `DeleteManyOutput`
|
|
224
|
+
|
|
225
|
+
Returned by bulk delete operations.
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
type DeleteManyOutput = {
|
|
229
|
+
deletedIds: string[]
|
|
230
|
+
ignoredIds: string[]
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Non-goals
|
|
237
|
+
|
|
238
|
+
* Concurrency control
|
|
239
|
+
* High-performance querying
|
|
240
|
+
* Large datasets
|
|
241
|
+
* ACID guarantees
|
|
242
|
+
|
|
243
|
+
For these, a dedicated database is recommended.
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## License
|
|
248
|
+
|
|
249
|
+
MIT
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -27,6 +27,106 @@ var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
|
|
|
27
27
|
var fsSync__namespace = /*#__PURE__*/_interopNamespaceDefault(fsSync);
|
|
28
28
|
var readline__namespace = /*#__PURE__*/_interopNamespaceDefault(readline);
|
|
29
29
|
|
|
30
|
+
class MultiEntryDb {
|
|
31
|
+
async getByIdOrThrow(id) {
|
|
32
|
+
const entry = await this.getById(id);
|
|
33
|
+
if (!entry)
|
|
34
|
+
throw new Error(`Entry with id '${id}' does not exist`);
|
|
35
|
+
return entry;
|
|
36
|
+
}
|
|
37
|
+
async getWhere(predicate, pagination) {
|
|
38
|
+
let totalMatched = 0;
|
|
39
|
+
const entries = [];
|
|
40
|
+
if (!pagination) {
|
|
41
|
+
for await (const entry of this.iterEntries()) {
|
|
42
|
+
const isMatch = predicate(entry);
|
|
43
|
+
if (isMatch)
|
|
44
|
+
entries.push(entry);
|
|
45
|
+
}
|
|
46
|
+
return entries;
|
|
47
|
+
}
|
|
48
|
+
const { take, page } = pagination;
|
|
49
|
+
const skip = pagination.skip ?? 0;
|
|
50
|
+
const startIndex = (page - 1) * take + skip;
|
|
51
|
+
const endIndex = startIndex + take;
|
|
52
|
+
for await (const entry of this.iterEntries()) {
|
|
53
|
+
const isMatch = predicate(entry);
|
|
54
|
+
if (isMatch) {
|
|
55
|
+
if (totalMatched >= startIndex && totalMatched < endIndex) {
|
|
56
|
+
entries.push(entry);
|
|
57
|
+
}
|
|
58
|
+
totalMatched++;
|
|
59
|
+
if (totalMatched >= endIndex)
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return entries;
|
|
64
|
+
}
|
|
65
|
+
getAll() {
|
|
66
|
+
return this.getWhere(() => true);
|
|
67
|
+
}
|
|
68
|
+
async getAllIds() {
|
|
69
|
+
const ids = [];
|
|
70
|
+
for await (const id of this.iterIds()) {
|
|
71
|
+
ids.push(id);
|
|
72
|
+
}
|
|
73
|
+
return ids;
|
|
74
|
+
}
|
|
75
|
+
deleteByIds(ids) {
|
|
76
|
+
return this.deleteWhere((entry) => ids.includes(entry.id));
|
|
77
|
+
}
|
|
78
|
+
async deleteWhere(predicate) {
|
|
79
|
+
const deletedIds = [];
|
|
80
|
+
const ignoredIds = [];
|
|
81
|
+
for await (const entry of this.iterEntries()) {
|
|
82
|
+
if (!predicate(entry))
|
|
83
|
+
continue;
|
|
84
|
+
const didDelete = await this.deleteById(entry.id);
|
|
85
|
+
if (didDelete) {
|
|
86
|
+
deletedIds.push(entry.id);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
ignoredIds.push(entry.id);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { deletedIds, ignoredIds };
|
|
93
|
+
}
|
|
94
|
+
async exists(id) {
|
|
95
|
+
const entry = await this.getById(id);
|
|
96
|
+
return entry !== null;
|
|
97
|
+
}
|
|
98
|
+
countAll() {
|
|
99
|
+
return this.countWhere(() => true);
|
|
100
|
+
}
|
|
101
|
+
async countWhere(predicate, pagination) {
|
|
102
|
+
let totalMatched = 0;
|
|
103
|
+
let count = 0;
|
|
104
|
+
if (!pagination) {
|
|
105
|
+
for await (const entry of this.iterEntries()) {
|
|
106
|
+
const isMatch = predicate(entry);
|
|
107
|
+
if (isMatch)
|
|
108
|
+
count++;
|
|
109
|
+
}
|
|
110
|
+
return count;
|
|
111
|
+
}
|
|
112
|
+
const { take, page } = pagination;
|
|
113
|
+
const skip = pagination.skip ?? 0;
|
|
114
|
+
const startIndex = (page - 1) * take + skip;
|
|
115
|
+
const endIndex = startIndex + take;
|
|
116
|
+
for await (const entry of this.iterEntries()) {
|
|
117
|
+
const isMatch = predicate(entry);
|
|
118
|
+
if (isMatch) {
|
|
119
|
+
if (totalMatched >= startIndex && totalMatched < endIndex)
|
|
120
|
+
count++;
|
|
121
|
+
totalMatched++;
|
|
122
|
+
if (totalMatched >= endIndex)
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return count;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
30
130
|
exports.FileType = void 0;
|
|
31
131
|
(function (FileType) {
|
|
32
132
|
FileType["File"] = "file";
|
|
@@ -35,7 +135,7 @@ exports.FileType = void 0;
|
|
|
35
135
|
})(exports.FileType || (exports.FileType = {}));
|
|
36
136
|
|
|
37
137
|
const DEFUALT_ENCODING = 'utf-8';
|
|
38
|
-
class
|
|
138
|
+
class Files {
|
|
39
139
|
async move(oldPath, newPath) {
|
|
40
140
|
await fs__namespace.rename(oldPath, newPath);
|
|
41
141
|
}
|
|
@@ -243,64 +343,44 @@ class FilesService {
|
|
|
243
343
|
}
|
|
244
344
|
}
|
|
245
345
|
|
|
246
|
-
class MultiEntryFileDb {
|
|
247
|
-
constructor(dirpath,
|
|
346
|
+
class MultiEntryFileDb extends MultiEntryDb {
|
|
347
|
+
constructor(dirpath, options) {
|
|
348
|
+
super();
|
|
248
349
|
this.dirpath = dirpath;
|
|
249
|
-
this.
|
|
250
|
-
this.
|
|
350
|
+
this.files = new Files();
|
|
351
|
+
this.parser = options?.parser ?? JSON;
|
|
352
|
+
this.noPathlikeIds = options?.noPathlikeIds ?? true;
|
|
251
353
|
}
|
|
252
354
|
async create(entry) {
|
|
253
355
|
await this.writeEntry(entry);
|
|
254
356
|
return entry;
|
|
255
357
|
}
|
|
256
358
|
async getById(id) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
async getByIdOrThrow(id) {
|
|
260
|
-
const entry = await this.readEntry(id);
|
|
261
|
-
if (!entry) {
|
|
262
|
-
throw new Error('Entry with id ' + id + ' does not exist');
|
|
263
|
-
}
|
|
264
|
-
return entry;
|
|
265
|
-
}
|
|
266
|
-
async getWhere(predicate, max) {
|
|
267
|
-
const entries = await this.getAll();
|
|
268
|
-
return entries.filter(predicate).slice(0, max);
|
|
269
|
-
}
|
|
270
|
-
async getAll(whereIds) {
|
|
271
|
-
const ids = whereIds === undefined ? await this.getAllIds() : whereIds;
|
|
272
|
-
const entries = [];
|
|
273
|
-
for (const id of ids) {
|
|
274
|
-
const entry = await this.readEntry(id);
|
|
275
|
-
if (entry)
|
|
276
|
-
entries.push(entry);
|
|
277
|
-
}
|
|
278
|
-
return entries;
|
|
279
|
-
}
|
|
280
|
-
async getAllIds() {
|
|
359
|
+
if (!this.isIdValid(id))
|
|
360
|
+
throw new Error(`Invalid id: ${id}`);
|
|
281
361
|
try {
|
|
282
|
-
const
|
|
283
|
-
|
|
362
|
+
const filepath = this.getFilePath(id);
|
|
363
|
+
const text = await this.files.read(filepath);
|
|
364
|
+
const entry = this.parser.parse(text);
|
|
365
|
+
return entry;
|
|
284
366
|
}
|
|
285
|
-
catch {
|
|
286
|
-
|
|
287
|
-
|
|
367
|
+
catch (error) {
|
|
368
|
+
console.error('Failed to read entry', error);
|
|
369
|
+
// File doesn't exist or invalid JSON
|
|
370
|
+
return null;
|
|
288
371
|
}
|
|
289
372
|
}
|
|
290
373
|
async update(id, updater) {
|
|
291
|
-
const entry = await this.
|
|
292
|
-
if (!entry) {
|
|
293
|
-
throw new Error('Entry with id ' + id + ' does not exist');
|
|
294
|
-
}
|
|
374
|
+
const entry = await this.getByIdOrThrow(id);
|
|
295
375
|
const updatedEntryFields = await updater(entry);
|
|
296
376
|
const updatedEntry = { ...entry, ...updatedEntryFields };
|
|
297
377
|
await this.writeEntry(updatedEntry);
|
|
298
378
|
if (updatedEntry.id !== id) {
|
|
299
|
-
await this.
|
|
379
|
+
await this.deleteById(id);
|
|
300
380
|
}
|
|
301
381
|
return updatedEntry;
|
|
302
382
|
}
|
|
303
|
-
async
|
|
383
|
+
async deleteById(id) {
|
|
304
384
|
try {
|
|
305
385
|
const filepath = this.getFilePath(id);
|
|
306
386
|
await this.files.delete(filepath, { force: false });
|
|
@@ -314,71 +394,51 @@ class MultiEntryFileDb {
|
|
|
314
394
|
async deleteByIds(ids) {
|
|
315
395
|
return this.deleteWhere((entry) => ids.includes(entry.id));
|
|
316
396
|
}
|
|
317
|
-
async deleteWhere(predicate) {
|
|
318
|
-
const deletedIds = [];
|
|
319
|
-
const ignoredIds = [];
|
|
320
|
-
for await (const entry of this.iterEntries()) {
|
|
321
|
-
if (!predicate(entry))
|
|
322
|
-
continue;
|
|
323
|
-
const didDelete = await this.delete(entry.id);
|
|
324
|
-
if (didDelete) {
|
|
325
|
-
deletedIds.push(entry.id);
|
|
326
|
-
}
|
|
327
|
-
else {
|
|
328
|
-
ignoredIds.push(entry.id);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
return { deletedIds, ignoredIds };
|
|
332
|
-
}
|
|
333
397
|
async destroy() {
|
|
334
398
|
await this.files.delete(this.dirpath);
|
|
335
399
|
}
|
|
336
|
-
async exists(id) {
|
|
337
|
-
const entry = await this.readEntry(id);
|
|
338
|
-
return entry !== null;
|
|
339
|
-
}
|
|
340
|
-
async countAll() {
|
|
341
|
-
const ids = await this.getAllIds();
|
|
342
|
-
return ids.length;
|
|
343
|
-
}
|
|
344
|
-
async countWhere(predicate) {
|
|
345
|
-
return (await this.getWhere(predicate)).length;
|
|
346
|
-
}
|
|
347
400
|
getFilePath(id) {
|
|
348
401
|
return path__namespace.join(this.dirpath, `${id}.json`);
|
|
349
402
|
}
|
|
350
|
-
async readEntry(id) {
|
|
351
|
-
try {
|
|
352
|
-
const filepath = this.getFilePath(id);
|
|
353
|
-
const text = await this.files.read(filepath);
|
|
354
|
-
const entry = this.parser.parse(text);
|
|
355
|
-
return entry;
|
|
356
|
-
}
|
|
357
|
-
catch (error) {
|
|
358
|
-
console.error('Failed to read entry', error);
|
|
359
|
-
// File doesn't exist or invalid JSON
|
|
360
|
-
return null;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
403
|
async writeEntry(entry) {
|
|
404
|
+
if (!this.isIdValid(entry.id))
|
|
405
|
+
throw new Error(`Invalid id: ${entry.id}`);
|
|
364
406
|
const filepath = this.getFilePath(entry.id);
|
|
365
407
|
await this.files.write(filepath, JSON.stringify(entry, null, 2));
|
|
366
408
|
}
|
|
409
|
+
isIdValid(id) {
|
|
410
|
+
if (typeof id !== 'string')
|
|
411
|
+
return false;
|
|
412
|
+
if (!this.noPathlikeIds)
|
|
413
|
+
return true;
|
|
414
|
+
if (id.includes('/') || id.includes('\\'))
|
|
415
|
+
return false;
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
367
418
|
async *iterEntries() {
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
const entry = await this.readEntry(id);
|
|
419
|
+
for await (const id of this.iterIds()) {
|
|
420
|
+
const entry = await this.getById(id);
|
|
371
421
|
if (entry)
|
|
372
422
|
yield entry;
|
|
373
423
|
}
|
|
374
424
|
}
|
|
425
|
+
async *iterIds() {
|
|
426
|
+
const filenames = await this.files.list(this.dirpath);
|
|
427
|
+
for (const filename of filenames) {
|
|
428
|
+
if (!filename.endsWith('.json'))
|
|
429
|
+
continue;
|
|
430
|
+
const id = filename.replace(/\.json$/, '');
|
|
431
|
+
if (this.isIdValid(id))
|
|
432
|
+
yield id;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
375
435
|
}
|
|
376
436
|
|
|
377
437
|
class SingleEntryFileDb {
|
|
378
438
|
constructor(filepath, parser = JSON) {
|
|
379
439
|
this.filepath = filepath;
|
|
380
440
|
this.parser = parser;
|
|
381
|
-
this.files = new
|
|
441
|
+
this.files = new Files();
|
|
382
442
|
}
|
|
383
443
|
path() {
|
|
384
444
|
return this.filepath;
|
|
@@ -411,6 +471,81 @@ class SingleEntryFileDb {
|
|
|
411
471
|
}
|
|
412
472
|
}
|
|
413
473
|
|
|
474
|
+
class MultiEntryMemDb extends MultiEntryDb {
|
|
475
|
+
constructor() {
|
|
476
|
+
super(...arguments);
|
|
477
|
+
this.entries = new Map();
|
|
478
|
+
}
|
|
479
|
+
async create(entry) {
|
|
480
|
+
this.entries.set(entry.id, entry);
|
|
481
|
+
return entry;
|
|
482
|
+
}
|
|
483
|
+
async getById(id) {
|
|
484
|
+
return this.entries.get(id) ?? null;
|
|
485
|
+
}
|
|
486
|
+
async update(id, updater) {
|
|
487
|
+
const entry = await this.getByIdOrThrow(id);
|
|
488
|
+
const updatedEntryFields = await updater(entry);
|
|
489
|
+
const updatedEntry = { ...entry, ...updatedEntryFields };
|
|
490
|
+
this.entries.set(updatedEntry.id, updatedEntry);
|
|
491
|
+
if (updatedEntry.id !== id)
|
|
492
|
+
this.entries.delete(id);
|
|
493
|
+
return updatedEntry;
|
|
494
|
+
}
|
|
495
|
+
async deleteById(id) {
|
|
496
|
+
return this.entries.delete(id);
|
|
497
|
+
}
|
|
498
|
+
async destroy() {
|
|
499
|
+
this.entries.clear();
|
|
500
|
+
}
|
|
501
|
+
async *iterEntries() {
|
|
502
|
+
for (const entry of this.entries.values()) {
|
|
503
|
+
yield entry;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
async *iterIds() {
|
|
507
|
+
for (const id of this.entries.keys()) {
|
|
508
|
+
yield id;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
class SingleEntryMemDb {
|
|
514
|
+
constructor(initialEntry = null) {
|
|
515
|
+
this.entry = null;
|
|
516
|
+
this.entry = initialEntry;
|
|
517
|
+
}
|
|
518
|
+
isInited() {
|
|
519
|
+
return this.entry !== null;
|
|
520
|
+
}
|
|
521
|
+
read() {
|
|
522
|
+
if (this.entry === null)
|
|
523
|
+
throw new Error('Entry not initialized');
|
|
524
|
+
return this.entry;
|
|
525
|
+
}
|
|
526
|
+
write(updaterOrEntry) {
|
|
527
|
+
let entry;
|
|
528
|
+
if (typeof updaterOrEntry === 'function') {
|
|
529
|
+
const updater = updaterOrEntry;
|
|
530
|
+
if (this.entry === null) {
|
|
531
|
+
throw new Error('Cannot update uninitialized entry. Use write(entry) to initialize first.');
|
|
532
|
+
}
|
|
533
|
+
const updatedFields = updater(this.entry);
|
|
534
|
+
entry = { ...this.entry, ...updatedFields };
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
entry = updaterOrEntry;
|
|
538
|
+
}
|
|
539
|
+
this.entry = entry;
|
|
540
|
+
return entry;
|
|
541
|
+
}
|
|
542
|
+
delete() {
|
|
543
|
+
this.entry = null;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
414
547
|
exports.MultiEntryFileDb = MultiEntryFileDb;
|
|
548
|
+
exports.MultiEntryMemDb = MultiEntryMemDb;
|
|
415
549
|
exports.SingleEntryFileDb = SingleEntryFileDb;
|
|
550
|
+
exports.SingleEntryMemDb = SingleEntryMemDb;
|
|
416
551
|
//# sourceMappingURL=index.cjs.map
|