@rljson/io 0.0.56 → 0.0.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.architecture.md +9 -9
- package/README.blog.md +3 -3
- package/README.contributors.md +32 -32
- package/README.md +18 -18
- package/README.public.md +7 -7
- package/README.trouble.md +36 -36
- package/dist/README.architecture.md +9 -9
- package/dist/README.blog.md +3 -3
- package/dist/README.contributors.md +32 -32
- package/dist/README.md +18 -18
- package/dist/README.public.md +7 -7
- package/dist/README.trouble.md +36 -36
- package/dist/conformance-tests/goldens/dump/empty.json +134 -134
- package/dist/conformance-tests/goldens/dump/two-tables.json +208 -208
- package/dist/conformance-tests/goldens/dumpTable/table1.json +12 -12
- package/dist/conformance-tests/goldens/rawTableCfgs.json +213 -213
- package/dist/conformance-tests/goldens/tableCfgs-1.json +158 -158
- package/dist/conformance-tests/goldens/tableCfgs.json +127 -127
- package/dist/conformance-tests/io-conformance.setup.ts +38 -38
- package/dist/conformance-tests/io-conformance.spec.ts +1069 -1069
- package/dist/conformance-tests/scripts/install-conformance-tests.js +103 -103
- package/dist/index.d.ts +1 -0
- package/dist/io-db-name-mapping.d.ts +15 -0
- package/dist/io.js +39 -1
- package/dist/src/example.ts +10 -10
- package/package.json +9 -9
|
@@ -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();
|