@enspirit/bmg-js 1.0.0
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/.claude/safe-setup/.env.example +3 -0
- package/.claude/safe-setup/Dockerfile.claude +36 -0
- package/.claude/safe-setup/HACKING.md +63 -0
- package/.claude/safe-setup/Makefile +22 -0
- package/.claude/safe-setup/docker-compose.yml +18 -0
- package/.claude/safe-setup/entrypoint.sh +13 -0
- package/.claude/settings.local.json +9 -0
- package/.claude/typescript-annotations.md +273 -0
- package/.github/workflows/test.yml +26 -0
- package/CLAUDE.md +48 -0
- package/Makefile +2 -0
- package/README.md +170 -0
- package/example/README.md +22 -0
- package/example/index.ts +316 -0
- package/example/package.json +16 -0
- package/example/tsconfig.json +11 -0
- package/package.json +34 -0
- package/src/Relation/Memory.ts +213 -0
- package/src/Relation/index.ts +1 -0
- package/src/index.ts +31 -0
- package/src/operators/_helpers.ts +240 -0
- package/src/operators/allbut.ts +19 -0
- package/src/operators/autowrap.ts +26 -0
- package/src/operators/constants.ts +12 -0
- package/src/operators/cross_product.ts +20 -0
- package/src/operators/exclude.ts +14 -0
- package/src/operators/extend.ts +20 -0
- package/src/operators/group.ts +53 -0
- package/src/operators/image.ts +27 -0
- package/src/operators/index.ts +31 -0
- package/src/operators/intersect.ts +24 -0
- package/src/operators/isEqual.ts +29 -0
- package/src/operators/isRelation.ts +5 -0
- package/src/operators/join.ts +25 -0
- package/src/operators/left_join.ts +41 -0
- package/src/operators/matching.ts +24 -0
- package/src/operators/minus.ts +24 -0
- package/src/operators/not_matching.ts +24 -0
- package/src/operators/one.ts +17 -0
- package/src/operators/prefix.ts +7 -0
- package/src/operators/project.ts +18 -0
- package/src/operators/rename.ts +17 -0
- package/src/operators/restrict.ts +14 -0
- package/src/operators/suffix.ts +7 -0
- package/src/operators/summarize.ts +85 -0
- package/src/operators/transform.ts +40 -0
- package/src/operators/ungroup.ts +41 -0
- package/src/operators/union.ts +27 -0
- package/src/operators/unwrap.ts +29 -0
- package/src/operators/where.ts +1 -0
- package/src/operators/wrap.ts +29 -0
- package/src/operators/yByX.ts +12 -0
- package/src/support/toPredicateFunc.ts +12 -0
- package/src/types.ts +178 -0
- package/src/utility-types.ts +77 -0
- package/tests/bmg.test.ts +16 -0
- package/tests/fixtures.ts +9 -0
- package/tests/operators/allbut.test.ts +51 -0
- package/tests/operators/autowrap.test.ts +82 -0
- package/tests/operators/constants.test.ts +37 -0
- package/tests/operators/cross_product.test.ts +90 -0
- package/tests/operators/exclude.test.ts +43 -0
- package/tests/operators/extend.test.ts +45 -0
- package/tests/operators/group.test.ts +69 -0
- package/tests/operators/image.test.ts +152 -0
- package/tests/operators/intersect.test.ts +53 -0
- package/tests/operators/isEqual.test.ts +111 -0
- package/tests/operators/join.test.ts +116 -0
- package/tests/operators/left_join.test.ts +116 -0
- package/tests/operators/matching.test.ts +91 -0
- package/tests/operators/minus.test.ts +47 -0
- package/tests/operators/not_matching.test.ts +104 -0
- package/tests/operators/one.test.ts +19 -0
- package/tests/operators/prefix.test.ts +37 -0
- package/tests/operators/project.test.ts +48 -0
- package/tests/operators/rename.test.ts +39 -0
- package/tests/operators/restrict.test.ts +27 -0
- package/tests/operators/suffix.test.ts +37 -0
- package/tests/operators/summarize.test.ts +109 -0
- package/tests/operators/transform.test.ts +94 -0
- package/tests/operators/ungroup.test.ts +67 -0
- package/tests/operators/union.test.ts +51 -0
- package/tests/operators/unwrap.test.ts +50 -0
- package/tests/operators/where.test.ts +33 -0
- package/tests/operators/wrap.test.ts +54 -0
- package/tests/operators/yByX.test.ts +32 -0
- package/tests/types/relation.test.ts +296 -0
- package/tsconfig.json +37 -0
- package/tsconfig.node.json +9 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Bmg } from 'src';
|
|
3
|
+
import { SUPPLIERS } from 'tests/fixtures';
|
|
4
|
+
import { rename , isEqual } from 'src/operators';
|
|
5
|
+
|
|
6
|
+
describe('.rename', () => {
|
|
7
|
+
|
|
8
|
+
const renaming = {sid: 'id', name: 'lastname'}
|
|
9
|
+
|
|
10
|
+
it('allows renaming relation tuples', () => {
|
|
11
|
+
const renamed = SUPPLIERS.rename(renaming);
|
|
12
|
+
const expected = Bmg([
|
|
13
|
+
{id: 'S1', lastname: 'Smith', status: 20, city: 'London' },
|
|
14
|
+
{id: 'S2', lastname: 'Jones', status: 10, city: 'Paris' },
|
|
15
|
+
{id: 'S3', lastname: 'Blake', status: 30, city: 'Paris' },
|
|
16
|
+
{id: 'S4', lastname: 'Clark', status: 20, city: 'London' },
|
|
17
|
+
{id: 'S5', lastname: 'Adams', status: 30, city: 'Athens' },
|
|
18
|
+
]);
|
|
19
|
+
expect(renamed.isEqual(expected)).to.be.true;
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
///
|
|
23
|
+
|
|
24
|
+
it('can be used standalone', () => {
|
|
25
|
+
const input = SUPPLIERS.toArray();
|
|
26
|
+
const res = rename(input, renaming);
|
|
27
|
+
const expected = SUPPLIERS.rename(renaming);
|
|
28
|
+
expect(isEqual(res, expected)).to.be.true;
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('supports a pure function', () => {
|
|
32
|
+
const input = SUPPLIERS.toArray();
|
|
33
|
+
const renamed = rename(input, (attr) => attr.toUpperCase()) as any[];
|
|
34
|
+
const smith = Bmg(renamed).restrict({ SID: 'S1' }).one();
|
|
35
|
+
const keys = Object.keys(smith).sort();
|
|
36
|
+
expect(keys).to.eql(['CITY', 'NAME', 'SID', 'STATUS']);
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { SUPPLIERS } from 'tests/fixtures';
|
|
4
|
+
import { restrict , isEqual } from 'src/operators';
|
|
5
|
+
|
|
6
|
+
describe('.restrict', () => {
|
|
7
|
+
|
|
8
|
+
it('allows filtering relations', () => {
|
|
9
|
+
const smith = SUPPLIERS.restrict((t) => t.sid === 'S1').one()
|
|
10
|
+
expect(smith.name).to.eql('Smith')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('has a tuple shortcut', () => {
|
|
14
|
+
const smith = SUPPLIERS.restrict({sid: 'S1'}).one()
|
|
15
|
+
expect(smith.name).to.eql('Smith')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
///
|
|
19
|
+
|
|
20
|
+
it('can be used standalone', () => {
|
|
21
|
+
const input = SUPPLIERS.toArray();
|
|
22
|
+
const res = restrict(input, { sid: 'S1' });
|
|
23
|
+
const expected = SUPPLIERS.restrict({ sid: 'S1' });
|
|
24
|
+
expect(isEqual(res, expected)).to.be.true;
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Bmg } from 'src';
|
|
3
|
+
import { SUPPLIERS } from 'tests/fixtures';
|
|
4
|
+
import { suffix , isEqual } from 'src/operators';
|
|
5
|
+
|
|
6
|
+
describe('.suffix', () => {
|
|
7
|
+
|
|
8
|
+
it('suffixes all attribute names', () => {
|
|
9
|
+
const suffixed = SUPPLIERS.suffix('_s');
|
|
10
|
+
const expected = Bmg([
|
|
11
|
+
{sid_s: 'S1', name_s: 'Smith', status_s: 20, city_s: 'London' },
|
|
12
|
+
{sid_s: 'S2', name_s: 'Jones', status_s: 10, city_s: 'Paris' },
|
|
13
|
+
{sid_s: 'S3', name_s: 'Blake', status_s: 30, city_s: 'Paris' },
|
|
14
|
+
{sid_s: 'S4', name_s: 'Clark', status_s: 20, city_s: 'London' },
|
|
15
|
+
{sid_s: 'S5', name_s: 'Adams', status_s: 30, city_s: 'Athens' },
|
|
16
|
+
]);
|
|
17
|
+
expect(suffixed.isEqual(expected)).to.be.true;
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('can exclude specific attributes', () => {
|
|
21
|
+
const suffixed = SUPPLIERS.suffix('_x', { except: ['sid'] });
|
|
22
|
+
const smith = suffixed.restrict({sid: 'S1'}).one();
|
|
23
|
+
expect(smith).to.have.property('sid', 'S1');
|
|
24
|
+
expect(smith).to.have.property('name_x', 'Smith');
|
|
25
|
+
expect(smith).to.have.property('city_x', 'London');
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
///
|
|
29
|
+
|
|
30
|
+
it('can be used standalone', () => {
|
|
31
|
+
const input = SUPPLIERS.toArray();
|
|
32
|
+
const res = suffix(input, '_y');
|
|
33
|
+
const expected = SUPPLIERS.suffix('_y');
|
|
34
|
+
expect(isEqual(res, expected)).to.be.true;
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { SUPPLIERS } from 'tests/fixtures';
|
|
3
|
+
import { Bmg } from 'src';
|
|
4
|
+
import { summarize, isEqual } from 'src/operators';
|
|
5
|
+
|
|
6
|
+
describe('.summarize', () => {
|
|
7
|
+
|
|
8
|
+
it('counts tuples per group', () => {
|
|
9
|
+
const result = SUPPLIERS.summarize(['city'], { count: 'count' });
|
|
10
|
+
const expected = Bmg([
|
|
11
|
+
{ city: 'London', count: 2 },
|
|
12
|
+
{ city: 'Paris', count: 2 },
|
|
13
|
+
{ city: 'Athens', count: 1 },
|
|
14
|
+
]);
|
|
15
|
+
expect(result.isEqual(expected)).to.be.true;
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('sums values per group', () => {
|
|
19
|
+
const result = SUPPLIERS.summarize(['city'], {
|
|
20
|
+
totalStatus: { op: 'sum', attr: 'status' }
|
|
21
|
+
});
|
|
22
|
+
const expected = Bmg([
|
|
23
|
+
{ city: 'London', totalStatus: 40 }, // 20 + 20
|
|
24
|
+
{ city: 'Paris', totalStatus: 40 }, // 10 + 30
|
|
25
|
+
{ city: 'Athens', totalStatus: 30 },
|
|
26
|
+
]);
|
|
27
|
+
expect(result.isEqual(expected)).to.be.true;
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('computes min/max per group', () => {
|
|
31
|
+
const result = SUPPLIERS.summarize(['city'], {
|
|
32
|
+
minStatus: { op: 'min', attr: 'status' },
|
|
33
|
+
maxStatus: { op: 'max', attr: 'status' }
|
|
34
|
+
});
|
|
35
|
+
const expected = Bmg([
|
|
36
|
+
{ city: 'London', minStatus: 20, maxStatus: 20 },
|
|
37
|
+
{ city: 'Paris', minStatus: 10, maxStatus: 30 },
|
|
38
|
+
{ city: 'Athens', minStatus: 30, maxStatus: 30 },
|
|
39
|
+
]);
|
|
40
|
+
expect(result.isEqual(expected)).to.be.true;
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('computes average per group', () => {
|
|
44
|
+
const result = SUPPLIERS.summarize(['city'], {
|
|
45
|
+
avgStatus: { op: 'avg', attr: 'status' }
|
|
46
|
+
});
|
|
47
|
+
const expected = Bmg([
|
|
48
|
+
{ city: 'London', avgStatus: 20 }, // (20 + 20) / 2
|
|
49
|
+
{ city: 'Paris', avgStatus: 20 }, // (10 + 30) / 2
|
|
50
|
+
{ city: 'Athens', avgStatus: 30 },
|
|
51
|
+
]);
|
|
52
|
+
expect(result.isEqual(expected)).to.be.true;
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('collects values per group', () => {
|
|
56
|
+
const result = SUPPLIERS.summarize(['city'], {
|
|
57
|
+
names: { op: 'collect', attr: 'name' }
|
|
58
|
+
});
|
|
59
|
+
// collect returns arrays - check specific group
|
|
60
|
+
const paris = result.restrict({ city: 'Paris' }).one();
|
|
61
|
+
expect(paris.names).to.include('Jones');
|
|
62
|
+
expect(paris.names).to.include('Blake');
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('supports custom aggregator functions', () => {
|
|
66
|
+
const data = Bmg([
|
|
67
|
+
{ city: 'NYC', name: 'Alice' },
|
|
68
|
+
{ city: 'NYC', name: 'Bob' },
|
|
69
|
+
]);
|
|
70
|
+
const result = data.summarize(['city'], {
|
|
71
|
+
names: (tuples) => tuples.map(t => t.name).sort().join(', ')
|
|
72
|
+
});
|
|
73
|
+
const expected = Bmg([
|
|
74
|
+
{ city: 'NYC', names: 'Alice, Bob' }
|
|
75
|
+
]);
|
|
76
|
+
expect(result.isEqual(expected)).to.be.true;
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('supports multiple group-by attributes', () => {
|
|
80
|
+
const data = Bmg([
|
|
81
|
+
{ a: 1, b: 'x', v: 10 },
|
|
82
|
+
{ a: 1, b: 'x', v: 20 },
|
|
83
|
+
{ a: 1, b: 'y', v: 30 },
|
|
84
|
+
{ a: 2, b: 'x', v: 40 },
|
|
85
|
+
]);
|
|
86
|
+
const result = data.summarize(['a', 'b'], { sum: { op: 'sum', attr: 'v' } });
|
|
87
|
+
const expected = Bmg([
|
|
88
|
+
{ a: 1, b: 'x', sum: 30 },
|
|
89
|
+
{ a: 1, b: 'y', sum: 30 },
|
|
90
|
+
{ a: 2, b: 'x', sum: 40 },
|
|
91
|
+
]);
|
|
92
|
+
expect(result.isEqual(expected)).to.be.true;
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('handles empty by array (grand total)', () => {
|
|
96
|
+
const result = SUPPLIERS.summarize([], { total: 'count' });
|
|
97
|
+
const expected = Bmg([{ total: 5 }]);
|
|
98
|
+
expect(result.isEqual(expected)).to.be.true;
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
///
|
|
102
|
+
|
|
103
|
+
it('can be used standalone', () => {
|
|
104
|
+
const standalone = summarize(SUPPLIERS.toArray(), ['city'], { count: 'count' });
|
|
105
|
+
const expected = SUPPLIERS.summarize(['city'], { count: 'count' });
|
|
106
|
+
expect(isEqual(standalone, expected)).to.be.true;
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Bmg } from 'src';
|
|
3
|
+
import { transform, isEqual } from 'src/operators';
|
|
4
|
+
|
|
5
|
+
describe('.transform', () => {
|
|
6
|
+
|
|
7
|
+
it('applies a function to all attribute values', () => {
|
|
8
|
+
const data = Bmg([
|
|
9
|
+
{ id: 1, value: 10 },
|
|
10
|
+
{ id: 2, value: 20 },
|
|
11
|
+
]);
|
|
12
|
+
const result = data.transform(String);
|
|
13
|
+
const expected = Bmg([
|
|
14
|
+
{ id: '1', value: '10' },
|
|
15
|
+
{ id: '2', value: '20' },
|
|
16
|
+
]);
|
|
17
|
+
expect(result.isEqual(expected)).to.be.true;
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('applies specific functions to named attributes', () => {
|
|
21
|
+
const data = Bmg([
|
|
22
|
+
{ name: 'alice', status: 10, city: 'NYC' },
|
|
23
|
+
{ name: 'bob', status: 20, city: 'LA' },
|
|
24
|
+
]);
|
|
25
|
+
const result = data.transform({
|
|
26
|
+
name: (v) => (v as string).toUpperCase(),
|
|
27
|
+
status: (v) => (v as number) * 2
|
|
28
|
+
});
|
|
29
|
+
const expected = Bmg([
|
|
30
|
+
{ name: 'ALICE', status: 20, city: 'NYC' },
|
|
31
|
+
{ name: 'BOB', status: 40, city: 'LA' },
|
|
32
|
+
]);
|
|
33
|
+
expect(result.isEqual(expected)).to.be.true;
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('chains multiple transformations with array', () => {
|
|
37
|
+
const data = Bmg([
|
|
38
|
+
{ id: 1, value: 10 },
|
|
39
|
+
{ id: 2, value: 20 },
|
|
40
|
+
]);
|
|
41
|
+
const result = data.transform([
|
|
42
|
+
String,
|
|
43
|
+
(v) => `[${v}]`
|
|
44
|
+
]);
|
|
45
|
+
const expected = Bmg([
|
|
46
|
+
{ id: '[1]', value: '[10]' },
|
|
47
|
+
{ id: '[2]', value: '[20]' },
|
|
48
|
+
]);
|
|
49
|
+
expect(result.isEqual(expected)).to.be.true;
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('chains per-attribute transformations', () => {
|
|
53
|
+
const data = Bmg([
|
|
54
|
+
{ name: 'Alice' },
|
|
55
|
+
{ name: 'Bob' },
|
|
56
|
+
]);
|
|
57
|
+
const result = data.transform({
|
|
58
|
+
name: [(v) => (v as string).toLowerCase(), (v) => (v as string).toUpperCase()]
|
|
59
|
+
});
|
|
60
|
+
const expected = Bmg([
|
|
61
|
+
{ name: 'ALICE' },
|
|
62
|
+
{ name: 'BOB' },
|
|
63
|
+
]);
|
|
64
|
+
expect(result.isEqual(expected)).to.be.true;
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('leaves attributes without transformers unchanged', () => {
|
|
68
|
+
const data = Bmg([
|
|
69
|
+
{ id: 1, name: 'alice', status: 10 },
|
|
70
|
+
{ id: 2, name: 'bob', status: 20 },
|
|
71
|
+
]);
|
|
72
|
+
const result = data.transform({
|
|
73
|
+
name: (v) => (v as string).toUpperCase()
|
|
74
|
+
});
|
|
75
|
+
const expected = Bmg([
|
|
76
|
+
{ id: 1, name: 'ALICE', status: 10 },
|
|
77
|
+
{ id: 2, name: 'BOB', status: 20 },
|
|
78
|
+
]);
|
|
79
|
+
expect(result.isEqual(expected)).to.be.true;
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
///
|
|
83
|
+
|
|
84
|
+
it('can be used standalone', () => {
|
|
85
|
+
const data = Bmg([
|
|
86
|
+
{ name: 'alice', value: 10 },
|
|
87
|
+
{ name: 'bob', value: 20 },
|
|
88
|
+
]);
|
|
89
|
+
const standalone = transform(data.toArray(), { name: (v) => (v as string).toUpperCase() });
|
|
90
|
+
const expected = data.transform({ name: (v) => (v as string).toUpperCase() });
|
|
91
|
+
expect(isEqual(standalone, expected)).to.be.true;
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Bmg } from 'src';
|
|
3
|
+
import { ungroup , isEqual } from 'src/operators';
|
|
4
|
+
|
|
5
|
+
describe('.ungroup', () => {
|
|
6
|
+
|
|
7
|
+
const grouped = Bmg([
|
|
8
|
+
{
|
|
9
|
+
order_id: 1,
|
|
10
|
+
customer: 'Alice',
|
|
11
|
+
items: [
|
|
12
|
+
{ item: 'Apple', qty: 2 },
|
|
13
|
+
{ item: 'Banana', qty: 3 },
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
order_id: 2,
|
|
18
|
+
customer: 'Bob',
|
|
19
|
+
items: [
|
|
20
|
+
{ item: 'Cherry', qty: 1 },
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
it('flattens nested relation into parent tuples', () => {
|
|
26
|
+
const result = grouped.ungroup('items');
|
|
27
|
+
const expected = Bmg([
|
|
28
|
+
{ order_id: 1, customer: 'Alice', item: 'Apple', qty: 2 },
|
|
29
|
+
{ order_id: 1, customer: 'Alice', item: 'Banana', qty: 3 },
|
|
30
|
+
{ order_id: 2, customer: 'Bob', item: 'Cherry', qty: 1 },
|
|
31
|
+
]);
|
|
32
|
+
expect(result.isEqual(expected)).to.be.true;
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('preserves parent attributes', () => {
|
|
36
|
+
const result = grouped.ungroup('items') as any;
|
|
37
|
+
const appleRow = result.restrict({ item: 'Apple' }).one();
|
|
38
|
+
expect(appleRow.order_id).to.eql(1);
|
|
39
|
+
expect(appleRow.customer).to.eql('Alice');
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('merges nested attributes', () => {
|
|
43
|
+
const result = grouped.ungroup('items') as any;
|
|
44
|
+
const appleRow = result.restrict({ item: 'Apple' }).one();
|
|
45
|
+
expect(appleRow.item).to.eql('Apple');
|
|
46
|
+
expect(appleRow.qty).to.eql(2);
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('is the inverse of group', () => {
|
|
50
|
+
const orders = Bmg([
|
|
51
|
+
{ order_id: 1, customer: 'Alice', item: 'Apple', qty: 2 },
|
|
52
|
+
{ order_id: 1, customer: 'Alice', item: 'Banana', qty: 3 },
|
|
53
|
+
{ order_id: 2, customer: 'Bob', item: 'Cherry', qty: 1 },
|
|
54
|
+
]);
|
|
55
|
+
const roundtrip = orders.group(['item', 'qty'], 'items').ungroup('items');
|
|
56
|
+
expect(roundtrip.isEqual(orders)).to.be.true;
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
///
|
|
60
|
+
|
|
61
|
+
it('can be used standalone', () => {
|
|
62
|
+
const res = ungroup(grouped.toArray(), 'items');
|
|
63
|
+
const expected = grouped.ungroup('items');
|
|
64
|
+
expect(isEqual(res, expected)).to.be.true;
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Bmg } from 'src';
|
|
3
|
+
import { union , isEqual } from 'src/operators';
|
|
4
|
+
|
|
5
|
+
describe('.union', () => {
|
|
6
|
+
|
|
7
|
+
const left = Bmg([
|
|
8
|
+
{ id: 1, name: 'Alice' },
|
|
9
|
+
{ id: 2, name: 'Bob' },
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
const right = Bmg([
|
|
13
|
+
{ id: 2, name: 'Bob' },
|
|
14
|
+
{ id: 3, name: 'Charlie' },
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
it('combines tuples from both relations', () => {
|
|
18
|
+
const result = left.union(right);
|
|
19
|
+
const expected = Bmg([
|
|
20
|
+
{ id: 1, name: 'Alice' },
|
|
21
|
+
{ id: 2, name: 'Bob' },
|
|
22
|
+
{ id: 3, name: 'Charlie' },
|
|
23
|
+
]);
|
|
24
|
+
expect(result.isEqual(expected)).to.be.true;
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('removes duplicates', () => {
|
|
28
|
+
const result = left.union(right);
|
|
29
|
+
const expected = Bmg([
|
|
30
|
+
{ id: 1, name: 'Alice' },
|
|
31
|
+
{ id: 2, name: 'Bob' },
|
|
32
|
+
{ id: 3, name: 'Charlie' },
|
|
33
|
+
]);
|
|
34
|
+
expect(result.isEqual(expected)).to.be.true;
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('is commutative (set semantics)', () => {
|
|
38
|
+
const lr = left.union(right);
|
|
39
|
+
const rl = right.union(left);
|
|
40
|
+
expect(lr.isEqual(rl)).to.be.true;
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
///
|
|
44
|
+
|
|
45
|
+
it('can be used standalone', () => {
|
|
46
|
+
const res = union(left.toArray(), right.toArray());
|
|
47
|
+
const expected = left.union(right);
|
|
48
|
+
expect(isEqual(res, expected)).to.be.true;
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Bmg } from 'src';
|
|
3
|
+
import { SUPPLIERS } from 'tests/fixtures';
|
|
4
|
+
import { unwrap , isEqual } from 'src/operators';
|
|
5
|
+
|
|
6
|
+
describe('.unwrap', () => {
|
|
7
|
+
|
|
8
|
+
const wrapped = Bmg([
|
|
9
|
+
{ sid: 'S1', name: 'Smith', details: { status: 20, city: 'London' } },
|
|
10
|
+
{ sid: 'S2', name: 'Jones', details: { status: 10, city: 'Paris' } },
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
it('flattens tuple-valued attribute', () => {
|
|
14
|
+
const result = wrapped.unwrap('details');
|
|
15
|
+
const expected = Bmg([
|
|
16
|
+
{ sid: 'S1', name: 'Smith', status: 20, city: 'London' },
|
|
17
|
+
{ sid: 'S2', name: 'Jones', status: 10, city: 'Paris' },
|
|
18
|
+
]);
|
|
19
|
+
expect(result.isEqual(expected)).to.be.true;
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('removes the wrapped attribute', () => {
|
|
23
|
+
const result = wrapped.unwrap('details');
|
|
24
|
+
const smith = result.restrict({ sid: 'S1' }).one();
|
|
25
|
+
expect(smith).to.not.have.property('details');
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('is the inverse of wrap', () => {
|
|
29
|
+
const roundtrip = SUPPLIERS.wrap(['status', 'city'], 'details').unwrap('details');
|
|
30
|
+
expect(roundtrip.isEqual(SUPPLIERS)).to.be.true;
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('handles deeply nested unwrap', () => {
|
|
34
|
+
const nested = Bmg([
|
|
35
|
+
{ id: 1, outer: { inner: { value: 42 } } }
|
|
36
|
+
]);
|
|
37
|
+
const result = nested.unwrap('outer');
|
|
38
|
+
const expected = Bmg([{ id: 1, inner: { value: 42 } }]);
|
|
39
|
+
expect(result.isEqual(expected)).to.be.true;
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
///
|
|
43
|
+
|
|
44
|
+
it('can be used standalone', () => {
|
|
45
|
+
const res = unwrap(wrapped.toArray(), 'details');
|
|
46
|
+
const expected = wrapped.unwrap('details');
|
|
47
|
+
expect(isEqual(res, expected)).to.be.true;
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { SUPPLIERS } from 'tests/fixtures';
|
|
4
|
+
import { where , isEqual } from 'src/operators';
|
|
5
|
+
|
|
6
|
+
describe('.where', () => {
|
|
7
|
+
|
|
8
|
+
it('allows filtering relations (alias for restrict)', () => {
|
|
9
|
+
const smith = SUPPLIERS.where((t) => t.sid === 'S1').one()
|
|
10
|
+
expect(smith.name).to.eql('Smith')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('has a tuple shortcut', () => {
|
|
14
|
+
const smith = SUPPLIERS.where({sid: 'S1'}).one()
|
|
15
|
+
expect(smith.name).to.eql('Smith')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('returns same result as restrict', () => {
|
|
19
|
+
const withWhere = SUPPLIERS.where({city: 'Paris'});
|
|
20
|
+
const withRestrict = SUPPLIERS.restrict({city: 'Paris'});
|
|
21
|
+
expect(withWhere.isEqual(withRestrict)).to.be.true;
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
///
|
|
25
|
+
|
|
26
|
+
it('can be used standalone', () => {
|
|
27
|
+
const input = SUPPLIERS.toArray();
|
|
28
|
+
const res = where(input, { sid: 'S1' });
|
|
29
|
+
const expected = SUPPLIERS.where({ sid: 'S1' });
|
|
30
|
+
expect(isEqual(res, expected)).to.be.true;
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Bmg } from 'src';
|
|
3
|
+
import { SUPPLIERS } from 'tests/fixtures';
|
|
4
|
+
import { wrap , isEqual } from 'src/operators';
|
|
5
|
+
|
|
6
|
+
describe('.wrap', () => {
|
|
7
|
+
|
|
8
|
+
it('wraps specified attributes into a tuple', () => {
|
|
9
|
+
const result = SUPPLIERS.wrap(['status', 'city'], 'details');
|
|
10
|
+
const expected = Bmg([
|
|
11
|
+
{ sid: 'S1', name: 'Smith', details: { status: 20, city: 'London' } },
|
|
12
|
+
{ sid: 'S2', name: 'Jones', details: { status: 10, city: 'Paris' } },
|
|
13
|
+
{ sid: 'S3', name: 'Blake', details: { status: 30, city: 'Paris' } },
|
|
14
|
+
{ sid: 'S4', name: 'Clark', details: { status: 20, city: 'London' } },
|
|
15
|
+
{ sid: 'S5', name: 'Adams', details: { status: 30, city: 'Athens' } },
|
|
16
|
+
]);
|
|
17
|
+
expect(result.isEqual(expected)).to.be.true;
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('preserves non-wrapped attributes', () => {
|
|
21
|
+
const result = SUPPLIERS.wrap(['status', 'city'], 'details');
|
|
22
|
+
const smith = result.restrict({ sid: 'S1' }).one();
|
|
23
|
+
expect(smith.sid).to.eql('S1');
|
|
24
|
+
expect(smith.name).to.eql('Smith');
|
|
25
|
+
expect(Object.keys(smith).sort()).to.eql(['details', 'name', 'sid']);
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('handles wrapping all attributes', () => {
|
|
29
|
+
const result = SUPPLIERS.wrap(['sid', 'name', 'status', 'city'], 'all');
|
|
30
|
+
const expected = Bmg([
|
|
31
|
+
{ all: { sid: 'S1', name: 'Smith', status: 20, city: 'London' } },
|
|
32
|
+
{ all: { sid: 'S2', name: 'Jones', status: 10, city: 'Paris' } },
|
|
33
|
+
{ all: { sid: 'S3', name: 'Blake', status: 30, city: 'Paris' } },
|
|
34
|
+
{ all: { sid: 'S4', name: 'Clark', status: 20, city: 'London' } },
|
|
35
|
+
{ all: { sid: 'S5', name: 'Adams', status: 30, city: 'Athens' } },
|
|
36
|
+
]);
|
|
37
|
+
expect(result.isEqual(expected)).to.be.true;
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('handles wrapping single attribute', () => {
|
|
41
|
+
const result = SUPPLIERS.wrap(['city'], 'location');
|
|
42
|
+
const smith = result.restrict({ sid: 'S1' }).one();
|
|
43
|
+
expect(smith.location).to.eql({ city: 'London' });
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
///
|
|
47
|
+
|
|
48
|
+
it('can be used standalone', () => {
|
|
49
|
+
const res = wrap(SUPPLIERS.toArray(), ['status', 'city'], 'details');
|
|
50
|
+
const expected = SUPPLIERS.wrap(['status', 'city'], 'details');
|
|
51
|
+
expect(isEqual(res, expected)).to.be.true;
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { SUPPLIERS } from 'tests/fixtures';
|
|
3
|
+
import { yByX } from 'src/operators';
|
|
4
|
+
|
|
5
|
+
describe('.yByX', () => {
|
|
6
|
+
|
|
7
|
+
it('is available on relations', () => {
|
|
8
|
+
expect(SUPPLIERS.yByX('name', 'sid')).to.eql({
|
|
9
|
+
'S1': 'Smith',
|
|
10
|
+
'S2': 'Jones',
|
|
11
|
+
'S3': 'Blake',
|
|
12
|
+
'S4': 'Clark',
|
|
13
|
+
'S5': 'Adams',
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
///
|
|
18
|
+
|
|
19
|
+
it('can be used standalone', () => {
|
|
20
|
+
const tuples = [
|
|
21
|
+
{sid: 'S1', name: 'Smith'},
|
|
22
|
+
{sid: 'S2', name: 'Jones'},
|
|
23
|
+
]
|
|
24
|
+
expect(yByX(tuples, 'name', 'sid')).to.eql(
|
|
25
|
+
{
|
|
26
|
+
'S1': 'Smith',
|
|
27
|
+
'S2': 'Jones',
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
});
|