@enspirit/bmg-js 1.0.0 → 1.0.2

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.
Files changed (103) hide show
  1. package/LICENSE.md +21 -0
  2. package/dist/Relation/Memory.d.ts +45 -0
  3. package/dist/Relation/index.d.ts +1 -0
  4. package/dist/bmg.cjs +2 -0
  5. package/dist/bmg.cjs.map +1 -0
  6. package/dist/bmg.modern.js +2 -0
  7. package/dist/bmg.modern.js.map +1 -0
  8. package/dist/bmg.module.js +2 -0
  9. package/dist/bmg.module.js.map +1 -0
  10. package/dist/bmg.umd.js +2 -0
  11. package/dist/bmg.umd.js.map +1 -0
  12. package/dist/index.d.ts +27 -0
  13. package/dist/lib-definitions.d.ts +1 -0
  14. package/dist/operators/_helpers.d.ts +142 -0
  15. package/dist/operators/allbut.d.ts +2 -0
  16. package/dist/operators/autowrap.d.ts +2 -0
  17. package/dist/operators/constants.d.ts +2 -0
  18. package/dist/operators/cross_product.d.ts +3 -0
  19. package/dist/operators/exclude.d.ts +2 -0
  20. package/dist/operators/extend.d.ts +2 -0
  21. package/dist/operators/group.d.ts +2 -0
  22. package/dist/operators/image.d.ts +2 -0
  23. package/dist/operators/index.d.ts +30 -0
  24. package/dist/operators/intersect.d.ts +2 -0
  25. package/dist/operators/isEqual.d.ts +2 -0
  26. package/dist/operators/isRelation.d.ts +1 -0
  27. package/dist/operators/join.d.ts +2 -0
  28. package/dist/operators/left_join.d.ts +2 -0
  29. package/dist/operators/matching.d.ts +2 -0
  30. package/dist/operators/minus.d.ts +2 -0
  31. package/dist/operators/not_matching.d.ts +2 -0
  32. package/dist/operators/one.d.ts +2 -0
  33. package/dist/operators/prefix.d.ts +2 -0
  34. package/dist/operators/project.d.ts +2 -0
  35. package/dist/operators/rename.d.ts +2 -0
  36. package/dist/operators/restrict.d.ts +2 -0
  37. package/dist/operators/suffix.d.ts +2 -0
  38. package/dist/operators/summarize.d.ts +2 -0
  39. package/dist/operators/transform.d.ts +2 -0
  40. package/dist/operators/ungroup.d.ts +2 -0
  41. package/dist/operators/union.d.ts +2 -0
  42. package/dist/operators/unwrap.d.ts +2 -0
  43. package/dist/operators/where.d.ts +1 -0
  44. package/dist/operators/wrap.d.ts +2 -0
  45. package/dist/operators/yByX.d.ts +2 -0
  46. package/dist/support/toPredicateFunc.d.ts +2 -0
  47. package/dist/types.d.ts +162 -0
  48. package/package.json +20 -6
  49. package/src/Relation/Memory.ts +13 -12
  50. package/src/index.ts +1 -1
  51. package/src/lib-definitions.ts +281 -0
  52. package/src/types.ts +142 -54
  53. package/.claude/safe-setup/.env.example +0 -3
  54. package/.claude/safe-setup/Dockerfile.claude +0 -36
  55. package/.claude/safe-setup/HACKING.md +0 -63
  56. package/.claude/safe-setup/Makefile +0 -22
  57. package/.claude/safe-setup/docker-compose.yml +0 -18
  58. package/.claude/safe-setup/entrypoint.sh +0 -13
  59. package/.claude/settings.local.json +0 -9
  60. package/.claude/typescript-annotations.md +0 -273
  61. package/.github/workflows/test.yml +0 -26
  62. package/CLAUDE.md +0 -48
  63. package/Makefile +0 -2
  64. package/example/README.md +0 -22
  65. package/example/index.ts +0 -316
  66. package/example/package.json +0 -16
  67. package/example/tsconfig.json +0 -11
  68. package/src/utility-types.ts +0 -77
  69. package/tests/bmg.test.ts +0 -16
  70. package/tests/fixtures.ts +0 -9
  71. package/tests/operators/allbut.test.ts +0 -51
  72. package/tests/operators/autowrap.test.ts +0 -82
  73. package/tests/operators/constants.test.ts +0 -37
  74. package/tests/operators/cross_product.test.ts +0 -90
  75. package/tests/operators/exclude.test.ts +0 -43
  76. package/tests/operators/extend.test.ts +0 -45
  77. package/tests/operators/group.test.ts +0 -69
  78. package/tests/operators/image.test.ts +0 -152
  79. package/tests/operators/intersect.test.ts +0 -53
  80. package/tests/operators/isEqual.test.ts +0 -111
  81. package/tests/operators/join.test.ts +0 -116
  82. package/tests/operators/left_join.test.ts +0 -116
  83. package/tests/operators/matching.test.ts +0 -91
  84. package/tests/operators/minus.test.ts +0 -47
  85. package/tests/operators/not_matching.test.ts +0 -104
  86. package/tests/operators/one.test.ts +0 -19
  87. package/tests/operators/prefix.test.ts +0 -37
  88. package/tests/operators/project.test.ts +0 -48
  89. package/tests/operators/rename.test.ts +0 -39
  90. package/tests/operators/restrict.test.ts +0 -27
  91. package/tests/operators/suffix.test.ts +0 -37
  92. package/tests/operators/summarize.test.ts +0 -109
  93. package/tests/operators/transform.test.ts +0 -94
  94. package/tests/operators/ungroup.test.ts +0 -67
  95. package/tests/operators/union.test.ts +0 -51
  96. package/tests/operators/unwrap.test.ts +0 -50
  97. package/tests/operators/where.test.ts +0 -33
  98. package/tests/operators/wrap.test.ts +0 -54
  99. package/tests/operators/yByX.test.ts +0 -32
  100. package/tests/types/relation.test.ts +0 -296
  101. package/tsconfig.json +0 -37
  102. package/tsconfig.node.json +0 -9
  103. package/vitest.config.ts +0 -15
@@ -1,94 +0,0 @@
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
- });
@@ -1,67 +0,0 @@
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
- });
@@ -1,51 +0,0 @@
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
- });
@@ -1,50 +0,0 @@
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
- });
@@ -1,33 +0,0 @@
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
- });
@@ -1,54 +0,0 @@
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
- });
@@ -1,32 +0,0 @@
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
- });
@@ -1,296 +0,0 @@
1
- /**
2
- * Type-level tests for the generic Relation<T> system.
3
- * These tests verify compile-time type safety.
4
- */
5
- import { describe, it, expectTypeOf } from 'vitest';
6
- import { Bmg } from 'src';
7
- import type { Relation, Tuple } from 'src/types';
8
-
9
- // Define a typed interface for testing
10
- interface Person {
11
- id: number;
12
- name: string;
13
- age: number;
14
- city: string;
15
- }
16
-
17
- interface Order {
18
- orderId: number;
19
- customerId: number;
20
- total: number;
21
- }
22
-
23
- describe('Type Safety', () => {
24
-
25
- describe('Bmg factory', () => {
26
- it('infers type from input array', () => {
27
- const r = Bmg([{ id: 1, name: 'Alice' }]);
28
- expectTypeOf(r.one()).toMatchTypeOf<{ id: number; name: string }>();
29
- });
30
-
31
- it('accepts explicit type parameter', () => {
32
- const r = Bmg<Person>([{ id: 1, name: 'Alice', age: 30, city: 'NYC' }]);
33
- expectTypeOf(r).toMatchTypeOf<Relation<Person>>();
34
- });
35
-
36
- it('returns untyped Relation when no type info', () => {
37
- const data: Tuple[] = [{ a: 1 }];
38
- const r = Bmg(data);
39
- expectTypeOf(r).toMatchTypeOf<Relation<Tuple>>();
40
- });
41
- });
42
-
43
- describe('one() and toArray()', () => {
44
- it('returns typed tuple from one()', () => {
45
- const r = Bmg<Person>([{ id: 1, name: 'Alice', age: 30, city: 'NYC' }]);
46
- const tuple = r.one();
47
- expectTypeOf(tuple.id).toBeNumber();
48
- expectTypeOf(tuple.name).toBeString();
49
- expectTypeOf(tuple.age).toBeNumber();
50
- });
51
-
52
- it('returns typed array from toArray()', () => {
53
- const r = Bmg<Person>([{ id: 1, name: 'Alice', age: 30, city: 'NYC' }]);
54
- const arr = r.toArray();
55
- expectTypeOf(arr).toMatchTypeOf<Person[]>();
56
- });
57
- });
58
-
59
- describe('project()', () => {
60
- it('narrows type to selected attributes', () => {
61
- const r = Bmg<Person>([{ id: 1, name: 'Alice', age: 30, city: 'NYC' }]);
62
- const projected = r.project(['id', 'name']);
63
- expectTypeOf(projected.one()).toMatchTypeOf<{ id: number; name: string }>();
64
- });
65
-
66
- it('accepts valid attribute names only', () => {
67
- const r = Bmg<Person>([{ id: 1, name: 'Alice', age: 30, city: 'NYC' }]);
68
- // This should compile - valid attributes
69
- r.project(['id', 'name']);
70
-
71
- // @ts-expect-error - 'invalid' is not a valid attribute
72
- r.project(['invalid']);
73
- });
74
- });
75
-
76
- describe('allbut()', () => {
77
- it('removes specified attributes from type', () => {
78
- const r = Bmg<Person>([{ id: 1, name: 'Alice', age: 30, city: 'NYC' }]);
79
- const result = r.allbut(['age', 'city']);
80
- expectTypeOf(result.one()).toMatchTypeOf<{ id: number; name: string }>();
81
- });
82
- });
83
-
84
- describe('restrict/where/exclude', () => {
85
- it('preserves type through restrict', () => {
86
- const r = Bmg<Person>([{ id: 1, name: 'Alice', age: 30, city: 'NYC' }]);
87
- const filtered = r.restrict({ city: 'NYC' });
88
- expectTypeOf(filtered.one()).toMatchTypeOf<Person>();
89
- });
90
-
91
- it('provides typed tuple in predicate function', () => {
92
- const r = Bmg<Person>([{ id: 1, name: 'Alice', age: 30, city: 'NYC' }]);
93
- r.restrict(t => {
94
- expectTypeOf(t.age).toBeNumber();
95
- return t.age > 25;
96
- });
97
- });
98
-
99
- it('where is alias for restrict', () => {
100
- const r = Bmg<Person>([{ id: 1, name: 'Alice', age: 30, city: 'NYC' }]);
101
- const filtered = r.where(t => t.age > 25);
102
- expectTypeOf(filtered).toMatchTypeOf<Relation<Person>>();
103
- });
104
- });
105
-
106
- describe('rename()', () => {
107
- it('transforms attribute names in type', () => {
108
- const r = Bmg<{ name: string; age: number }>([{ name: 'Alice', age: 30 }]);
109
- const renamed = r.rename({ name: 'fullName' });
110
- const tuple = renamed.one();
111
- // Check individual properties since mapped types can be tricky for toMatchTypeOf
112
- expectTypeOf(tuple.fullName).toBeString();
113
- expectTypeOf(tuple.age).toBeNumber();
114
- });
115
- });
116
-
117
- describe('extend()', () => {
118
- it('adds new attributes to type', () => {
119
- const r = Bmg<{ id: number; age: number }>([{ id: 1, age: 30 }]);
120
- const extended = r.extend({
121
- senior: (t) => t.age >= 65,
122
- label: (t) => `Person ${t.id}`
123
- });
124
- const tuple = extended.one();
125
- expectTypeOf(tuple.id).toBeNumber();
126
- expectTypeOf(tuple.age).toBeNumber();
127
- expectTypeOf(tuple.senior).toBeBoolean();
128
- expectTypeOf(tuple.label).toBeString();
129
- });
130
- });
131
-
132
- describe('constants()', () => {
133
- it('adds constant attributes to type', () => {
134
- const r = Bmg<{ id: number }>([{ id: 1 }]);
135
- const result = r.constants({ version: 1, active: true });
136
- const tuple = result.one();
137
- expectTypeOf(tuple.id).toBeNumber();
138
- expectTypeOf(tuple.version).toBeNumber();
139
- expectTypeOf(tuple.active).toBeBoolean();
140
- });
141
- });
142
-
143
- describe('join()', () => {
144
- it('combines types from both relations', () => {
145
- const people = Bmg<Person>([{ id: 1, name: 'Alice', age: 30, city: 'NYC' }]);
146
- // Use array to avoid variance issues with Relation<Order> vs Relation<Tuple>
147
- const orders: Order[] = [{ orderId: 100, customerId: 1, total: 50 }];
148
-
149
- const joined = people.join(orders, { id: 'customerId' });
150
- const tuple = joined.one();
151
-
152
- // Should have all Person attributes
153
- expectTypeOf(tuple.id).toBeNumber();
154
- expectTypeOf(tuple.name).toBeString();
155
- expectTypeOf(tuple.age).toBeNumber();
156
-
157
- // Should have Order attributes (minus the join key)
158
- expectTypeOf(tuple.orderId).toBeNumber();
159
- expectTypeOf(tuple.total).toBeNumber();
160
- });
161
- });
162
-
163
- describe('cross_product()', () => {
164
- it('combines all attributes from both relations', () => {
165
- const r1 = Bmg<{ a: number }>([{ a: 1 }]);
166
- const r2 = Bmg<{ b: string }>([{ b: 'x' }]);
167
-
168
- const product = r1.cross_product(r2);
169
- const tuple = product.one();
170
-
171
- expectTypeOf(tuple.a).toBeNumber();
172
- expectTypeOf(tuple.b).toBeString();
173
- });
174
- });
175
-
176
- describe('union/minus/intersect', () => {
177
- it('preserves type in set operations', () => {
178
- const r1 = Bmg<Person>([{ id: 1, name: 'Alice', age: 30, city: 'NYC' }]);
179
- const r2 = Bmg<Person>([{ id: 2, name: 'Bob', age: 25, city: 'LA' }]);
180
-
181
- expectTypeOf(r1.union(r2)).toMatchTypeOf<Relation<Person>>();
182
- expectTypeOf(r1.minus(r2)).toMatchTypeOf<Relation<Person>>();
183
- expectTypeOf(r1.intersect(r2)).toMatchTypeOf<Relation<Person>>();
184
- });
185
- });
186
-
187
- describe('matching/not_matching', () => {
188
- it('preserves left type in semi-joins', () => {
189
- const people = Bmg<Person>([{ id: 1, name: 'Alice', age: 30, city: 'NYC' }]);
190
- // Use array to avoid variance issues with Relation<Order> vs Relation<Tuple>
191
- const orders: Order[] = [{ orderId: 100, customerId: 1, total: 50 }];
192
-
193
- const matched = people.matching(orders, { id: 'customerId' });
194
- expectTypeOf(matched).toMatchTypeOf<Relation<Person>>();
195
-
196
- const notMatched = people.not_matching(orders, { id: 'customerId' });
197
- expectTypeOf(notMatched).toMatchTypeOf<Relation<Person>>();
198
- });
199
- });
200
-
201
- describe('group()', () => {
202
- it('creates nested relation type', () => {
203
- const r = Bmg<{ orderId: number; item: string; qty: number }>([
204
- { orderId: 1, item: 'A', qty: 10 }
205
- ]);
206
-
207
- const grouped = r.group(['item', 'qty'], 'items');
208
- const tuple = grouped.one();
209
-
210
- expectTypeOf(tuple.orderId).toBeNumber();
211
- // items should be a Relation of the grouped attributes
212
- expectTypeOf(tuple.items).toMatchTypeOf<Relation<{ item: string; qty: number }>>();
213
- });
214
- });
215
-
216
- describe('wrap()', () => {
217
- it('creates nested object type', () => {
218
- const r = Bmg<{ id: number; street: string; city: string }>([
219
- { id: 1, street: '123 Main', city: 'NYC' }
220
- ]);
221
-
222
- const wrapped = r.wrap(['street', 'city'], 'address');
223
- const tuple = wrapped.one();
224
-
225
- expectTypeOf(tuple.id).toBeNumber();
226
- expectTypeOf(tuple.address).toMatchTypeOf<{ street: string; city: string }>();
227
- });
228
- });
229
-
230
- describe('prefix/suffix', () => {
231
- it('transforms attribute names with prefix', () => {
232
- const r = Bmg<{ id: number; name: string }>([{ id: 1, name: 'Alice' }]);
233
- const prefixed = r.prefix('user_');
234
- const tuple = prefixed.one();
235
-
236
- expectTypeOf(tuple).toMatchTypeOf<{ user_id: number; user_name: string }>();
237
- });
238
-
239
- it('transforms attribute names with suffix', () => {
240
- const r = Bmg<{ id: number; name: string }>([{ id: 1, name: 'Alice' }]);
241
- const suffixed = r.suffix('_val');
242
- const tuple = suffixed.one();
243
-
244
- expectTypeOf(tuple).toMatchTypeOf<{ id_val: number; name_val: string }>();
245
- });
246
- });
247
-
248
- describe('yByX()', () => {
249
- it('returns correctly typed record', () => {
250
- const r = Bmg<{ id: number; name: string }>([
251
- { id: 1, name: 'Alice' },
252
- { id: 2, name: 'Bob' }
253
- ]);
254
-
255
- const byId = r.yByX('name', 'id');
256
- expectTypeOf(byId).toMatchTypeOf<Record<number, string>>();
257
- });
258
- });
259
-
260
- describe('transform()', () => {
261
- it('preserves type structure', () => {
262
- const r = Bmg<{ id: number; name: string }>([{ id: 1, name: 'Alice' }]);
263
- const transformed = r.transform({ name: v => (v as string).toUpperCase() });
264
- expectTypeOf(transformed).toMatchTypeOf<Relation<{ id: number; name: string }>>();
265
- });
266
- });
267
-
268
- describe('autowrap()', () => {
269
- it('returns untyped Relation (dynamic)', () => {
270
- const r = Bmg<{ id: number; user_name: string; user_email: string }>([
271
- { id: 1, user_name: 'Alice', user_email: 'alice@test.com' }
272
- ]);
273
-
274
- const autowrapped = r.autowrap();
275
- // autowrap loses type information (dynamic structure)
276
- expectTypeOf(autowrapped).toMatchTypeOf<Relation<Tuple>>();
277
- });
278
- });
279
-
280
- describe('method chaining', () => {
281
- it('preserves types through operator chain', () => {
282
- const r = Bmg<Person>([{ id: 1, name: 'Alice', age: 30, city: 'NYC' }]);
283
-
284
- const result = r
285
- .restrict(t => t.age > 25)
286
- .project(['id', 'name'])
287
- .rename({ name: 'fullName' });
288
-
289
- const tuple = result.one();
290
- // Check individual properties since mapped types can be tricky for toMatchTypeOf
291
- expectTypeOf(tuple.id).toBeNumber();
292
- expectTypeOf(tuple.fullName).toBeString();
293
- });
294
- });
295
-
296
- });