@jsonkit/db 2.0.1 → 3.0.2

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 CHANGED
@@ -4,8 +4,8 @@
4
4
 
5
5
  The package exposes two core concepts:
6
6
 
7
- * **Single-entry databases** – manage exactly one JSON-serializable object.
8
- * **Multi-entry databases** – manage collections of identifiable records keyed by an `id`.
7
+ - **Single-entry databases** – manage exactly one JSON-serializable object.
8
+ - **Multi-entry databases** – manage collections of identifiable records keyed by an `id`.
9
9
 
10
10
  Both concepts are available in **file-backed** and **in-memory** variants.
11
11
 
@@ -19,67 +19,28 @@ npm install @jsonkit/db
19
19
 
20
20
  ---
21
21
 
22
- ## Core Types
23
-
24
- ### `Identifiable`
25
-
26
- ```ts
27
- type Identifiable = { id: string }
28
- ```
29
-
30
- All multi-entry databases require entries to have a string `id`.
31
-
32
- ### `Promisable<T>`
33
-
34
- A value or a promise of a value.
35
-
36
- ```ts
37
- type Promisable<T> = T | Promise<T>
38
- ```
39
-
40
- ### `PredicateFn<T>`
41
-
42
- Used for filtering entries.
43
-
44
- ```ts
45
- type PredicateFn<T extends Identifiable> = (entry: T) => boolean
46
- ```
47
-
48
- ### `DeleteManyOutput`
49
-
50
- Returned by bulk delete operations.
51
-
52
- ```ts
53
- type DeleteManyOutput = {
54
- deletedIds: string[]
55
- ignoredIds: string[]
56
- }
57
- ```
58
-
59
- ---
60
-
61
22
  ## Multi-entry Databases
62
23
 
63
24
  Multi-entry databases manage collections of entries keyed by `id`.
64
25
 
65
26
  ### Common API (`MultiEntryDb<T>`)
66
27
 
67
- All multi-entry implementations expose the same async API:
68
-
69
- * `create(entry)`
70
- * `getById(id)`
71
- * `getByIdOrThrow(id)`
72
- * `getWhere(predicate, max?)`
73
- * `getAll(ids?)`
74
- * `getAllIds()`
75
- * `update(id, updater)`
76
- * `delete(id)`
77
- * `deleteByIds(ids)`
78
- * `deleteWhere(predicate)`
79
- * `exists(id)`
80
- * `countAll()`
81
- * `countWhere(predicate)`
82
- * `destroy()`
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()`
83
44
 
84
45
  Updates are **partial merges**, and changing an entry’s `id` during an update is supported.
85
46
 
@@ -97,9 +58,9 @@ const db = new MultiEntryFileDb<User>('./data/users')
97
58
 
98
59
  #### Behavior
99
60
 
100
- * Each entry is stored as `<id>.json` in the provided directory.
101
- * The directory is created implicitly as files are written.
102
- * IDs are validated to prevent path traversal by default.
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.
103
64
 
104
65
  #### Constructor
105
66
 
@@ -116,9 +77,9 @@ new MultiEntryFileDb<T>(dirpath, options?)
116
77
 
117
78
  #### Notes
118
79
 
119
- * Failed reads (missing file or invalid JSON) return `null`.
120
- * `destroy()` deletes the entire directory.
121
- * Intended for development, prototyping, and small datasets.
80
+ - Failed reads (missing file or invalid JSON) return `null`.
81
+ - `destroy()` deletes the entire directory.
82
+ - Intended for development, prototyping, and small datasets.
122
83
 
123
84
  ---
124
85
 
@@ -134,9 +95,9 @@ const db = new MultiEntryMemDb<User>()
134
95
 
135
96
  #### Behavior
136
97
 
137
- * Fast, ephemeral storage.
138
- * Ideal for tests and short-lived processes.
139
- * `destroy()` clears all entries.
98
+ - Fast, ephemeral storage.
99
+ - Ideal for tests and short-lived processes.
100
+ - `destroy()` clears all entries.
140
101
 
141
102
  ---
142
103
 
@@ -146,10 +107,10 @@ Single-entry databases manage **exactly one value**, often used for configuratio
146
107
 
147
108
  ### Common API (`SingleEntryDb<T>`)
148
109
 
149
- * `isInited()`
150
- * `read()`
151
- * `write(entry | updater)`
152
- * `delete()`
110
+ - `isInited()`
111
+ - `read()`
112
+ - `write(entry | updater)`
113
+ - `delete()`
153
114
 
154
115
  `write` supports either replacing the entry or partially updating it via an updater function.
155
116
 
@@ -167,9 +128,9 @@ const db = new SingleEntryFileDb<AppConfig>('./config.json')
167
128
 
168
129
  #### Behavior
169
130
 
170
- * Reads and writes a single JSON file.
171
- * `isInited()` checks file existence.
172
- * `read()` throws if the file does not exist.
131
+ - Reads and writes a single JSON file.
132
+ - `isInited()` checks file existence.
133
+ - `read()` throws if the file does not exist.
173
134
 
174
135
  #### Constructor
175
136
 
@@ -177,7 +138,7 @@ const db = new SingleEntryFileDb<AppConfig>('./config.json')
177
138
  new SingleEntryFileDb<T>(filepath, parser?)
178
139
  ```
179
140
 
180
- * `parser` defaults to `JSON`.
141
+ - `parser` defaults to `JSON`.
181
142
 
182
143
  ---
183
144
 
@@ -193,9 +154,9 @@ const db = new SingleEntryMemDb<AppConfig>()
193
154
 
194
155
  #### Behavior
195
156
 
196
- * Optional initial value.
197
- * `read()` throws if uninitialized.
198
- * `delete()` resets the entry to `null`.
157
+ - Optional initial value.
158
+ - `read()` throws if uninitialized.
159
+ - `delete()` resets the entry to `null`.
199
160
 
200
161
  ---
201
162
 
@@ -217,20 +178,67 @@ const allUsers = await users.getAll()
217
178
 
218
179
  ## Use Cases
219
180
 
220
- * Rapid application prototyping
221
- * CLI tools
222
- * Small internal services
223
- * Tests and mocks
224
- * Configuration and state persistence
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
+ ```
225
233
 
226
234
  ---
227
235
 
228
236
  ## Non-goals
229
237
 
230
- * Concurrency control
231
- * High-performance querying
232
- * Large datasets
233
- * ACID guarantees
238
+ - Concurrency control
239
+ - High-performance querying
240
+ - Large datasets
241
+ - ACID guarantees
234
242
 
235
243
  For these, a dedicated database is recommended.
236
244
 
@@ -27,16 +27,112 @@ 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";
33
133
  FileType["Directory"] = "directory";
34
134
  //Symlink: 'symlink'
35
135
  })(exports.FileType || (exports.FileType = {}));
36
- class SingleEntryDb {
37
- }
38
- class MultiEntryDb {
39
- }
40
136
 
41
137
  const DEFUALT_ENCODING = 'utf-8';
42
138
  class Files {
@@ -260,53 +356,31 @@ class MultiEntryFileDb extends MultiEntryDb {
260
356
  return entry;
261
357
  }
262
358
  async getById(id) {
263
- return await this.readEntry(id);
264
- }
265
- async getByIdOrThrow(id) {
266
- const entry = await this.readEntry(id);
267
- if (!entry) {
268
- throw new Error('Entry with id ' + id + ' does not exist');
269
- }
270
- return entry;
271
- }
272
- async getWhere(predicate, max) {
273
- const entries = await this.getAll();
274
- return entries.filter(predicate).slice(0, max);
275
- }
276
- async getAll(whereIds) {
277
- const ids = whereIds === undefined ? await this.getAllIds() : whereIds;
278
- const entries = [];
279
- for (const id of ids) {
280
- const entry = await this.readEntry(id);
281
- if (entry)
282
- entries.push(entry);
283
- }
284
- return entries;
285
- }
286
- async getAllIds() {
359
+ if (!this.isIdValid(id))
360
+ throw new Error(`Invalid id: ${id}`);
287
361
  try {
288
- const entries = await this.files.list(this.dirpath);
289
- return entries.filter((name) => name.endsWith('.json')).map((name) => name.slice(0, -5)); // Remove .json extension
362
+ const filepath = this.getFilePath(id);
363
+ const text = await this.files.read(filepath);
364
+ const entry = this.parser.parse(text);
365
+ return entry;
290
366
  }
291
- catch {
292
- // Directory might not exist
293
- return [];
367
+ catch (error) {
368
+ console.error('Failed to read entry', error);
369
+ // File doesn't exist or invalid JSON
370
+ return null;
294
371
  }
295
372
  }
296
373
  async update(id, updater) {
297
- const entry = await this.readEntry(id);
298
- if (!entry) {
299
- throw new Error('Entry with id ' + id + ' does not exist');
300
- }
374
+ const entry = await this.getByIdOrThrow(id);
301
375
  const updatedEntryFields = await updater(entry);
302
376
  const updatedEntry = { ...entry, ...updatedEntryFields };
303
377
  await this.writeEntry(updatedEntry);
304
378
  if (updatedEntry.id !== id) {
305
- await this.delete(id);
379
+ await this.deleteById(id);
306
380
  }
307
381
  return updatedEntry;
308
382
  }
309
- async delete(id) {
383
+ async deleteById(id) {
310
384
  try {
311
385
  const filepath = this.getFilePath(id);
312
386
  await this.files.delete(filepath, { force: false });
@@ -320,54 +394,12 @@ class MultiEntryFileDb extends MultiEntryDb {
320
394
  async deleteByIds(ids) {
321
395
  return this.deleteWhere((entry) => ids.includes(entry.id));
322
396
  }
323
- async deleteWhere(predicate) {
324
- const deletedIds = [];
325
- const ignoredIds = [];
326
- for await (const entry of this.iterEntries()) {
327
- if (!predicate(entry))
328
- continue;
329
- const didDelete = await this.delete(entry.id);
330
- if (didDelete) {
331
- deletedIds.push(entry.id);
332
- }
333
- else {
334
- ignoredIds.push(entry.id);
335
- }
336
- }
337
- return { deletedIds, ignoredIds };
338
- }
339
397
  async destroy() {
340
398
  await this.files.delete(this.dirpath);
341
399
  }
342
- async exists(id) {
343
- const entry = await this.readEntry(id);
344
- return entry !== null;
345
- }
346
- async countAll() {
347
- const ids = await this.getAllIds();
348
- return ids.length;
349
- }
350
- async countWhere(predicate) {
351
- return (await this.getWhere(predicate)).length;
352
- }
353
400
  getFilePath(id) {
354
401
  return path__namespace.join(this.dirpath, `${id}.json`);
355
402
  }
356
- async readEntry(id) {
357
- if (!this.isIdValid(id))
358
- throw new Error(`Invalid id: ${id}`);
359
- try {
360
- const filepath = this.getFilePath(id);
361
- const text = await this.files.read(filepath);
362
- const entry = this.parser.parse(text);
363
- return entry;
364
- }
365
- catch (error) {
366
- console.error('Failed to read entry', error);
367
- // File doesn't exist or invalid JSON
368
- return null;
369
- }
370
- }
371
403
  async writeEntry(entry) {
372
404
  if (!this.isIdValid(entry.id))
373
405
  throw new Error(`Invalid id: ${entry.id}`);
@@ -384,18 +416,26 @@ class MultiEntryFileDb extends MultiEntryDb {
384
416
  return true;
385
417
  }
386
418
  async *iterEntries() {
387
- const ids = await this.getAllIds();
388
- for (const id of ids) {
389
- const entry = await this.readEntry(id);
419
+ for await (const id of this.iterIds()) {
420
+ const entry = await this.getById(id);
390
421
  if (entry)
391
422
  yield entry;
392
423
  }
393
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
+ }
394
435
  }
395
436
 
396
- class SingleEntryFileDb extends SingleEntryDb {
437
+ class SingleEntryFileDb {
397
438
  constructor(filepath, parser = JSON) {
398
- super();
399
439
  this.filepath = filepath;
400
440
  this.parser = parser;
401
441
  this.files = new Files();
@@ -443,108 +483,54 @@ class MultiEntryMemDb extends MultiEntryDb {
443
483
  async getById(id) {
444
484
  return this.entries.get(id) ?? null;
445
485
  }
446
- async getByIdOrThrow(id) {
447
- const entry = await this.getById(id);
448
- if (!entry) {
449
- throw new Error('Entry with id ' + id + ' does not exist');
450
- }
451
- return entry;
452
- }
453
- async getWhere(predicate, max) {
454
- const entries = Array.from(this.entries.values()).filter(predicate);
455
- return max !== undefined ? entries.slice(0, max) : entries;
456
- }
457
- async getAll(whereIds) {
458
- if (whereIds === undefined) {
459
- return Array.from(this.entries.values());
460
- }
461
- const entries = [];
462
- for (const id of whereIds) {
463
- const entry = this.entries.get(id);
464
- if (entry)
465
- entries.push(entry);
466
- }
467
- return entries;
468
- }
469
- async getAllIds() {
470
- return Array.from(this.entries.keys());
471
- }
472
486
  async update(id, updater) {
473
- const entry = this.entries.get(id);
474
- if (!entry) {
475
- throw new Error('Entry with id ' + id + ' does not exist');
476
- }
487
+ const entry = await this.getByIdOrThrow(id);
477
488
  const updatedEntryFields = await updater(entry);
478
489
  const updatedEntry = { ...entry, ...updatedEntryFields };
479
490
  this.entries.set(updatedEntry.id, updatedEntry);
480
- if (updatedEntry.id !== id) {
491
+ if (updatedEntry.id !== id)
481
492
  this.entries.delete(id);
482
- }
483
493
  return updatedEntry;
484
494
  }
485
- async delete(id) {
495
+ async deleteById(id) {
486
496
  return this.entries.delete(id);
487
497
  }
488
- async deleteByIds(ids) {
489
- return this.deleteWhere((entry) => ids.includes(entry.id));
490
- }
491
- async deleteWhere(predicate) {
492
- const deletedIds = [];
493
- const ignoredIds = [];
494
- for (const [id, entry] of this.entries) {
495
- if (!predicate(entry))
496
- continue;
497
- const didDelete = await this.delete(id);
498
- if (didDelete) {
499
- deletedIds.push(id);
500
- }
501
- else {
502
- ignoredIds.push(id);
503
- }
504
- }
505
- return { deletedIds, ignoredIds };
506
- }
507
498
  async destroy() {
508
499
  this.entries.clear();
509
500
  }
510
- async exists(id) {
511
- return this.entries.has(id);
512
- }
513
- async countAll() {
514
- return this.entries.size;
515
- }
516
- async countWhere(predicate) {
517
- return Array.from(this.entries.values()).filter(predicate).length;
518
- }
519
501
  async *iterEntries() {
520
502
  for (const entry of this.entries.values()) {
521
503
  yield entry;
522
504
  }
523
505
  }
506
+ async *iterIds() {
507
+ for (const id of this.entries.keys()) {
508
+ yield id;
509
+ }
510
+ }
524
511
  }
525
512
 
526
- class SingleEntryMemDb extends SingleEntryDb {
513
+ class SingleEntryMemDb {
527
514
  constructor(initialEntry = null) {
528
- super();
529
515
  this.entry = null;
530
516
  this.entry = initialEntry;
531
517
  }
532
- async isInited() {
518
+ isInited() {
533
519
  return this.entry !== null;
534
520
  }
535
- async read() {
521
+ read() {
536
522
  if (this.entry === null)
537
523
  throw new Error('Entry not initialized');
538
524
  return this.entry;
539
525
  }
540
- async write(updaterOrEntry) {
526
+ write(updaterOrEntry) {
541
527
  let entry;
542
528
  if (typeof updaterOrEntry === 'function') {
543
529
  const updater = updaterOrEntry;
544
530
  if (this.entry === null) {
545
531
  throw new Error('Cannot update uninitialized entry. Use write(entry) to initialize first.');
546
532
  }
547
- const updatedFields = await updater(this.entry);
533
+ const updatedFields = updater(this.entry);
548
534
  entry = { ...this.entry, ...updatedFields };
549
535
  }
550
536
  else {
@@ -553,15 +539,13 @@ class SingleEntryMemDb extends SingleEntryDb {
553
539
  this.entry = entry;
554
540
  return entry;
555
541
  }
556
- async delete() {
542
+ delete() {
557
543
  this.entry = null;
558
544
  }
559
545
  }
560
546
 
561
- exports.MultiEntryDb = MultiEntryDb;
562
547
  exports.MultiEntryFileDb = MultiEntryFileDb;
563
548
  exports.MultiEntryMemDb = MultiEntryMemDb;
564
- exports.SingleEntryDb = SingleEntryDb;
565
549
  exports.SingleEntryFileDb = SingleEntryFileDb;
566
550
  exports.SingleEntryMemDb = SingleEntryMemDb;
567
551
  //# sourceMappingURL=index.cjs.map