@rljson/io 0.0.40 → 0.0.42

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