@rljson/io 0.0.55 → 0.0.57

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