@enspirit/bmg-js 1.0.0 → 1.0.1

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 (201) hide show
  1. package/dist/bmg.cjs +2 -0
  2. package/dist/bmg.cjs.map +1 -0
  3. package/dist/bmg.modern.js +2 -0
  4. package/dist/bmg.modern.js.map +1 -0
  5. package/dist/bmg.module.js +2 -0
  6. package/dist/bmg.module.js.map +1 -0
  7. package/dist/bmg.umd.js +2 -0
  8. package/dist/bmg.umd.js.map +1 -0
  9. package/dist/index.d.ts +1 -0
  10. package/dist/src/Relation/Memory.d.ts +46 -0
  11. package/dist/src/Relation/index.d.ts +1 -0
  12. package/dist/src/Relation.d.ts +8 -0
  13. package/dist/src/index.d.ts +27 -0
  14. package/dist/src/operators/_helpers.d.ts +142 -0
  15. package/dist/src/operators/allbut.d.ts +2 -0
  16. package/dist/src/operators/autowrap.d.ts +2 -0
  17. package/dist/src/operators/constants.d.ts +2 -0
  18. package/dist/src/operators/cross_product.d.ts +3 -0
  19. package/dist/src/operators/exclude.d.ts +2 -0
  20. package/dist/src/operators/extend.d.ts +2 -0
  21. package/dist/src/operators/group.d.ts +2 -0
  22. package/dist/src/operators/image.d.ts +2 -0
  23. package/dist/src/operators/index.d.ts +30 -0
  24. package/dist/src/operators/intersect.d.ts +2 -0
  25. package/dist/src/operators/isEqual.d.ts +2 -0
  26. package/dist/src/operators/isRelation.d.ts +1 -0
  27. package/dist/src/operators/join.d.ts +2 -0
  28. package/dist/src/operators/left_join.d.ts +2 -0
  29. package/dist/src/operators/matching.d.ts +2 -0
  30. package/dist/src/operators/minus.d.ts +2 -0
  31. package/dist/src/operators/not_matching.d.ts +2 -0
  32. package/dist/src/operators/one.d.ts +2 -0
  33. package/dist/src/operators/prefix.d.ts +2 -0
  34. package/dist/src/operators/project.d.ts +2 -0
  35. package/dist/src/operators/rename.d.ts +2 -0
  36. package/dist/src/operators/restrict.d.ts +2 -0
  37. package/dist/src/operators/suffix.d.ts +2 -0
  38. package/dist/src/operators/summarize.d.ts +2 -0
  39. package/dist/src/operators/transform.d.ts +2 -0
  40. package/dist/src/operators/ungroup.d.ts +2 -0
  41. package/dist/src/operators/union.d.ts +2 -0
  42. package/dist/src/operators/unwrap.d.ts +2 -0
  43. package/dist/src/operators/where.d.ts +1 -0
  44. package/dist/src/operators/wrap.d.ts +2 -0
  45. package/dist/src/operators/yByX.d.ts +2 -0
  46. package/dist/src/src/Relation/Memory.d.ts +46 -0
  47. package/dist/src/src/Relation/index.d.ts +1 -0
  48. package/dist/src/src/index.d.ts +27 -0
  49. package/dist/src/src/operators/_helpers.d.ts +142 -0
  50. package/dist/src/src/operators/allbut.d.ts +2 -0
  51. package/dist/src/src/operators/autowrap.d.ts +2 -0
  52. package/dist/src/src/operators/constants.d.ts +2 -0
  53. package/dist/src/src/operators/cross_product.d.ts +3 -0
  54. package/dist/src/src/operators/exclude.d.ts +2 -0
  55. package/dist/src/src/operators/extend.d.ts +2 -0
  56. package/dist/src/src/operators/group.d.ts +2 -0
  57. package/dist/src/src/operators/image.d.ts +2 -0
  58. package/dist/src/src/operators/index.d.ts +30 -0
  59. package/dist/src/src/operators/intersect.d.ts +2 -0
  60. package/dist/src/src/operators/isEqual.d.ts +2 -0
  61. package/dist/src/src/operators/isRelation.d.ts +1 -0
  62. package/dist/src/src/operators/join.d.ts +2 -0
  63. package/dist/src/src/operators/left_join.d.ts +2 -0
  64. package/dist/src/src/operators/matching.d.ts +2 -0
  65. package/dist/src/src/operators/minus.d.ts +2 -0
  66. package/dist/src/src/operators/not_matching.d.ts +2 -0
  67. package/dist/src/src/operators/one.d.ts +2 -0
  68. package/dist/src/src/operators/prefix.d.ts +2 -0
  69. package/dist/src/src/operators/project.d.ts +2 -0
  70. package/dist/src/src/operators/rename.d.ts +2 -0
  71. package/dist/src/src/operators/restrict.d.ts +2 -0
  72. package/dist/src/src/operators/suffix.d.ts +2 -0
  73. package/dist/src/src/operators/summarize.d.ts +2 -0
  74. package/dist/src/src/operators/transform.d.ts +2 -0
  75. package/dist/src/src/operators/ungroup.d.ts +2 -0
  76. package/dist/src/src/operators/union.d.ts +2 -0
  77. package/dist/src/src/operators/unwrap.d.ts +2 -0
  78. package/dist/src/src/operators/where.d.ts +1 -0
  79. package/dist/src/src/operators/wrap.d.ts +2 -0
  80. package/dist/src/src/operators/yByX.d.ts +2 -0
  81. package/dist/src/src/support/toPredicateFunc.d.ts +2 -0
  82. package/dist/src/src/types.d.ts +101 -0
  83. package/dist/src/src/utility-types.d.ts +43 -0
  84. package/dist/src/support/toPredicateFunc.d.ts +2 -0
  85. package/dist/src/tests/bmg.test.d.ts +1 -0
  86. package/dist/src/tests/fixtures.d.ts +6 -0
  87. package/dist/src/tests/operators/allbut.test.d.ts +1 -0
  88. package/dist/src/tests/operators/autowrap.test.d.ts +1 -0
  89. package/dist/src/tests/operators/constants.test.d.ts +1 -0
  90. package/dist/src/tests/operators/cross_product.test.d.ts +1 -0
  91. package/dist/src/tests/operators/exclude.test.d.ts +1 -0
  92. package/dist/src/tests/operators/extend.test.d.ts +1 -0
  93. package/dist/src/tests/operators/group.test.d.ts +1 -0
  94. package/dist/src/tests/operators/image.test.d.ts +1 -0
  95. package/dist/src/tests/operators/intersect.test.d.ts +1 -0
  96. package/dist/src/tests/operators/isEqual.test.d.ts +1 -0
  97. package/dist/src/tests/operators/join.test.d.ts +1 -0
  98. package/dist/src/tests/operators/left_join.test.d.ts +1 -0
  99. package/dist/src/tests/operators/matching.test.d.ts +1 -0
  100. package/dist/src/tests/operators/minus.test.d.ts +1 -0
  101. package/dist/src/tests/operators/not_matching.test.d.ts +1 -0
  102. package/dist/src/tests/operators/one.test.d.ts +1 -0
  103. package/dist/src/tests/operators/prefix.test.d.ts +1 -0
  104. package/dist/src/tests/operators/project.test.d.ts +1 -0
  105. package/dist/src/tests/operators/rename.test.d.ts +1 -0
  106. package/dist/src/tests/operators/restrict.test.d.ts +1 -0
  107. package/dist/src/tests/operators/suffix.test.d.ts +1 -0
  108. package/dist/src/tests/operators/summarize.test.d.ts +1 -0
  109. package/dist/src/tests/operators/transform.test.d.ts +1 -0
  110. package/dist/src/tests/operators/ungroup.test.d.ts +1 -0
  111. package/dist/src/tests/operators/union.test.d.ts +1 -0
  112. package/dist/src/tests/operators/unwrap.test.d.ts +1 -0
  113. package/dist/src/tests/operators/where.test.d.ts +1 -0
  114. package/dist/src/tests/operators/wrap.test.d.ts +1 -0
  115. package/dist/src/tests/operators/yByX.test.d.ts +1 -0
  116. package/dist/src/tests/types/relation.test.d.ts +1 -0
  117. package/dist/src/types.d.ts +101 -0
  118. package/dist/src/utility-types.d.ts +43 -0
  119. package/dist/tests/bmg.test.d.ts +1 -0
  120. package/dist/tests/fixtures.d.ts +6 -0
  121. package/dist/tests/operators/allbut.test.d.ts +1 -0
  122. package/dist/tests/operators/autowrap.test.d.ts +1 -0
  123. package/dist/tests/operators/constants.test.d.ts +1 -0
  124. package/dist/tests/operators/cross_product.test.d.ts +1 -0
  125. package/dist/tests/operators/exclude.test.d.ts +1 -0
  126. package/dist/tests/operators/extend.test.d.ts +1 -0
  127. package/dist/tests/operators/group.test.d.ts +1 -0
  128. package/dist/tests/operators/image.test.d.ts +1 -0
  129. package/dist/tests/operators/intersect.test.d.ts +1 -0
  130. package/dist/tests/operators/isEqual.test.d.ts +1 -0
  131. package/dist/tests/operators/join.test.d.ts +1 -0
  132. package/dist/tests/operators/left_join.test.d.ts +1 -0
  133. package/dist/tests/operators/matching.test.d.ts +1 -0
  134. package/dist/tests/operators/minus.test.d.ts +1 -0
  135. package/dist/tests/operators/not_matching.test.d.ts +1 -0
  136. package/dist/tests/operators/one.test.d.ts +1 -0
  137. package/dist/tests/operators/prefix.test.d.ts +1 -0
  138. package/dist/tests/operators/project.test.d.ts +1 -0
  139. package/dist/tests/operators/rename.test.d.ts +1 -0
  140. package/dist/tests/operators/restrict.test.d.ts +1 -0
  141. package/dist/tests/operators/suffix.test.d.ts +1 -0
  142. package/dist/tests/operators/summarize.test.d.ts +1 -0
  143. package/dist/tests/operators/transform.test.d.ts +1 -0
  144. package/dist/tests/operators/ungroup.test.d.ts +1 -0
  145. package/dist/tests/operators/union.test.d.ts +1 -0
  146. package/dist/tests/operators/unwrap.test.d.ts +1 -0
  147. package/dist/tests/operators/where.test.d.ts +1 -0
  148. package/dist/tests/operators/wrap.test.d.ts +1 -0
  149. package/dist/tests/operators/yByX.test.d.ts +1 -0
  150. package/dist/tests/types/relation.test.d.ts +1 -0
  151. package/package.json +15 -3
  152. package/.claude/safe-setup/.env.example +0 -3
  153. package/.claude/safe-setup/Dockerfile.claude +0 -36
  154. package/.claude/safe-setup/HACKING.md +0 -63
  155. package/.claude/safe-setup/Makefile +0 -22
  156. package/.claude/safe-setup/docker-compose.yml +0 -18
  157. package/.claude/safe-setup/entrypoint.sh +0 -13
  158. package/.claude/settings.local.json +0 -9
  159. package/.claude/typescript-annotations.md +0 -273
  160. package/.github/workflows/test.yml +0 -26
  161. package/CLAUDE.md +0 -48
  162. package/Makefile +0 -2
  163. package/example/README.md +0 -22
  164. package/example/index.ts +0 -316
  165. package/example/package.json +0 -16
  166. package/example/tsconfig.json +0 -11
  167. package/tests/bmg.test.ts +0 -16
  168. package/tests/fixtures.ts +0 -9
  169. package/tests/operators/allbut.test.ts +0 -51
  170. package/tests/operators/autowrap.test.ts +0 -82
  171. package/tests/operators/constants.test.ts +0 -37
  172. package/tests/operators/cross_product.test.ts +0 -90
  173. package/tests/operators/exclude.test.ts +0 -43
  174. package/tests/operators/extend.test.ts +0 -45
  175. package/tests/operators/group.test.ts +0 -69
  176. package/tests/operators/image.test.ts +0 -152
  177. package/tests/operators/intersect.test.ts +0 -53
  178. package/tests/operators/isEqual.test.ts +0 -111
  179. package/tests/operators/join.test.ts +0 -116
  180. package/tests/operators/left_join.test.ts +0 -116
  181. package/tests/operators/matching.test.ts +0 -91
  182. package/tests/operators/minus.test.ts +0 -47
  183. package/tests/operators/not_matching.test.ts +0 -104
  184. package/tests/operators/one.test.ts +0 -19
  185. package/tests/operators/prefix.test.ts +0 -37
  186. package/tests/operators/project.test.ts +0 -48
  187. package/tests/operators/rename.test.ts +0 -39
  188. package/tests/operators/restrict.test.ts +0 -27
  189. package/tests/operators/suffix.test.ts +0 -37
  190. package/tests/operators/summarize.test.ts +0 -109
  191. package/tests/operators/transform.test.ts +0 -94
  192. package/tests/operators/ungroup.test.ts +0 -67
  193. package/tests/operators/union.test.ts +0 -51
  194. package/tests/operators/unwrap.test.ts +0 -50
  195. package/tests/operators/where.test.ts +0 -33
  196. package/tests/operators/wrap.test.ts +0 -54
  197. package/tests/operators/yByX.test.ts +0 -32
  198. package/tests/types/relation.test.ts +0 -296
  199. package/tsconfig.json +0 -37
  200. package/tsconfig.node.json +0 -9
  201. package/vitest.config.ts +0 -15
@@ -1,273 +0,0 @@
1
- # TypeScript Type Safety Implementation Plan
2
-
3
- ## Goal
4
- Add opt-in generic type parameters to enable:
5
- - IDE autocomplete on tuple attributes
6
- - Compile-time validation of attribute names
7
- - Type transformation tracking through operator chains
8
-
9
- ## Core Changes
10
-
11
- ### 1. Generic Relation Interface (`src/types.ts`)
12
-
13
- ```typescript
14
- export interface Relation<T extends Tuple = Tuple> {
15
- one(): T;
16
- toArray(): T[];
17
-
18
- // Preserve type
19
- restrict(p: TypedPredicate<T>): Relation<T>;
20
- where(p: TypedPredicate<T>): Relation<T>;
21
-
22
- // Transform type
23
- project<K extends keyof T>(attrs: K[]): Relation<Pick<T, K>>;
24
- allbut<K extends keyof T>(attrs: K[]): Relation<Omit<T, K>>;
25
- rename<R extends RenameMap<T>>(r: R): Relation<Renamed<T, R>>;
26
- extend<E extends Record<string, unknown>>(e: TypedExtension<T, E>): Relation<T & E>;
27
- constants<C extends Tuple>(c: C): Relation<T & C>;
28
-
29
- // Binary ops - combine types
30
- join<R extends Tuple>(right: RelationOperand<R>, keys?: JoinKeys<T, R>): Relation<Joined<T, R>>;
31
- left_join<R extends Tuple>(right: RelationOperand<R>, keys?: JoinKeys<T, R>): Relation<LeftJoined<T, R>>;
32
- cross_product<R extends Tuple>(right: RelationOperand<R>): Relation<T & R>;
33
-
34
- // Nesting
35
- group<K extends keyof T, As extends string>(attrs: K[], as: As): Relation<Grouped<T, K, As>>;
36
- ungroup<K extends keyof T>(attr: K): Relation<Ungrouped<T, K>>;
37
- // ...
38
- }
39
- ```
40
-
41
- ### 2. Generic MemoryRelation Class (`src/Relation/Memory.ts`)
42
-
43
- ```typescript
44
- export class MemoryRelation<T extends Tuple = Tuple> implements Relation<T> {
45
- constructor(private tuples: T[]) {}
46
-
47
- project<K extends keyof T>(attrs: K[]): MemoryRelation<Pick<T, K>> {
48
- return project(this, attrs as string[]) as MemoryRelation<Pick<T, K>>;
49
- }
50
- // ...
51
- }
52
- ```
53
-
54
- ### 3. Generic Bmg Factory (`src/index.ts`)
55
-
56
- ```typescript
57
- export function Bmg<T extends Tuple>(tuples: T[]): MemoryRelation<T>;
58
- export function Bmg(tuples: Tuple[]): MemoryRelation<Tuple>;
59
- export function Bmg<T extends Tuple = Tuple>(tuples: T[]): MemoryRelation<T> {
60
- return new MemoryRelation<T>(tuples);
61
- }
62
- ```
63
-
64
- ### 4. Utility Types (`src/utility-types.ts` - new file)
65
-
66
- ```typescript
67
- // Rename: { name: 'fullName' } transforms { name, age } to { fullName, age }
68
- export type RenameMap<T> = { [K in keyof T]?: string };
69
- export type Renamed<T, R extends RenameMap<T>> = {
70
- [K in keyof T as K extends keyof R ? (R[K] extends string ? R[K] : K) : K]: T[K];
71
- };
72
-
73
- // Join: combine left & right, remove duplicate keys from right
74
- export type CommonKeys<L, R> = Extract<keyof L, keyof R>;
75
- export type Joined<L, R> = L & Omit<R, CommonKeys<L, R>>;
76
- export type LeftJoined<L, R> = L & Partial<Omit<R, CommonKeys<L, R>>>;
77
-
78
- // Group/Ungroup
79
- export type Grouped<T, K extends keyof T, As extends string> =
80
- Omit<T, K> & Record<As, Relation<Pick<T, K>>>;
81
- export type Ungrouped<T, K extends keyof T> =
82
- T[K] extends Relation<infer N> ? Omit<T, K> & N : never;
83
-
84
- // Wrap/Unwrap
85
- export type Wrapped<T, K extends keyof T, As extends string> =
86
- Omit<T, K> & Record<As, Pick<T, K>>;
87
- export type Unwrapped<T, K extends keyof T> =
88
- T[K] extends Record<string, unknown> ? Omit<T, K> & T[K] : never;
89
-
90
- // Prefix/Suffix (template literal types)
91
- export type Prefixed<T, P extends string, Ex extends keyof T = never> = {
92
- [K in keyof T as K extends Ex ? K : `${P}${K & string}`]: T[K];
93
- };
94
- ```
95
-
96
- ### 5. Typed Predicates (`src/types.ts`)
97
-
98
- ```typescript
99
- export type TypedPredicateFunc<T> = (t: T) => boolean;
100
- export type TypedPredicate<T> = TypedPredicateFunc<T> | Partial<T>;
101
- ```
102
-
103
- ## Implementation Phases
104
-
105
- ### Phase 1: Foundation
106
- 1. Add `Relation<T = Tuple>` interface with generics
107
- 2. Add `MemoryRelation<T = Tuple>` class
108
- 3. Update `Bmg<T>()` factory
109
- 4. Type these operators first (highest value):
110
- - `one()`, `toArray()` - return `T` / `T[]`
111
- - `project()` - `Pick<T, K>`
112
- - `allbut()` - `Omit<T, K>`
113
- - `restrict()`, `where()`, `exclude()` - preserve `T` with typed predicate
114
-
115
- ### Phase 2: Structure-Changing Operators
116
- - `extend()` - `T & E`
117
- - `constants()` - `T & C`
118
- - `rename()` - `Renamed<T, R>`
119
- - `prefix()`, `suffix()` - template literal types
120
-
121
- ### Phase 3: Binary Operations
122
- - `union()`, `minus()`, `intersect()` - require same `T`
123
- - `join()` - `Joined<T, R>`
124
- - `left_join()` - `LeftJoined<T, R>`
125
- - `cross_product()` - `T & R`
126
- - `matching()`, `not_matching()` - preserve `T`
127
-
128
- ### Phase 4: Nesting Operators
129
- - `group()` - `Grouped<T, K, As>`
130
- - `ungroup()` - `Ungrouped<T, K>`
131
- - `wrap()` - `Wrapped<T, K, As>`
132
- - `unwrap()` - `Unwrapped<T, K>`
133
- - `image()` - adds `Relation<...>` attribute
134
-
135
- ### Phase 5: Complex Operators
136
- - `summarize()` - infer result from aggregators
137
- - `transform()` - preserve structure, optional value type tracking
138
- - `autowrap()` - return `Relation<Tuple>` (dynamic, un-typeable)
139
-
140
- ## Backwards Compatibility
141
-
142
- - Default type parameter `= Tuple` ensures existing untyped code compiles
143
- - When `T = Tuple = Record<string, unknown>`, `keyof T = string`, so any string[] is accepted
144
- - No breaking changes to existing API
145
-
146
- ## Files to Modify
147
-
148
- | File | Changes |
149
- |------|---------|
150
- | `src/types.ts` | Add generics to `Relation`, typed predicates |
151
- | `src/utility-types.ts` (new) | Utility types: `Renamed`, `Joined`, `Grouped`, etc. |
152
- | `src/Relation/Memory.ts` | Add `<T>` parameter, update method signatures |
153
- | `src/index.ts` | Update `Bmg` factory with type inference |
154
- | `src/operators/_helpers.ts` | Generic `toOperationalOperand<T>` |
155
-
156
- ## Example Usage After Implementation
157
-
158
- ```typescript
159
- // Opt-in: provide type for full type safety
160
- interface Supplier {
161
- sid: string;
162
- name: string;
163
- status: number;
164
- city: string;
165
- }
166
-
167
- const suppliers = Bmg<Supplier>([
168
- { sid: 'S1', name: 'Smith', status: 20, city: 'London' },
169
- ]);
170
-
171
- // IDE autocomplete: suggests 'sid', 'name', 'status', 'city'
172
- const result = suppliers
173
- .project(['sid', 'name']) // Relation<{ sid: string; name: string }>
174
- .rename({ name: 'fullName' }) // Relation<{ sid: string; fullName: string }>
175
- .extend({ upper: t => t.fullName.toUpperCase() }); // t.fullName autocompletes
176
-
177
- // Compile error for invalid attributes
178
- suppliers.project(['invalid']); // Error: 'invalid' not in keyof Supplier
179
-
180
- // Untyped usage still works (backwards compatible)
181
- const r = Bmg([{ a: 1 }]); // Relation<Tuple>
182
- r.project(['a']); // Works, no validation
183
- ```
184
-
185
- ## Test Plan
186
-
187
- ### 1. Compile-Time Type Tests
188
-
189
- Create `tests/types/relation.test-d.ts` using vitest's type testing:
190
-
191
- ```typescript
192
- import { Bmg } from 'src';
193
- import { expectTypeOf } from 'vitest';
194
-
195
- interface Person { id: number; name: string; age: number }
196
-
197
- describe('Type Safety', () => {
198
- const people = Bmg<Person>([{ id: 1, name: 'Alice', age: 30 }]);
199
-
200
- it('project narrows type to Pick<T, K>', () => {
201
- const result = people.project(['id', 'name']);
202
- expectTypeOf(result.one()).toEqualTypeOf<{ id: number; name: string }>();
203
- });
204
-
205
- it('rejects invalid attribute names', () => {
206
- // @ts-expect-error - 'invalid' is not a key of Person
207
- people.project(['invalid']);
208
- });
209
-
210
- it('restrict preserves type with typed predicate', () => {
211
- const result = people.restrict(t => t.age > 25);
212
- expectTypeOf(result.one()).toEqualTypeOf<Person>();
213
- });
214
-
215
- it('predicate function receives typed tuple', () => {
216
- people.restrict(t => {
217
- expectTypeOf(t.id).toBeNumber();
218
- expectTypeOf(t.name).toBeString();
219
- return true;
220
- });
221
- });
222
-
223
- it('join combines types correctly', () => {
224
- interface Order { orderId: number; personId: number; total: number }
225
- const orders = Bmg<Order>([]);
226
- const joined = people.join(orders, { id: 'personId' });
227
- expectTypeOf(joined.one()).toEqualTypeOf<{
228
- id: number; name: string; age: number; orderId: number; total: number
229
- }>();
230
- });
231
-
232
- it('rename transforms keys', () => {
233
- const renamed = people.rename({ name: 'fullName' });
234
- expectTypeOf(renamed.one()).toEqualTypeOf<{ id: number; fullName: string; age: number }>();
235
- });
236
-
237
- it('untyped usage still works', () => {
238
- const r = Bmg([{ a: 1 }]); // Relation<Tuple>
239
- r.project(['a']); // Should compile
240
- r.project(['anything']); // Should compile (no validation when T=Tuple)
241
- });
242
- });
243
- ```
244
-
245
- ### 2. Runtime Tests
246
-
247
- All 152 existing tests must continue to pass without modification - validates backwards compatibility.
248
-
249
- ### 3. Type Test Coverage by Phase
250
-
251
- | Phase | Type Tests |
252
- |-------|-----------|
253
- | Phase 1 | `project`, `allbut`, `restrict`, `one`, `toArray` |
254
- | Phase 2 | `extend`, `constants`, `rename`, `prefix`, `suffix` |
255
- | Phase 3 | `join`, `left_join`, `cross_product`, `union`, `minus` |
256
- | Phase 4 | `group`, `ungroup`, `wrap`, `unwrap`, `image` |
257
- | Phase 5 | `summarize`, `transform`, `autowrap` (escape hatch) |
258
-
259
- ### 4. Test File Structure
260
-
261
- ```
262
- tests/
263
- types/
264
- relation.test-d.ts # Core Relation<T> type tests
265
- operators.test-d.ts # Operator type transformation tests
266
- utility-types.test-d.ts # Utility type unit tests
267
- ```
268
-
269
- ## Limitations
270
-
271
- - Function-based `rename()` loses type tracking (only object syntax typed)
272
- - `autowrap()` returns `Relation<Tuple>` (dynamic structure)
273
- - Requires TypeScript 4.1+ for template literal types
@@ -1,26 +0,0 @@
1
- name: Tests
2
-
3
- on:
4
- push:
5
- branches: ['*']
6
- pull_request:
7
- branches: ['*']
8
-
9
- jobs:
10
- test:
11
- runs-on: ubuntu-latest
12
-
13
- steps:
14
- - uses: actions/checkout@v4
15
-
16
- - uses: pnpm/action-setup@v4
17
-
18
- - uses: actions/setup-node@v4
19
- with:
20
- node-version: '20'
21
-
22
- - name: Install dependencies
23
- run: pnpm install
24
-
25
- - name: Run tests
26
- run: pnpm test
package/CLAUDE.md DELETED
@@ -1,48 +0,0 @@
1
- # What is Bmg.js ?
2
-
3
- A Typescript implementation of BMG, a relational algebra originally implemented
4
- in/for Ruby :
5
-
6
- - https://github.com/enspirit/bmg
7
- - https://www.relational-algebra.dev/
8
-
9
- The aim of bmg.js is NOT to implement the SQL compiler, but only to provide an
10
- implementation of main algebra operators to work on arrays of javascript objects.
11
-
12
- ## Development flow
13
-
14
- * List operators to support, by order of importance
15
- * Add each operator, in order, with unit tests.
16
- * One commit per operator, don't forget to adapt the README.
17
- * Tests MUST succeed at all times, run them with `npm run test`
18
-
19
- ## Theory
20
-
21
- IMPORTANT rules:
22
-
23
- 1. Relations NEVER have duplicates
24
- 2. Order of tuples and order or attributes in a tuple are not important semantically
25
- 3. Mathematically, relations are sets of tuples ; tuples are sets of (attr, value) pairs.
26
- 4. Two relations are equal of they have the exact same set of exact same tuples.
27
-
28
- ## About unit tests
29
-
30
- IMPORTANT rules:
31
-
32
- * Favor purely relational tests: compare an obtained relation with the expected relation
33
- using `isEqual`.
34
- * Do NEVER access the "first" tuple, since there is no such tuple.
35
- * Instead, use `r.restrict(...predicate...).one` with a predicate that selects the
36
- tuple you are interested in. `one` will correctly fail if your assumption is wrong.
37
-
38
- ## Implemented operators
39
-
40
- **Relational:** restrict, where, exclude, project, allbut, extend, rename, prefix, suffix, constants, union, minus, intersect, matching, not_matching, join, left_join, cross_product, cross_join, image, summarize, group, ungroup, wrap, unwrap, autowrap, transform
41
-
42
- **Non-relational:** one, yByX, toArray, isRelation, isEqual
43
-
44
- ## TODO
45
-
46
- - [ ] sort - Order tuples by attributes
47
- - [ ] page - Pagination (offset + limit)
48
- - [ ] size, empty, first, exists - Utility helpers
package/Makefile DELETED
@@ -1,2 +0,0 @@
1
- dist/bmg.cjs: src/**/*
2
- pnpm run build
package/example/README.md DELETED
@@ -1,22 +0,0 @@
1
- # Bmg.js Example
2
-
3
- Demonstrates relational algebra operations using the classic suppliers/parts/shipments dataset with full TypeScript type safety.
4
-
5
- ## Running the example
6
-
7
- ```bash
8
- npm install
9
- npm start
10
- ```
11
-
12
- ## What's covered
13
-
14
- - Basic operations: `restrict`, `project`, `rename`
15
- - Extending relations: `extend`, `constants`
16
- - Set operations: `union`, `minus`, `intersect`
17
- - Join operations: `join`, `left_join`, `matching`, `not_matching`
18
- - Aggregation: `summarize` with `count`, `sum`, `avg`, `min`, `max`
19
- - Grouping and nesting: `group`, `image`, `wrap`
20
- - Data transformation: `transform`, `allbut`
21
- - Extracting tuples: `one()`
22
- - Relation properties: `isEqual`, `Bmg.isRelation`
package/example/index.ts DELETED
@@ -1,316 +0,0 @@
1
- /**
2
- * Bmg.js Example - Relational Algebra Operations (TypeScript)
3
- *
4
- * This example demonstrates chaining various relational operators
5
- * using the classic suppliers/parts/shipments dataset with full type safety.
6
- */
7
-
8
- import { Bmg, Relation } from '@enspirit/bmg-js';
9
-
10
- // =============================================================================
11
- // Type Definitions
12
- // =============================================================================
13
-
14
- interface Supplier {
15
- sid: string;
16
- name: string;
17
- status: number;
18
- city: string;
19
- }
20
-
21
- interface Part {
22
- pid: string;
23
- pname: string;
24
- color: string;
25
- weight: number;
26
- city: string;
27
- }
28
-
29
- interface Shipment {
30
- sid: string;
31
- pid: string;
32
- qty: number;
33
- }
34
-
35
- // =============================================================================
36
- // Sample Data: Suppliers, Parts, and Shipments
37
- // =============================================================================
38
-
39
- const suppliers = Bmg<Supplier>([
40
- { sid: 'S1', name: 'Smith', status: 20, city: 'London' },
41
- { sid: 'S2', name: 'Jones', status: 10, city: 'Paris' },
42
- { sid: 'S3', name: 'Blake', status: 30, city: 'Paris' },
43
- { sid: 'S4', name: 'Clark', status: 20, city: 'London' },
44
- { sid: 'S5', name: 'Adams', status: 30, city: 'Athens' },
45
- ]);
46
-
47
- const parts = Bmg<Part>([
48
- { pid: 'P1', pname: 'Nut', color: 'Red', weight: 12, city: 'London' },
49
- { pid: 'P2', pname: 'Bolt', color: 'Green', weight: 17, city: 'Paris' },
50
- { pid: 'P3', pname: 'Screw', color: 'Blue', weight: 17, city: 'Oslo' },
51
- { pid: 'P4', pname: 'Screw', color: 'Red', weight: 14, city: 'London' },
52
- { pid: 'P5', pname: 'Cam', color: 'Blue', weight: 12, city: 'Paris' },
53
- { pid: 'P6', pname: 'Cog', color: 'Red', weight: 19, city: 'London' },
54
- ]);
55
-
56
- const shipments = Bmg<Shipment>([
57
- { sid: 'S1', pid: 'P1', qty: 300 },
58
- { sid: 'S1', pid: 'P2', qty: 200 },
59
- { sid: 'S1', pid: 'P3', qty: 400 },
60
- { sid: 'S1', pid: 'P4', qty: 200 },
61
- { sid: 'S1', pid: 'P5', qty: 100 },
62
- { sid: 'S1', pid: 'P6', qty: 100 },
63
- { sid: 'S2', pid: 'P1', qty: 300 },
64
- { sid: 'S2', pid: 'P2', qty: 400 },
65
- { sid: 'S3', pid: 'P2', qty: 200 },
66
- { sid: 'S4', pid: 'P2', qty: 200 },
67
- { sid: 'S4', pid: 'P4', qty: 300 },
68
- { sid: 'S4', pid: 'P5', qty: 400 },
69
- ]);
70
-
71
- // Helper to print results
72
- const printRelation = <T>(title: string, relation: Relation<T>): void => {
73
- console.log(`\n${'='.repeat(60)}`);
74
- console.log(title);
75
- console.log('='.repeat(60));
76
- console.log(relation.toArray());
77
- };
78
-
79
- // =============================================================================
80
- // Example 1: Basic Operations - restrict, project, rename
81
- // =============================================================================
82
-
83
- console.log('\n*** EXAMPLE 1: Basic Operations ***');
84
-
85
- // Find suppliers in Paris, showing only their id and name
86
- // Type: Relation<{ sid: string; name: string }>
87
- const parisSuppliers = suppliers
88
- .restrict({ city: 'Paris' })
89
- .project(['sid', 'name']);
90
-
91
- printRelation('Suppliers in Paris (id and name only)', parisSuppliers);
92
-
93
- // Rename attributes for clarity
94
- // Type: Relation<{ supplierId: string; supplierName: string; city: string }>
95
- const renamedSuppliers = suppliers
96
- .project(['sid', 'name', 'city'])
97
- .rename({ sid: 'supplierId', name: 'supplierName' });
98
-
99
- printRelation('Suppliers with renamed attributes', renamedSuppliers);
100
-
101
- // =============================================================================
102
- // Example 2: Extending Relations - extend, constants, prefix
103
- // =============================================================================
104
-
105
- console.log('\n*** EXAMPLE 2: Extending Relations ***');
106
-
107
- // Add computed attributes
108
- // Type: Relation<Part & { weightInKg: number; description: string }>
109
- const partsWithMetrics = parts
110
- .extend({
111
- weightInKg: (t: Part) => t.weight / 1000,
112
- description: (t: Part) => `${t.color} ${t.pname}`,
113
- })
114
- .project(['pid', 'description', 'weight', 'weightInKg']);
115
-
116
- printRelation('Parts with computed metrics', partsWithMetrics);
117
-
118
- // Add constant values
119
- const taggedParts = parts
120
- .restrict({ color: 'Red' })
121
- .constants({ category: 'Primary', inStock: true })
122
- .project(['pid', 'pname', 'category', 'inStock']);
123
-
124
- printRelation('Red parts with constant tags', taggedParts);
125
-
126
- // =============================================================================
127
- // Example 3: Set Operations - union, minus, intersect
128
- // =============================================================================
129
-
130
- console.log('\n*** EXAMPLE 3: Set Operations ***');
131
-
132
- const partCities = parts.project(['city']);
133
-
134
- // Cities where we have either suppliers or parts
135
- const allCities = suppliers.project(['city']).union(partCities);
136
- printRelation('All cities (suppliers OR parts)', allCities);
137
-
138
- // Cities where we have both suppliers and parts
139
- const commonCities = suppliers.project(['city']).intersect(partCities);
140
- printRelation('Common cities (suppliers AND parts)', commonCities);
141
-
142
- // Cities with parts but no suppliers
143
- const partOnlyCities = partCities.minus(suppliers.project(['city']));
144
- printRelation('Cities with parts but no suppliers', partOnlyCities);
145
-
146
- // =============================================================================
147
- // Example 4: Join Operations - join, left_join, matching
148
- // =============================================================================
149
-
150
- console.log('\n*** EXAMPLE 4: Join Operations ***');
151
-
152
- // Natural join: suppliers and parts in the same city
153
- const supplierPartsInSameCity = suppliers
154
- .project(['sid', 'name', 'city'])
155
- .join(parts.project(['pid', 'pname', 'city']));
156
-
157
- printRelation('Suppliers and parts in the same city', supplierPartsInSameCity);
158
-
159
- // Full shipment details with supplier and part info
160
- const fullShipments = shipments
161
- .join(suppliers.project(['sid', 'name']), ['sid'])
162
- .join(parts.project(['pid', 'pname', 'color']), ['pid'])
163
- .project(['name', 'pname', 'color', 'qty']);
164
-
165
- printRelation('Full shipment details', fullShipments);
166
-
167
- // Left join: all suppliers with their shipments (if any)
168
- const suppliersWithShipments = suppliers
169
- .project(['sid', 'name'])
170
- .left_join(
171
- shipments.summarize(['sid'], { totalQty: { op: 'sum', attr: 'qty' } }),
172
- ['sid']
173
- );
174
-
175
- printRelation('All suppliers with total shipped qty', suppliersWithShipments);
176
-
177
- // Matching: suppliers who have shipped something
178
- const activeSuppliers = suppliers.matching(shipments, ['sid']);
179
- printRelation('Suppliers who have made shipments', activeSuppliers);
180
-
181
- // Not matching: suppliers who haven't shipped anything
182
- const inactiveSuppliers = suppliers.not_matching(shipments, ['sid']);
183
- printRelation('Suppliers with no shipments', inactiveSuppliers);
184
-
185
- // =============================================================================
186
- // Example 5: Aggregation - summarize
187
- // =============================================================================
188
-
189
- console.log('\n*** EXAMPLE 5: Aggregation ***');
190
-
191
- // Count suppliers per city
192
- const suppliersPerCity = suppliers.summarize(['city'], {
193
- supplierCount: 'count',
194
- avgStatus: { op: 'avg', attr: 'status' },
195
- });
196
-
197
- printRelation('Suppliers per city with average status', suppliersPerCity);
198
-
199
- // Shipment statistics per supplier
200
- const shipmentStats = shipments
201
- .join(suppliers.project(['sid', 'name']), ['sid'])
202
- .summarize(['name'], {
203
- shipmentCount: 'count',
204
- totalQty: { op: 'sum', attr: 'qty' },
205
- avgQty: { op: 'avg', attr: 'qty' },
206
- minQty: { op: 'min', attr: 'qty' },
207
- maxQty: { op: 'max', attr: 'qty' },
208
- });
209
-
210
- printRelation('Shipment statistics per supplier', shipmentStats);
211
-
212
- // Grand total
213
- const grandTotal = shipments.summarize([], {
214
- totalShipments: 'count',
215
- totalQuantity: { op: 'sum', attr: 'qty' },
216
- });
217
-
218
- printRelation('Grand total', grandTotal);
219
-
220
- // =============================================================================
221
- // Example 6: Grouping and Nesting - group, image, wrap
222
- // =============================================================================
223
-
224
- console.log('\n*** EXAMPLE 6: Grouping and Nesting ***');
225
-
226
- // Group parts: nest pid and pname into a 'items' relation, grouped by color
227
- const partsByColor = parts
228
- .project(['color', 'pid', 'pname'])
229
- .group(['pid', 'pname'], 'items');
230
-
231
- printRelation('Parts grouped by color (nested items)', partsByColor);
232
-
233
- // Show expanded view of one group
234
- const redParts = partsByColor.restrict({ color: 'Red' }).one();
235
- console.log('Red parts items:', redParts.items.toArray());
236
-
237
- // Image: for each supplier, get their shipped parts
238
- const supplierParts = suppliers
239
- .project(['sid', 'name'])
240
- .image(shipments.project(['sid', 'pid', 'qty']), 'shipments', ['sid']);
241
-
242
- printRelation('Each supplier with their shipments', supplierParts);
243
-
244
- // Wrap: combine part attributes into a nested object
245
- const wrappedParts = parts.wrap(['color', 'weight'], 'specs');
246
-
247
- printRelation('Parts with wrapped specs', wrappedParts);
248
-
249
- // =============================================================================
250
- // Example 7: Complex Chained Query
251
- // =============================================================================
252
-
253
- console.log('\n*** EXAMPLE 7: Complex Chained Query ***');
254
-
255
- // Find supplier quantities by part color
256
- const supplierQtyByColor = shipments
257
- .join(parts.project(['pid', 'color']), ['pid'])
258
- .join(suppliers.project(['sid', 'name']), ['sid'])
259
- .summarize(['color', 'name'], {
260
- totalQty: { op: 'sum', attr: 'qty' },
261
- })
262
- .project(['color', 'name', 'totalQty']);
263
-
264
- printRelation('Supplier quantities by part color', supplierQtyByColor);
265
-
266
- // =============================================================================
267
- // Example 8: Data Transformation - transform, allbut
268
- // =============================================================================
269
-
270
- console.log('\n*** EXAMPLE 8: Data Transformation ***');
271
-
272
- // Transform all string values to uppercase
273
- const uppercasedSuppliers = suppliers.transform({
274
- name: (v: unknown) => (v as string).toUpperCase(),
275
- city: (v: unknown) => (v as string).toUpperCase(),
276
- });
277
-
278
- printRelation('Suppliers with uppercase names and cities', uppercasedSuppliers);
279
-
280
- // Allbut: select all attributes except some
281
- // Type: Relation<{ pid: string; pname: string; color: string; weight: number }>
282
- const partsWithoutLocation = parts.allbut(['city']);
283
-
284
- printRelation('Parts without city attribute', partsWithoutLocation);
285
-
286
- // =============================================================================
287
- // Example 9: Using .one() to extract a single tuple
288
- // =============================================================================
289
-
290
- console.log('\n*** EXAMPLE 9: Extracting Single Tuples ***');
291
-
292
- // Get a specific supplier - returns typed Supplier
293
- const smith: Supplier = suppliers.restrict({ sid: 'S1' }).one();
294
- console.log('\nSupplier S1 (Smith):', smith);
295
-
296
- // Get the total quantity shipped
297
- const total = shipments.summarize([], { total: { op: 'sum', attr: 'qty' } }).one();
298
- console.log('Total quantity shipped:', total.total);
299
-
300
- // =============================================================================
301
- // Example 10: Checking Relation Properties
302
- // =============================================================================
303
-
304
- console.log('\n*** EXAMPLE 10: Relation Properties ***');
305
-
306
- // Check if two relations are equal
307
- const parisSuppliers1 = suppliers.restrict({ city: 'Paris' });
308
- const parisSuppliers2 = suppliers.restrict((t: Supplier) => t.city === 'Paris');
309
-
310
- console.log('\nAre the two Paris supplier queries equal?', parisSuppliers1.isEqual(parisSuppliers2));
311
-
312
- // Check if something is a relation
313
- console.log('Is suppliers a relation?', Bmg.isRelation(suppliers));
314
- console.log('Is an array a relation?', Bmg.isRelation([{ a: 1 }]));
315
-
316
- console.log('\n*** ALL EXAMPLES COMPLETED ***\n');