@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,36 @@
|
|
|
1
|
+
FROM alpine:latest
|
|
2
|
+
|
|
3
|
+
# Install packages: base tools, node, ruby, postgresql client
|
|
4
|
+
RUN apk update && apk add --no-cache \
|
|
5
|
+
bash \
|
|
6
|
+
curl \
|
|
7
|
+
git \
|
|
8
|
+
build-base \
|
|
9
|
+
nodejs \
|
|
10
|
+
npm \
|
|
11
|
+
vim \
|
|
12
|
+
sudo
|
|
13
|
+
|
|
14
|
+
# Create non-root user with sudo access
|
|
15
|
+
RUN addgroup -S bmg && adduser -S bmg -G bmg \
|
|
16
|
+
&& echo "bmg ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
|
|
17
|
+
|
|
18
|
+
# Install Claude Code CLI globally
|
|
19
|
+
RUN npm install -g @anthropic-ai/claude-code
|
|
20
|
+
|
|
21
|
+
# Set up working directory
|
|
22
|
+
WORKDIR /workspace
|
|
23
|
+
|
|
24
|
+
# Copy and set up entrypoint
|
|
25
|
+
COPY entrypoint.sh /entrypoint.sh
|
|
26
|
+
RUN chmod +x /entrypoint.sh
|
|
27
|
+
|
|
28
|
+
# Create .claude directory for history persistence
|
|
29
|
+
RUN mkdir -p /home/bmg/.claude && chown -R bmg:bmg /home/bmg/.claude
|
|
30
|
+
|
|
31
|
+
# Switch to non-root user
|
|
32
|
+
USER bmg
|
|
33
|
+
|
|
34
|
+
# Use entrypoint to configure git and run command
|
|
35
|
+
ENTRYPOINT ["/entrypoint.sh"]
|
|
36
|
+
CMD ["bash"]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Hacking on Bmg with Claude Code
|
|
2
|
+
|
|
3
|
+
This Docker setup provides a sandboxed environment with all tools needed to hack on Bmg:
|
|
4
|
+
- Node.js
|
|
5
|
+
- Claude Code CLI
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
1. Docker and Docker Compose installed
|
|
10
|
+
2. A Claude Code account
|
|
11
|
+
|
|
12
|
+
## Getting Started
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
cd .claude/safe-setup
|
|
16
|
+
|
|
17
|
+
# Create your .env file from template
|
|
18
|
+
cp .env.example .env
|
|
19
|
+
|
|
20
|
+
# Edit .env to set your git identity
|
|
21
|
+
# GIT_USER_NAME=Your Name
|
|
22
|
+
# GIT_USER_EMAIL=your.email@example.com
|
|
23
|
+
|
|
24
|
+
# Build and start containers
|
|
25
|
+
make up
|
|
26
|
+
|
|
27
|
+
# Enter the dev environment
|
|
28
|
+
make shell
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Persistent Data
|
|
32
|
+
|
|
33
|
+
- **Claude history**: Stored in a named Docker volume (`claude-history`) that persists across container restarts
|
|
34
|
+
- **Git configuration**: Automatically set from `GIT_USER_NAME` and `GIT_USER_EMAIL` environment variables
|
|
35
|
+
|
|
36
|
+
## Inside the Container
|
|
37
|
+
|
|
38
|
+
You're now user `bmg` in `/workspace` (the Bmg project root).
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Install project dependencies
|
|
42
|
+
npm install
|
|
43
|
+
|
|
44
|
+
# Run tests
|
|
45
|
+
npm run test
|
|
46
|
+
|
|
47
|
+
# Use Claude Code
|
|
48
|
+
claude
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
PostgreSQL is pre-configured via environment variables. Just run `psql` to connect.
|
|
52
|
+
|
|
53
|
+
## Commands
|
|
54
|
+
|
|
55
|
+
| Command | Description |
|
|
56
|
+
|---------|-------------|
|
|
57
|
+
| `make up` | Build and start containers |
|
|
58
|
+
| `make down` | Stop containers |
|
|
59
|
+
| `make shell` | Enter dev container |
|
|
60
|
+
| `make restart` | Restart everything |
|
|
61
|
+
| `make logs` | Follow container logs |
|
|
62
|
+
| `make status` | Show container status |
|
|
63
|
+
| `make clean` | Remove containers and images |
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
.PHONY: up down shell restart clean logs status
|
|
2
|
+
|
|
3
|
+
up:
|
|
4
|
+
docker compose up -d --build
|
|
5
|
+
@echo "Containers started. Use 'make shell' to access dev environment"
|
|
6
|
+
|
|
7
|
+
down:
|
|
8
|
+
docker compose down
|
|
9
|
+
|
|
10
|
+
shell:
|
|
11
|
+
docker compose exec dev bash
|
|
12
|
+
|
|
13
|
+
restart: down up
|
|
14
|
+
|
|
15
|
+
clean: down
|
|
16
|
+
docker compose down --rmi all --volumes
|
|
17
|
+
|
|
18
|
+
logs:
|
|
19
|
+
docker compose logs -f
|
|
20
|
+
|
|
21
|
+
status:
|
|
22
|
+
@docker compose ps
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
services:
|
|
2
|
+
|
|
3
|
+
dev:
|
|
4
|
+
build:
|
|
5
|
+
context: .
|
|
6
|
+
dockerfile: Dockerfile.claude
|
|
7
|
+
container_name: claude
|
|
8
|
+
volumes:
|
|
9
|
+
- ../..:/workspace
|
|
10
|
+
- claude-history:/home/bmg/.claude
|
|
11
|
+
environment:
|
|
12
|
+
- GIT_USER_NAME=${GIT_USER_NAME:-}
|
|
13
|
+
- GIT_USER_EMAIL=${GIT_USER_EMAIL:-}
|
|
14
|
+
stdin_open: true
|
|
15
|
+
tty: true
|
|
16
|
+
|
|
17
|
+
volumes:
|
|
18
|
+
claude-history:
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Configure git user/email from environment variables if provided
|
|
4
|
+
if [ -n "$GIT_USER_NAME" ]; then
|
|
5
|
+
git config --global user.name "$GIT_USER_NAME"
|
|
6
|
+
fi
|
|
7
|
+
|
|
8
|
+
if [ -n "$GIT_USER_EMAIL" ]; then
|
|
9
|
+
git config --global user.email "$GIT_USER_EMAIL"
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
# Execute the command passed to the container
|
|
13
|
+
exec "$@"
|
|
@@ -0,0 +1,273 @@
|
|
|
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
|
|
@@ -0,0 +1,26 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
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
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Bmg.js - Relational Algebra for JavaScript/TypeScript
|
|
2
|
+
|
|
3
|
+
A TypeScript/JavaScript implementation of [BMG](https://www.relational-algebra.dev/), providing relational algebra operators for working with arrays of objects.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @enspirit/bmg-js
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
Using the Relation abstraction:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { Bmg } from '@enspirit/bmg-js'
|
|
17
|
+
|
|
18
|
+
const suppliers = Bmg([
|
|
19
|
+
{ sid: 'S1', name: 'Smith', status: 20, city: 'London' },
|
|
20
|
+
{ sid: 'S2', name: 'Jones', status: 10, city: 'Paris' },
|
|
21
|
+
{ sid: 'S3', name: 'Blake', status: 30, city: 'Paris' },
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
// Chain operations fluently
|
|
25
|
+
const parisSuppliers = suppliers
|
|
26
|
+
.restrict({ city: 'Paris' })
|
|
27
|
+
.project(['sid', 'name'])
|
|
28
|
+
|
|
29
|
+
console.log(parisSuppliers.toArray())
|
|
30
|
+
// => [{ sid: 'S2', name: 'Jones' }, { sid: 'S3', name: 'Blake' }]
|
|
31
|
+
|
|
32
|
+
// Extract a single tuple
|
|
33
|
+
const smith = suppliers.restrict({ sid: 'S1' }).one()
|
|
34
|
+
// => { sid: 'S1', name: 'Smith', status: 20, city: 'London' }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Using standalone operators on plain arrays:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { restrict, project } from '@enspirit/bmg-js'
|
|
41
|
+
|
|
42
|
+
const suppliers = [
|
|
43
|
+
{ sid: 'S1', name: 'Smith', city: 'London' },
|
|
44
|
+
{ sid: 'S2', name: 'Jones', city: 'Paris' },
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
const result = project(restrict(suppliers, { city: 'Paris' }), ['name'])
|
|
48
|
+
// => [{ name: 'Jones' }]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## TypeScript Support
|
|
52
|
+
|
|
53
|
+
Bmg.js provides full TypeScript support with generic types:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { Bmg } from '@enspirit/bmg-js'
|
|
57
|
+
|
|
58
|
+
interface Supplier {
|
|
59
|
+
sid: string
|
|
60
|
+
name: string
|
|
61
|
+
status: number
|
|
62
|
+
city: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const suppliers = Bmg<Supplier>([
|
|
66
|
+
{ sid: 'S1', name: 'Smith', status: 20, city: 'London' },
|
|
67
|
+
])
|
|
68
|
+
|
|
69
|
+
// Type-safe operations with autocomplete
|
|
70
|
+
const projected = suppliers.project(['sid', 'name'])
|
|
71
|
+
// Type: Relation<{ sid: string; name: string }>
|
|
72
|
+
|
|
73
|
+
const one = suppliers.restrict({ sid: 'S1' }).one()
|
|
74
|
+
// Type: Supplier
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
See the [full type-safe example](./example/index.ts) for more.
|
|
78
|
+
|
|
79
|
+
## Available Operators
|
|
80
|
+
|
|
81
|
+
| Category | Operator | Description |
|
|
82
|
+
|----------|----------|-------------|
|
|
83
|
+
| **Filtering** | `restrict(predicate)` | Keep tuples matching predicate |
|
|
84
|
+
| | `where(predicate)` | Alias for restrict |
|
|
85
|
+
| | `exclude(predicate)` | Keep tuples NOT matching predicate |
|
|
86
|
+
| | `matching(other, keys?)` | Semi-join (tuples with match in other) |
|
|
87
|
+
| | `not_matching(other, keys?)` | Anti-join (tuples without match) |
|
|
88
|
+
| **Projection** | `project(attrs)` | Keep only specified attributes |
|
|
89
|
+
| | `allbut(attrs)` | Keep all attributes except specified |
|
|
90
|
+
| **Extension** | `extend(extensions)` | Add computed attributes |
|
|
91
|
+
| | `constants(values)` | Add constant attributes |
|
|
92
|
+
| **Renaming** | `rename(mapping)` | Rename attributes |
|
|
93
|
+
| | `prefix(pfx, options?)` | Add prefix to attribute names |
|
|
94
|
+
| | `suffix(sfx, options?)` | Add suffix to attribute names |
|
|
95
|
+
| **Set Operations** | `union(other)` | Set union of two relations |
|
|
96
|
+
| | `minus(other)` | Set difference (tuples in left but not right) |
|
|
97
|
+
| | `intersect(other)` | Set intersection (tuples in both) |
|
|
98
|
+
| **Join Operations** | `join(other, keys?)` | Natural join on common/specified attributes |
|
|
99
|
+
| | `left_join(other, keys?)` | Left outer join |
|
|
100
|
+
| | `cross_product(other)` | Cartesian product |
|
|
101
|
+
| | `cross_join(other)` | Alias for cross_product |
|
|
102
|
+
| **Nesting & Grouping** | `image(other, as, keys?)` | Nest matching tuples as relation attribute |
|
|
103
|
+
| | `group(attrs, as)` | Group attributes into nested relation |
|
|
104
|
+
| | `ungroup(attr)` | Flatten nested relation |
|
|
105
|
+
| | `wrap(attrs, as)` | Wrap attributes into tuple-valued attribute |
|
|
106
|
+
| | `unwrap(attr)` | Flatten tuple-valued attribute |
|
|
107
|
+
| | `autowrap(options?)` | Auto-wrap by separator pattern |
|
|
108
|
+
| **Aggregation** | `summarize(by, aggregators)` | Group and aggregate |
|
|
109
|
+
| **Transformation** | `transform(transformation)` | Transform attribute values |
|
|
110
|
+
| **Non-Relational** | `one()` | Extract single tuple (throws if not exactly one) |
|
|
111
|
+
| | `toArray()` | Convert relation to array |
|
|
112
|
+
| | `isEqual(other)` | Check set equality |
|
|
113
|
+
| | `yByX(y, x)` | Create `{ x-value: y-value }` mapping |
|
|
114
|
+
| | `Bmg.isRelation(value)` | Check if value is a Relation (static) |
|
|
115
|
+
|
|
116
|
+
Built-in aggregators for `summarize`: `count`, `sum`, `min`, `max`, `avg`, `collect`
|
|
117
|
+
|
|
118
|
+
## Theory
|
|
119
|
+
|
|
120
|
+
Bmg.js implements relational algebra with these principles:
|
|
121
|
+
|
|
122
|
+
1. **No duplicates** - Relations are sets; duplicate tuples are automatically removed
|
|
123
|
+
2. **Order independence** - Tuple order and attribute order have no semantic meaning
|
|
124
|
+
3. **Set equality** - Two relations are equal if they contain the same tuples
|
|
125
|
+
|
|
126
|
+
## Horizon
|
|
127
|
+
|
|
128
|
+
The aim is to have a language where one can write beautiful functional expressions:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
suppliers
|
|
132
|
+
|> restrict( _ ~> _.status > 20 )
|
|
133
|
+
|> rename(sid: 'id', name: 'lastname')
|
|
134
|
+
|> restrict(city: 'Paris')
|
|
135
|
+
|> one
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
This will be provided by [Elo](https://elo-lang.org).
|
|
139
|
+
|
|
140
|
+
## Versioning
|
|
141
|
+
|
|
142
|
+
Bmg.js follows [Semantic Versioning](https://semver.org/) (SemVer):
|
|
143
|
+
|
|
144
|
+
- **MAJOR** (x.0.0) - Breaking changes to the API (renamed operators, changed signatures, removed features)
|
|
145
|
+
- **MINOR** (0.x.0) - New operators or features, fully backward-compatible
|
|
146
|
+
- **PATCH** (0.0.x) - Bug fixes and performance improvements, no API changes
|
|
147
|
+
|
|
148
|
+
For Bmg.js specifically:
|
|
149
|
+
- Adding a new operator (e.g., `sort`, `page`) is a **minor** release
|
|
150
|
+
- Fixing incorrect behavior in an existing operator is a **patch** release
|
|
151
|
+
- Changing an operator's signature or renaming it is a **major** release
|
|
152
|
+
|
|
153
|
+
## Contributing
|
|
154
|
+
|
|
155
|
+
1. Fork the repository
|
|
156
|
+
2. Create a feature branch (`git checkout -b feature/new-operator`)
|
|
157
|
+
3. Add your changes with tests
|
|
158
|
+
4. Ensure tests pass: `npm run test`
|
|
159
|
+
5. Commit following conventional commits (e.g., `feat: add sort operator`)
|
|
160
|
+
6. Open a pull request
|
|
161
|
+
|
|
162
|
+
When adding a new operator:
|
|
163
|
+
- Add the operator implementation
|
|
164
|
+
- Add unit tests (use relational comparisons with `isEqual`, avoid accessing "first" tuple)
|
|
165
|
+
- Update the README operators table
|
|
166
|
+
- One commit per operator
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
MIT
|