@rljson/io 0.0.30 → 0.0.32

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,891 @@
1
+ // @license
2
+ // Copyright (c) 2025 Rljson
3
+ //
4
+ // Use of this source code is governed by terms that can be
5
+ // found in the LICENSE file in the root of this package.
6
+
7
+ // ⚠️ DO NOT MODIFY THIS FILE DIRECTLY ⚠️
8
+ //
9
+ // This file is a copy of @rljson/io/test/io-conformance.spec.ts.
10
+ //
11
+ // To make changes, please execute the following steps:
12
+ // 1. Clone <https://github.com/rljson/io>
13
+ // 2. Make changes to the original file in the test folder
14
+ // 3. Submit a pull request
15
+ // 4. Publish a the new changes to npm
16
+
17
+
18
+ import { hip, rmhsh } from '@rljson/hash';
19
+ import {
20
+ addColumnsToTableCfg,
21
+ exampleTableCfg,
22
+ IngredientsTable,
23
+ Rljson,
24
+ TableCfg,
25
+ TableType,
26
+ } from '@rljson/rljson';
27
+
28
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
29
+
30
+ import { Io, IoTestSetup, IoTools } from '@rljson/io';
31
+
32
+ import { testSetup } from './io-conformance.setup.ts';
33
+ import { expectGolden, ExpectGoldenOptions } from './setup/goldens.ts';
34
+
35
+ const ego: ExpectGoldenOptions = {
36
+ npmUpdateGoldensEnabled: false,
37
+ };
38
+
39
+ export const runIoConformanceTests = () => {
40
+ return describe('Io Conformance', async () => {
41
+ let io: Io;
42
+ let ioTools: IoTools;
43
+ let setup: IoTestSetup;
44
+
45
+ beforeEach(async () => {
46
+ setup = testSetup();
47
+ await setup.init();
48
+ io = setup.io;
49
+ await io.init();
50
+ await io.isReady();
51
+ ioTools = new IoTools(io);
52
+ });
53
+
54
+ afterEach(async () => {
55
+ await io.close();
56
+ await setup.tearDown();
57
+ });
58
+
59
+ describe('isReady()', () => {
60
+ it('should return a resolved promise', async () => {
61
+ await io.isReady();
62
+ });
63
+ });
64
+
65
+ const createExampleTable = async (key: string) => {
66
+ // Register a new table config and generate the table
67
+ const tableCfg: TableCfg = exampleTableCfg({ key });
68
+ try {
69
+ await io.createOrExtendTable({ tableCfg: tableCfg });
70
+ } catch (error) {
71
+ throw error; // Re-throw the error after logging it
72
+ }
73
+ };
74
+
75
+ describe('tableCfgs table', () => {
76
+ it('should be available after isReady() resolves', async () => {
77
+ const dump = await io.dumpTable({ table: 'tableCfgs' });
78
+ await expectGolden('io-conformance/tableCfgs.json', ego).toBe(dump);
79
+ });
80
+ });
81
+
82
+ describe('tableCfgs()', () => {
83
+ it('returns an rljson object containing the newest config for each table', async () => {
84
+ //create four tables with two versions each
85
+ const tableV0: TableCfg = {
86
+ key: 'table0',
87
+ type: 'ingredients',
88
+ isHead: false,
89
+ isRoot: false,
90
+ isShared: true,
91
+ columns: [
92
+ { key: '_hash', type: 'string' },
93
+ { key: 'col0', type: 'string' },
94
+ ],
95
+ };
96
+
97
+ const tableV1 = addColumnsToTableCfg(tableV0, [
98
+ { key: 'col1', type: 'string' },
99
+ ]);
100
+
101
+ const tableV2 = addColumnsToTableCfg(tableV1, [
102
+ { key: 'col2', type: 'string' },
103
+ ]);
104
+
105
+ await io.createOrExtendTable({ tableCfg: tableV0 });
106
+ await io.createOrExtendTable({ tableCfg: tableV1 });
107
+ await io.createOrExtendTable({ tableCfg: tableV2 });
108
+
109
+ // Check the tableCfgs
110
+ const actualTableCfgs = (await io.tableCfgs()).tableCfgs
111
+ ._data as unknown as TableCfg[];
112
+
113
+ expect(actualTableCfgs.length).toBe(3);
114
+ expect((actualTableCfgs[0] as TableCfg).key).toBe('tableCfgs');
115
+ expect((actualTableCfgs[1] as TableCfg).key).toBe('revisions');
116
+ expect((actualTableCfgs[2] as TableCfg).key).toBe('table0');
117
+ });
118
+ });
119
+
120
+ describe('throws an error', async () => {
121
+ it('if the hashes in the tableCfg are wrong', async () => {
122
+ const tableCfg: TableCfg = hip(exampleTableCfg({ key: 'table1' }));
123
+ tableCfg._hash = 'wrongHash';
124
+ let message: string = '';
125
+ try {
126
+ await io.createOrExtendTable({ tableCfg: tableCfg });
127
+ } catch (err: any) {
128
+ message = err.message;
129
+ }
130
+
131
+ expect(message).toBe(
132
+ 'Hash "wrongHash" does not match the newly calculated one "LM5fm8eNChH3kE3D38X0Fa". ' +
133
+ 'Please make sure that all systems are producing the same hashes.',
134
+ );
135
+ });
136
+ });
137
+
138
+ describe('tableExists(tableKey)', () => {
139
+ it('returns true if the table exists', async () => {
140
+ await createExampleTable('table1');
141
+ const exists = await io.tableExists('table1');
142
+ expect(exists).toBe(true);
143
+ });
144
+
145
+ it('returns false if the table does not exist', async () => {
146
+ const exists = await io.tableExists('nonexistentTable');
147
+ expect(exists).toBe(false);
148
+ });
149
+ });
150
+
151
+ describe('createOrExtendTable(request)', () => {
152
+ let existing: TableCfg;
153
+ beforeEach(async () => {
154
+ existing = exampleTableCfg();
155
+ await io.createOrExtendTable({ tableCfg: existing });
156
+ });
157
+
158
+ describe('throws an error', () => {
159
+ it('if the hashes in the tableCfg are wrong', async () => {
160
+ const tableCfg: TableCfg = hip(exampleTableCfg({ key: 'table' }));
161
+ const rightHash = tableCfg._hash;
162
+ tableCfg._hash = 'wrongHash';
163
+ let message: string = '';
164
+ try {
165
+ await io.createOrExtendTable({ tableCfg: tableCfg });
166
+ } catch (err: any) {
167
+ message = err.message;
168
+ }
169
+
170
+ expect(message).toBe(
171
+ `Hash "wrongHash" does not match the newly calculated one "${rightHash}". ` +
172
+ 'Please make sure that all systems are producing the same hashes.',
173
+ );
174
+ });
175
+
176
+ it('when the update has invalid column types', async () => {
177
+ const update = exampleTableCfg({
178
+ ...existing,
179
+ columns: [
180
+ ...existing.columns,
181
+ {
182
+ key: 'x',
183
+ type: 'unknown' as any,
184
+ },
185
+ ],
186
+ });
187
+
188
+ await expect(
189
+ io.createOrExtendTable({ tableCfg: update }),
190
+ ).rejects.toThrow(
191
+ 'Invalid table configuration: Column "x" of table "table" has an unsupported type "unknown"',
192
+ );
193
+ });
194
+
195
+ it('when the update has deleted columns', async () => {
196
+ const update = {
197
+ ...existing,
198
+ columns: [existing.columns[0], existing.columns[1]],
199
+ };
200
+ await expect(
201
+ io.createOrExtendTable({ tableCfg: update }),
202
+ ).rejects.toThrow(
203
+ 'Invalid update of table able "table": ' +
204
+ 'Columns must not be deleted. Deleted columns: b',
205
+ );
206
+ });
207
+
208
+ it('when column keys have changed', async () => {
209
+ const update = {
210
+ ...existing,
211
+ columns: [
212
+ { ...existing.columns[0], key: '_hash' },
213
+ { ...existing.columns[1], key: 'b' },
214
+ { ...existing.columns[2], key: 'a' },
215
+ ],
216
+ };
217
+ await expect(
218
+ io.createOrExtendTable({ tableCfg: update }),
219
+ ).rejects.toThrow(
220
+ 'Invalid update of table able "table": Column keys must not change! ' +
221
+ 'Column "a" was renamed into "b".',
222
+ );
223
+ });
224
+
225
+ it('when column types have changed', async () => {
226
+ const update = {
227
+ ...existing,
228
+ columns: [
229
+ { ...existing.columns[0], type: 'string' },
230
+ { ...existing.columns[1], type: 'boolean' },
231
+ { ...existing.columns[2], type: 'number' },
232
+ ],
233
+ } as TableCfg;
234
+ await expect(
235
+ io.createOrExtendTable({ tableCfg: update }),
236
+ ).rejects.toThrow(
237
+ 'Invalid update of table able "table": ' +
238
+ 'Column types must not change! ' +
239
+ 'Type of column "a" was changed from "string" to boolean.',
240
+ );
241
+ });
242
+ });
243
+
244
+ it('should add a table and a table config', async () => {
245
+ const tablesFromConfig = async () => {
246
+ const tables = (await io.tableCfgs())
247
+ .tableCfgs as IngredientsTable<TableCfg>;
248
+
249
+ return tables._data.map((e) => e.key);
250
+ };
251
+
252
+ const physicalTables = async () => await ioTools.allTableKeys();
253
+
254
+ // Create a first table
255
+ await createExampleTable('table1');
256
+
257
+ expect(await tablesFromConfig()).toEqual([
258
+ 'tableCfgs',
259
+ 'revisions',
260
+ 'table',
261
+ 'table1',
262
+ ]);
263
+ expect(await physicalTables()).toEqual([
264
+ 'tableCfgs',
265
+ 'revisions',
266
+ 'table',
267
+ 'table1',
268
+ ]);
269
+
270
+ // Create a second table
271
+ await createExampleTable('table2');
272
+ expect(await tablesFromConfig()).toEqual([
273
+ 'tableCfgs',
274
+ 'revisions',
275
+ 'table',
276
+ 'table1',
277
+ 'table2',
278
+ ]);
279
+ expect(await physicalTables()).toEqual([
280
+ 'tableCfgs',
281
+ 'revisions',
282
+ 'table',
283
+ 'table1',
284
+ 'table2',
285
+ ]);
286
+ });
287
+
288
+ it('should do nothing when the columns do not have changed', async () => {
289
+ const exampleCfg: TableCfg = exampleTableCfg({ key: 'tableA' });
290
+ await io.createOrExtendTable({ tableCfg: exampleCfg });
291
+
292
+ // Check state before
293
+ const dumpBefore = await io.dumpTable({ table: 'tableA' });
294
+
295
+ // Add same table config again
296
+ await io.createOrExtendTable({ tableCfg: exampleCfg });
297
+
298
+ // Dump again, should be the same
299
+ const dumpAfter = await io.dumpTable({ table: 'tableA' });
300
+ expect(dumpBefore).toEqual(dumpAfter);
301
+ });
302
+
303
+ it('should extend an existing table', async () => {
304
+ // Create a first table
305
+ const tableCfg: TableCfg = exampleTableCfg({ key: 'tableA' });
306
+ await io.createOrExtendTable({ tableCfg });
307
+ await io.write({
308
+ data: {
309
+ tableA: {
310
+ _data: [{ a: 'hello', b: 5 }],
311
+ },
312
+ },
313
+ });
314
+
315
+ // Check the table content before
316
+ const dump = rmhsh(await io.dumpTable({ table: 'tableA' }));
317
+ const dumpExpected = {
318
+ tableA: {
319
+ _data: [
320
+ {
321
+ a: 'hello',
322
+ b: 5,
323
+ },
324
+ ],
325
+ _tableCfg: 'MfpwQygnDmu3ISp6dBjsEf',
326
+ },
327
+ };
328
+ expect(dump).toEqual(dumpExpected);
329
+
330
+ // Update the table by adding a new column
331
+ const tableCfg2 = addColumnsToTableCfg(tableCfg, [
332
+ { key: 'keyA1', type: 'string' },
333
+ { key: 'keyA2', type: 'string' },
334
+ { key: 'keyB2', type: 'string' },
335
+ ]);
336
+
337
+ await io.createOrExtendTable({ tableCfg: tableCfg2 });
338
+
339
+ // Check the table contents after.
340
+ const dump2 = rmhsh(await io.dumpTable({ table: 'tableA' }));
341
+
342
+ // Only the hash of the table config has changed
343
+ expect(dump.tableA._tableCfg).not.toEqual(dump2.tableA._tableCfg);
344
+
345
+ const dumpExpected2 = {
346
+ ...dumpExpected,
347
+ tableA: {
348
+ ...dumpExpected.tableA,
349
+ _tableCfg: dump2.tableA._tableCfg,
350
+ },
351
+ };
352
+
353
+ expect(dump2).toEqual(dumpExpected2);
354
+
355
+ // Now add a new row adding
356
+ await io.write({
357
+ data: {
358
+ tableA: {
359
+ _data: [{ keyA1: 'a1', keyA2: 'a2', keyB2: 'b2' }],
360
+ },
361
+ },
362
+ });
363
+
364
+ // Check the table contents after. It has an additional row.
365
+ const dump3 = rmhsh(await io.dumpTable({ table: 'tableA' }));
366
+ expect(dump3).toEqual({
367
+ tableA: {
368
+ _data: [
369
+ {
370
+ a: 'hello',
371
+ b: 5,
372
+ },
373
+ {
374
+ keyA1: 'a1',
375
+ keyA2: 'a2',
376
+ keyB2: 'b2',
377
+ },
378
+ ],
379
+ _tableCfg: 'swD0rJhzryBIY7sfxIV8Gl',
380
+ },
381
+ });
382
+ });
383
+ });
384
+
385
+ describe('write(request)', async () => {
386
+ it('adds data to existing data', async () => {
387
+ const exampleCfg: TableCfg = exampleTableCfg({ key: 'tableA' });
388
+ const tableCfg: TableCfg = {
389
+ ...exampleCfg,
390
+ columns: [
391
+ { key: '_hash', type: 'string' },
392
+ { key: 'keyA1', type: 'string' },
393
+ { key: 'keyA2', type: 'string' },
394
+ { key: 'keyB2', type: 'string' },
395
+ ],
396
+ };
397
+
398
+ await io.createOrExtendTable({ tableCfg });
399
+ const allTableKeys = await ioTools.allTableKeys();
400
+ expect(allTableKeys).toContain('tableA');
401
+
402
+ expect('tableA').toBe(tableCfg.key);
403
+
404
+ // Write a first item
405
+ await io.write({
406
+ data: {
407
+ tableA: {
408
+ _data: [{ keyA2: 'a2' }],
409
+ },
410
+ },
411
+ });
412
+
413
+ expect(await io.rowCount('tableA')).toEqual(1);
414
+
415
+ const dump = await io.dump();
416
+ const items = (dump.tableA as TableType)._data;
417
+ expect(items).toEqual([
418
+ {
419
+ _hash: 'apLP3I2XLnVm13umIZdVhV',
420
+ keyA2: 'a2',
421
+ },
422
+ ]);
423
+
424
+ // Write a second item
425
+ await io.write({
426
+ data: {
427
+ tableA: {
428
+ _data: [{ keyB2: 'b2' }],
429
+ },
430
+ },
431
+ });
432
+
433
+ const dump2 = await io.dump();
434
+ const items2 = (dump2.tableA as TableType)._data;
435
+ expect(items2).toEqual([
436
+ {
437
+ _hash: 'apLP3I2XLnVm13umIZdVhV',
438
+ keyA2: 'a2',
439
+ },
440
+ {
441
+ _hash: 'oNNJMCE_2iycGPDyM_5_lp',
442
+ keyB2: 'b2',
443
+ },
444
+ ]);
445
+ });
446
+
447
+ it('does not add the same data twice', async () => {
448
+ const tableName = 'testTable';
449
+ const exampleCfg: TableCfg = exampleTableCfg({ key: tableName });
450
+ const tableCfg: TableCfg = {
451
+ ...exampleCfg,
452
+ columns: [
453
+ { key: '_hash', type: 'string' },
454
+ { key: 'string', type: 'string' },
455
+ { key: 'number', type: 'number' },
456
+ { key: 'null', type: 'string' },
457
+ { key: 'boolean', type: 'boolean' },
458
+ { key: 'array', type: 'jsonArray' },
459
+ { key: 'object', type: 'json' },
460
+ ],
461
+ };
462
+
463
+ await io.createOrExtendTable({ tableCfg });
464
+ const allTableKeys = await ioTools.allTableKeys();
465
+ expect(allTableKeys).toContain(tableName);
466
+ expect(await ioTools.allColumnKeys(tableName)).toEqual([
467
+ '_hash',
468
+ 'string',
469
+ 'number',
470
+ 'null',
471
+ 'boolean',
472
+ 'array',
473
+ 'object',
474
+ ]);
475
+
476
+ const rows = [
477
+ {
478
+ string: 'hello',
479
+ number: 5,
480
+ null: null,
481
+ boolean: true,
482
+ array: [1, 2, { a: 10 }],
483
+ object: { a: 1, b: { c: 3 } },
484
+ },
485
+ {
486
+ string: 'world',
487
+ number: 6,
488
+ null: null,
489
+ boolean: true,
490
+ array: [1, 2, { a: 10 }],
491
+ object: { a: 1, b: 2 },
492
+ },
493
+ ];
494
+
495
+ const testData: Rljson = {
496
+ testTable: {
497
+ _data: rows,
498
+ },
499
+ };
500
+
501
+ // Get row count before
502
+ const rowCountBefore = await io.rowCount(tableName);
503
+ expect(rowCountBefore).toEqual(0);
504
+
505
+ // Write first two rows
506
+ await io.write({ data: testData });
507
+ const rowCountAfterFirstWrite = await io.rowCount(tableName);
508
+ expect(rowCountAfterFirstWrite).toEqual(2);
509
+
510
+ // Write the same item again
511
+ expect(io.write({ data: testData }));
512
+
513
+ // Nothing changes because the data is the same
514
+ const rowCountAfterSecondWrite = await io.rowCount(tableName);
515
+ expect(rowCountAfterSecondWrite).toEqual(rowCountAfterFirstWrite);
516
+ });
517
+
518
+ describe('throws', () => {
519
+ it('when table does not exist', async () => {
520
+ await expect(
521
+ io.write({
522
+ data: {
523
+ tableA: {
524
+ _data: [{ keyA2: 'a2' }],
525
+ },
526
+ },
527
+ }),
528
+ ).rejects.toThrow('The following tables do not exist: tableA');
529
+ });
530
+ });
531
+ });
532
+
533
+ describe('readRows({table, where})', async () => {
534
+ describe('should return rows matching the where clause', async () => {
535
+ beforeEach(async () => {
536
+ const tableName = 'testTable';
537
+ const exampleCfg: TableCfg = exampleTableCfg({ key: tableName });
538
+ const tableCfg: TableCfg = {
539
+ ...exampleCfg,
540
+ columns: [
541
+ { key: '_hash', type: 'string' },
542
+ { key: 'string', type: 'string' },
543
+ { key: 'number', type: 'number' },
544
+ { key: 'null', type: 'string' },
545
+ { key: 'boolean', type: 'boolean' },
546
+ { key: 'array', type: 'jsonArray' },
547
+ { key: 'object', type: 'json' },
548
+ ],
549
+ };
550
+
551
+ await io.createOrExtendTable({ tableCfg });
552
+
553
+ const testData: Rljson = {
554
+ testTable: {
555
+ _data: [
556
+ {
557
+ string: 'hello',
558
+ number: 5,
559
+ null: null,
560
+ boolean: true,
561
+ array: [1, 2, { a: 10 }],
562
+ object: { a: 1, b: { c: 3 } },
563
+ },
564
+ {
565
+ string: 'world',
566
+ number: 6,
567
+ null: null,
568
+ boolean: true,
569
+ array: [1, 2, { a: 10 }],
570
+ object: { a: 1, b: 2 },
571
+ },
572
+ {
573
+ string: 'third',
574
+ number: null,
575
+ null: 'test',
576
+ boolean: false,
577
+ array: [3, 4, { a: 10 }],
578
+ object: { a: 1, b: 2 },
579
+ },
580
+ ],
581
+ },
582
+ };
583
+ await io.write({ data: testData });
584
+ });
585
+
586
+ it('with where searching string values', async () => {
587
+ const result = rmhsh(
588
+ await io.readRows({
589
+ table: 'testTable',
590
+ where: { string: 'hello' },
591
+ }),
592
+ );
593
+
594
+ expect(result).toEqual({
595
+ testTable: {
596
+ _data: [
597
+ {
598
+ array: [1, 2, { a: 10 }],
599
+ boolean: true,
600
+ null: null,
601
+ number: 5,
602
+ object: {
603
+ a: 1,
604
+ b: {
605
+ c: 3,
606
+ },
607
+ },
608
+ string: 'hello',
609
+ },
610
+ ],
611
+ },
612
+ });
613
+ });
614
+
615
+ it('with where searching number values', async () => {
616
+ const result = rmhsh(
617
+ await io.readRows({
618
+ table: 'testTable',
619
+ where: { number: 6 },
620
+ }),
621
+ );
622
+
623
+ expect(result).toEqual({
624
+ testTable: {
625
+ _data: [
626
+ {
627
+ array: [1, 2, { a: 10 }],
628
+ boolean: true,
629
+ null: null,
630
+ number: 6,
631
+ object: { a: 1, b: 2 },
632
+ string: 'world',
633
+ },
634
+ ],
635
+ },
636
+ });
637
+ });
638
+
639
+ it('with where searching null values', async () => {
640
+ const result = rmhsh(
641
+ await io.readRows({
642
+ table: 'testTable',
643
+ where: { null: null },
644
+ }),
645
+ );
646
+
647
+ expect(result).toEqual({
648
+ testTable: {
649
+ _data: [
650
+ {
651
+ array: [1, 2, { a: 10 }],
652
+ boolean: true,
653
+ null: null,
654
+ number: 5,
655
+ object: { a: 1, b: { c: 3 } },
656
+ string: 'hello',
657
+ },
658
+ {
659
+ array: [1, 2, { a: 10 }],
660
+ boolean: true,
661
+ null: null,
662
+ number: 6,
663
+ object: { a: 1, b: 2 },
664
+ string: 'world',
665
+ },
666
+ ],
667
+ },
668
+ });
669
+ });
670
+
671
+ it('with where searching boolean values', async () => {
672
+ const result = rmhsh(
673
+ await io.readRows({
674
+ table: 'testTable',
675
+ where: { boolean: true },
676
+ }),
677
+ );
678
+
679
+ expect(result).toEqual({
680
+ testTable: {
681
+ _data: [
682
+ {
683
+ array: [1, 2, { a: 10 }],
684
+ boolean: true,
685
+ null: null,
686
+ number: 5,
687
+ object: { a: 1, b: { c: 3 } },
688
+ string: 'hello',
689
+ },
690
+ {
691
+ array: [1, 2, { a: 10 }],
692
+ boolean: true,
693
+ null: null,
694
+ number: 6,
695
+ object: { a: 1, b: 2 },
696
+ string: 'world',
697
+ },
698
+ ],
699
+ },
700
+ });
701
+ });
702
+
703
+ it('with where searching array values', async () => {
704
+ const result = rmhsh(
705
+ await io.readRows({
706
+ table: 'testTable',
707
+ //where: { array: [1, 2, { a: 10 }] },
708
+ where: {
709
+ array: [1, 2, { a: 10, _hash: 'LeFJOCQVgToOfbUuKJQ-GO' }],
710
+ },
711
+ }),
712
+ );
713
+
714
+ expect(result).toEqual({
715
+ testTable: {
716
+ _data: [
717
+ {
718
+ array: [1, 2, { a: 10 }],
719
+ boolean: true,
720
+ null: null,
721
+ number: 5,
722
+ object: { a: 1, b: { c: 3 } },
723
+ string: 'hello',
724
+ },
725
+ {
726
+ array: [1, 2, { a: 10 }],
727
+ boolean: true,
728
+ null: null,
729
+ number: 6,
730
+ object: { a: 1, b: 2 },
731
+ string: 'world',
732
+ },
733
+ ],
734
+ },
735
+ });
736
+ });
737
+
738
+ it('with where searching object values', async () => {
739
+ const result = rmhsh(
740
+ await io.readRows({
741
+ table: 'testTable',
742
+ //where: { object: { a: 1, b: { c: 3 } } },
743
+ where: {
744
+ object: {
745
+ a: 1,
746
+ b: { c: 3, _hash: 'yrqcsGrHfad4G4u9fgcAxY' },
747
+ _hash: 'd-0fwNtdekpWJzLu4goUDI',
748
+ },
749
+ },
750
+ }),
751
+ );
752
+
753
+ expect(result).toEqual({
754
+ testTable: {
755
+ _data: [
756
+ {
757
+ array: [1, 2, { a: 10 }],
758
+ boolean: true,
759
+ null: null,
760
+ number: 5,
761
+ object: { a: 1, b: { c: 3 } },
762
+ string: 'hello',
763
+ },
764
+ ],
765
+ },
766
+ });
767
+ });
768
+ });
769
+
770
+ it('should return an empty array if no rows match the where clause', async () => {
771
+ await createExampleTable('testTable');
772
+
773
+ await io.write({
774
+ data: {
775
+ testTable: {
776
+ _data: [
777
+ { a: 'value1', b: 2 },
778
+ { a: 'value3', b: 4 },
779
+ ],
780
+ },
781
+ },
782
+ });
783
+
784
+ const result = await io.readRows({
785
+ table: 'testTable',
786
+ where: { a: 'nonexistent' },
787
+ });
788
+
789
+ expect(result).toEqual({
790
+ testTable: {
791
+ _data: [],
792
+ },
793
+ });
794
+ });
795
+
796
+ it('should throw an error if the table does not exist', async () => {
797
+ await expect(
798
+ io.readRows({
799
+ table: 'nonexistentTable',
800
+ where: { column1: 'value1' },
801
+ }),
802
+ ).rejects.toThrow('Table "nonexistentTable" not found');
803
+ });
804
+
805
+ it('should throw an error if the where clause is invalid', async () => {
806
+ await createExampleTable('testTable');
807
+
808
+ await expect(
809
+ io.readRows({
810
+ table: 'testTable',
811
+ where: { invalidColumn: 'value' },
812
+ }),
813
+ ).rejects.toThrow(
814
+ 'The following columns do not exist in table "testTable": invalidColumn.',
815
+ );
816
+ });
817
+ });
818
+
819
+ describe('rowCount(table)', () => {
820
+ it('returns the number of rows in the table', async () => {
821
+ await createExampleTable('table1');
822
+ await createExampleTable('table2');
823
+ await io.write({
824
+ data: {
825
+ table1: {
826
+ _data: [
827
+ { a: 'a1' },
828
+ { a: 'a2' },
829
+ { a: 'a3' },
830
+ { a: 'a4' },
831
+ { a: 'a5' },
832
+ ],
833
+ },
834
+ table2: {
835
+ _data: [{ a: 'a1' }, { a: 'a2' }],
836
+ },
837
+ },
838
+ });
839
+ const count1 = await io.rowCount('table1');
840
+ const count2 = await io.rowCount('table2');
841
+ expect(count1).toBe(5);
842
+ expect(count2).toBe(2);
843
+ });
844
+
845
+ it('throws an error if the table does not exist', async () => {
846
+ await expect(io.rowCount('nonexistentTable')).rejects.toThrow(
847
+ 'Table "nonexistentTable" not found',
848
+ );
849
+ });
850
+ });
851
+
852
+ describe('dump()', () => {
853
+ it('returns a copy of the complete database', async () => {
854
+ await expectGolden('io-conformance/dump/empty.json', ego).toBe(
855
+ await io.dump(),
856
+ );
857
+ await createExampleTable('table1');
858
+ await createExampleTable('table2');
859
+ await expectGolden('io-conformance/dump/two-tables.json', ego).toBe(
860
+ await io.dump(),
861
+ );
862
+ });
863
+ });
864
+
865
+ describe('dumpTable(request)', () => {
866
+ it('returns a copy of the table', async () => {
867
+ await createExampleTable('table1');
868
+
869
+ await io.write({
870
+ data: {
871
+ table1: {
872
+ _data: [{ a: 'a2' }],
873
+ },
874
+ },
875
+ });
876
+
877
+ await expectGolden('io-conformance/dumpTable/table1.json', ego).toBe(
878
+ await io.dumpTable({ table: 'table1' }),
879
+ );
880
+ });
881
+
882
+ it('throws an error if the table does not exist', async () => {
883
+ await expect(
884
+ io.dumpTable({ table: 'nonexistentTable' }),
885
+ ).rejects.toThrow('Table "nonexistentTable" not found');
886
+ });
887
+ });
888
+ });
889
+ };
890
+
891
+ runIoConformanceTests();