@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,22 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
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');
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bmg-example",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Examples demonstrating Bmg.js relational algebra operations",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "tsx index.ts"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@enspirit/bmg-js": "file:.."
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"tsx": "^4.7.0",
|
|
14
|
+
"typescript": "^5.3.0"
|
|
15
|
+
}
|
|
16
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@enspirit/bmg-js",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Relational algebra for JavaScript/TypeScript - work with arrays of objects using powerful set operations",
|
|
5
|
+
"keywords": ["relational", "algebra", "relation", "set", "query", "data", "functional"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/enspirit/bmg.js"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"source": "src/index.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
"types": "./dist/src/index.d.ts",
|
|
15
|
+
"require": "./dist/bmg.cjs",
|
|
16
|
+
"default": "./dist/bmg.modern.js"
|
|
17
|
+
},
|
|
18
|
+
"main": "./dist/bmg.cjs",
|
|
19
|
+
"module": "./dist/bmg.module.js",
|
|
20
|
+
"types": "./dist/src/index.d.ts",
|
|
21
|
+
"unpkg": "./dist/bmg.umd.js",
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "microbundle",
|
|
24
|
+
"dev": "microbundle watch",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"test:watch": "vitest"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"microbundle": "^0.15.1",
|
|
30
|
+
"vite-tsconfig-paths": "^5.1.4",
|
|
31
|
+
"vitest": "^1.4.0"
|
|
32
|
+
},
|
|
33
|
+
"packageManager": "pnpm@8.11.0+sha1.01bf39424f1008f29012bbae851d2acb8a20a0d0"
|
|
34
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import {
|
|
2
|
+
allbut,
|
|
3
|
+
autowrap,
|
|
4
|
+
constants,
|
|
5
|
+
cross_product,
|
|
6
|
+
exclude,
|
|
7
|
+
extend,
|
|
8
|
+
group,
|
|
9
|
+
image,
|
|
10
|
+
intersect,
|
|
11
|
+
isEqual,
|
|
12
|
+
join,
|
|
13
|
+
left_join,
|
|
14
|
+
matching,
|
|
15
|
+
minus,
|
|
16
|
+
not_matching,
|
|
17
|
+
one,
|
|
18
|
+
prefix,
|
|
19
|
+
project,
|
|
20
|
+
rename,
|
|
21
|
+
restrict,
|
|
22
|
+
suffix,
|
|
23
|
+
summarize,
|
|
24
|
+
where,
|
|
25
|
+
transform,
|
|
26
|
+
ungroup,
|
|
27
|
+
union,
|
|
28
|
+
unwrap,
|
|
29
|
+
wrap,
|
|
30
|
+
yByX,
|
|
31
|
+
} from "../operators";
|
|
32
|
+
import type {
|
|
33
|
+
AttrName,
|
|
34
|
+
AutowrapOptions,
|
|
35
|
+
JoinKeys,
|
|
36
|
+
Relation,
|
|
37
|
+
RelationOperand,
|
|
38
|
+
Transformation,
|
|
39
|
+
Tuple,
|
|
40
|
+
TypedPredicate,
|
|
41
|
+
TypedExtension,
|
|
42
|
+
RenameMap,
|
|
43
|
+
Renamed,
|
|
44
|
+
Prefixed,
|
|
45
|
+
Suffixed,
|
|
46
|
+
Joined,
|
|
47
|
+
LeftJoined,
|
|
48
|
+
Wrapped,
|
|
49
|
+
Unwrapped,
|
|
50
|
+
Ungrouped,
|
|
51
|
+
} from "../types";
|
|
52
|
+
import type { AggregatorResults } from "../utility-types";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* In-memory implementation of the Relation interface.
|
|
56
|
+
*
|
|
57
|
+
* @typeParam T - The tuple type for this relation. Defaults to `Tuple` (Record<string, unknown>).
|
|
58
|
+
*/
|
|
59
|
+
export class MemoryRelation<T = Tuple> implements Relation<T> {
|
|
60
|
+
|
|
61
|
+
constructor(private tuples: T[]) {
|
|
62
|
+
this.tuples = tuples;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// === Type-preserving operators ===
|
|
66
|
+
|
|
67
|
+
restrict(p: TypedPredicate<T>): Relation<T> {
|
|
68
|
+
return restrict(this as any, p as any) as unknown as Relation<T>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
where(p: TypedPredicate<T>): Relation<T> {
|
|
72
|
+
return where(this as any, p as any) as unknown as Relation<T>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
exclude(p: TypedPredicate<T>): Relation<T> {
|
|
76
|
+
return exclude(this as any, p as any) as unknown as Relation<T>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// === Projection operators ===
|
|
80
|
+
|
|
81
|
+
project<K extends keyof T>(attrs: K[]): Relation<Pick<T, K>> {
|
|
82
|
+
return project(this as any, attrs as AttrName[]) as unknown as Relation<Pick<T, K>>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
allbut<K extends keyof T>(attrs: K[]): Relation<Omit<T, K>> {
|
|
86
|
+
return allbut(this as any, attrs as AttrName[]) as unknown as Relation<Omit<T, K>>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// === Extension operators ===
|
|
90
|
+
|
|
91
|
+
extend<E extends Record<string, unknown>>(e: TypedExtension<T, E>): Relation<T & E> {
|
|
92
|
+
return extend(this as any, e as any) as unknown as Relation<T & E>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
constants<C extends Tuple>(consts: C): Relation<T & C> {
|
|
96
|
+
return constants(this as any, consts) as unknown as Relation<T & C>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// === Rename operators ===
|
|
100
|
+
|
|
101
|
+
rename<R extends RenameMap<T>>(r: R): Relation<Renamed<T, R>> {
|
|
102
|
+
return rename(this as any, r as any) as unknown as Relation<Renamed<T, R>>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
prefix<P extends string, Ex extends keyof T = never>(pfx: P, options?: { except?: Ex[] }): Relation<Prefixed<T, P, Ex>> {
|
|
106
|
+
return prefix(this as any, pfx, options as any) as unknown as Relation<Prefixed<T, P, Ex>>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
suffix<S extends string, Ex extends keyof T = never>(sfx: S, options?: { except?: Ex[] }): Relation<Suffixed<T, S, Ex>> {
|
|
110
|
+
return suffix(this as any, sfx, options as any) as unknown as Relation<Suffixed<T, S, Ex>>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// === Set operators ===
|
|
114
|
+
|
|
115
|
+
union(right: RelationOperand<T>): Relation<T> {
|
|
116
|
+
return union(this as any, right as any) as unknown as Relation<T>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
minus(right: RelationOperand<T>): Relation<T> {
|
|
120
|
+
return minus(this as any, right as any) as unknown as Relation<T>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
intersect(right: RelationOperand<T>): Relation<T> {
|
|
124
|
+
return intersect(this as any, right as any) as unknown as Relation<T>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// === Semi-join operators ===
|
|
128
|
+
|
|
129
|
+
matching<R>(right: RelationOperand<R>, keys?: JoinKeys): Relation<T> {
|
|
130
|
+
return matching(this as any, right as any, keys) as unknown as Relation<T>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
not_matching<R>(right: RelationOperand<R>, keys?: JoinKeys): Relation<T> {
|
|
134
|
+
return not_matching(this as any, right as any, keys) as unknown as Relation<T>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// === Join operators ===
|
|
138
|
+
|
|
139
|
+
join<R>(right: RelationOperand<R>, keys?: JoinKeys): Relation<Joined<T, R>> {
|
|
140
|
+
return join(this as any, right as any, keys) as unknown as Relation<Joined<T, R>>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
left_join<R>(right: RelationOperand<R>, keys?: JoinKeys): Relation<LeftJoined<T, R>> {
|
|
144
|
+
return left_join(this as any, right as any, keys) as unknown as Relation<LeftJoined<T, R>>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
cross_product<R>(right: RelationOperand<R>): Relation<T & R> {
|
|
148
|
+
return cross_product(this as any, right as any) as unknown as Relation<T & R>;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
cross_join<R>(right: RelationOperand<R>): Relation<T & R> {
|
|
152
|
+
return cross_product(this as any, right as any) as unknown as Relation<T & R>;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// === Nesting operators ===
|
|
156
|
+
|
|
157
|
+
image<R, As extends string>(right: RelationOperand<R>, as: As, keys?: JoinKeys): Relation<T & Record<As, Relation<Omit<R, keyof T & keyof R>>>> {
|
|
158
|
+
return image(this as any, right as any, as, keys) as unknown as Relation<T & Record<As, Relation<Omit<R, keyof T & keyof R>>>>;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
group<K extends keyof T, As extends string>(attrs: K[], as: As): Relation<Omit<T, K> & Record<As, Relation<Pick<T, K>>>> {
|
|
162
|
+
return group(this as any, attrs as AttrName[], as) as unknown as Relation<Omit<T, K> & Record<As, Relation<Pick<T, K>>>>;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
ungroup<K extends keyof T>(attr: K): Relation<Ungrouped<T, K>> {
|
|
166
|
+
return ungroup(this as any, attr as AttrName) as unknown as Relation<Ungrouped<T, K>>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
wrap<K extends keyof T, As extends string>(attrs: K[], as: As): Relation<Wrapped<T, K, As>> {
|
|
170
|
+
return wrap(this as any, attrs as AttrName[], as) as unknown as Relation<Wrapped<T, K, As>>;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
unwrap<K extends keyof T>(attr: K): Relation<Unwrapped<T, K>> {
|
|
174
|
+
return unwrap(this as any, attr as AttrName) as unknown as Relation<Unwrapped<T, K>>;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// === Aggregation ===
|
|
178
|
+
|
|
179
|
+
summarize<By extends keyof T, Aggs extends Record<string, unknown>>(by: By[], aggs: Aggs): Relation<Pick<T, By> & AggregatorResults<Aggs>> {
|
|
180
|
+
return summarize(this as any, by as AttrName[], aggs as any) as unknown as Relation<Pick<T, By> & AggregatorResults<Aggs>>;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// === Transform ===
|
|
184
|
+
|
|
185
|
+
transform(t: Transformation): Relation<T> {
|
|
186
|
+
return transform(this as any, t) as unknown as Relation<T>;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// === Dynamic ===
|
|
190
|
+
|
|
191
|
+
autowrap(options?: AutowrapOptions): Relation<Tuple> {
|
|
192
|
+
return autowrap(this as any, options) as Relation<Tuple>;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// === Non-relational ===
|
|
196
|
+
|
|
197
|
+
one(): T {
|
|
198
|
+
return one(this as any) as T;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
toArray(): T[] {
|
|
202
|
+
return this.tuples;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
yByX<Y extends keyof T, X extends keyof T>(y: Y, x: X): Record<T[X] & PropertyKey, T[Y]> {
|
|
206
|
+
return yByX(this as any, y as AttrName, x as AttrName) as Record<T[X] & PropertyKey, T[Y]>;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
isEqual(right: any): boolean {
|
|
210
|
+
return isEqual(this as any, right as any);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Memory';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export * from './operators';
|
|
2
|
+
export * from './types';
|
|
3
|
+
export * from './utility-types';
|
|
4
|
+
|
|
5
|
+
import { MemoryRelation } from './Relation';
|
|
6
|
+
import { isRelation } from './operators';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates a new in-memory relation from an array of tuples.
|
|
10
|
+
*
|
|
11
|
+
* @typeParam T - The tuple type. Inferred from input or explicitly provided.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // Untyped usage (backwards compatible)
|
|
15
|
+
* const r = Bmg([{ id: 1, name: 'Alice' }]);
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // Typed usage with explicit type parameter
|
|
19
|
+
* interface Person { id: number; name: string }
|
|
20
|
+
* const r = Bmg<Person>([{ id: 1, name: 'Alice' }]);
|
|
21
|
+
* r.project(['id']); // Autocomplete suggests 'id' | 'name'
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // Type is inferred from input
|
|
25
|
+
* const r = Bmg([{ id: 1, name: 'Alice' }] as const);
|
|
26
|
+
*/
|
|
27
|
+
export function Bmg<T>(tuples: T[]): MemoryRelation<T> {
|
|
28
|
+
return new MemoryRelation<T>(tuples);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
Bmg.isRelation = isRelation;
|