boundlessdb 0.1.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/CHANGELOG.md +89 -0
- package/LICENSE +21 -0
- package/README.md +545 -0
- package/dist/better-sqlite3-shim.d.ts +12 -0
- package/dist/better-sqlite3-shim.d.ts.map +1 -0
- package/dist/better-sqlite3-shim.js +18 -0
- package/dist/better-sqlite3-shim.js.map +1 -0
- package/dist/browser.d.ts +14 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +14 -0
- package/dist/browser.js.map +1 -0
- package/dist/config/extractor.d.ts +22 -0
- package/dist/config/extractor.d.ts.map +1 -0
- package/dist/config/extractor.js +132 -0
- package/dist/config/extractor.js.map +1 -0
- package/dist/config/validator.d.ts +14 -0
- package/dist/config/validator.d.ts.map +1 -0
- package/dist/config/validator.js +94 -0
- package/dist/config/validator.js.map +1 -0
- package/dist/event-store.browser.d.ts +61 -0
- package/dist/event-store.browser.d.ts.map +1 -0
- package/dist/event-store.browser.js +323 -0
- package/dist/event-store.browser.js.map +1 -0
- package/dist/event-store.d.ts +101 -0
- package/dist/event-store.d.ts.map +1 -0
- package/dist/event-store.js +249 -0
- package/dist/event-store.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/query-builder.d.ts +72 -0
- package/dist/query-builder.d.ts.map +1 -0
- package/dist/query-builder.js +84 -0
- package/dist/query-builder.js.map +1 -0
- package/dist/storage/interface.d.ts +48 -0
- package/dist/storage/interface.d.ts.map +1 -0
- package/dist/storage/interface.js +5 -0
- package/dist/storage/interface.js.map +1 -0
- package/dist/storage/memory.d.ts +27 -0
- package/dist/storage/memory.d.ts.map +1 -0
- package/dist/storage/memory.js +94 -0
- package/dist/storage/memory.js.map +1 -0
- package/dist/storage/postgres.d.ts +76 -0
- package/dist/storage/postgres.d.ts.map +1 -0
- package/dist/storage/postgres.js +346 -0
- package/dist/storage/postgres.js.map +1 -0
- package/dist/storage/sqlite.d.ts +47 -0
- package/dist/storage/sqlite.d.ts.map +1 -0
- package/dist/storage/sqlite.js +249 -0
- package/dist/storage/sqlite.js.map +1 -0
- package/dist/storage/sqljs.d.ts +60 -0
- package/dist/storage/sqljs.d.ts.map +1 -0
- package/dist/storage/sqljs.js +354 -0
- package/dist/storage/sqljs.js.map +1 -0
- package/dist/types.d.ts +172 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +52 -0
- package/dist/types.js.map +1 -0
- package/package.json +75 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to BoundlessDB will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
### Breaking Changes
|
|
8
|
+
|
|
9
|
+
#### Removed: Token/Cryptographic Signing
|
|
10
|
+
- Removed `token.ts` and `token.browser.ts`
|
|
11
|
+
- Removed `secret` option from `EventStoreOptions`
|
|
12
|
+
- **Migration:** Use `appendCondition` directly (see below)
|
|
13
|
+
|
|
14
|
+
#### Removed: Decider Pattern Helpers
|
|
15
|
+
- Removed `src/decider.ts` with `Decider` type, `evolve()` and `decide()` helpers
|
|
16
|
+
- **Migration:** Use plain functions with standard `reduce`:
|
|
17
|
+
```typescript
|
|
18
|
+
// Before
|
|
19
|
+
const state = evolve(events, decider);
|
|
20
|
+
const newEvents = decide(command, state, decider);
|
|
21
|
+
|
|
22
|
+
// After
|
|
23
|
+
const state = events.reduce(evolve, initialState);
|
|
24
|
+
const newEvents = decide(command, state);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
#### Changed: Token → AppendCondition
|
|
28
|
+
- `read()` now returns `appendCondition` as a plain object (not encoded token)
|
|
29
|
+
- `append()` accepts `AppendCondition` directly
|
|
30
|
+
- **Migration:**
|
|
31
|
+
```typescript
|
|
32
|
+
// Before
|
|
33
|
+
const { events, token } = await store.read({ conditions });
|
|
34
|
+
await store.append(newEvents, token);
|
|
35
|
+
|
|
36
|
+
// After
|
|
37
|
+
const { events, appendCondition } = await store.read({ conditions });
|
|
38
|
+
await store.append(newEvents, appendCondition);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
#### QueryResult Class
|
|
44
|
+
- `read()` returns a `QueryResult` with helper methods:
|
|
45
|
+
- `isEmpty()`, `count`, `first()`, `last()`
|
|
46
|
+
- `position`, `conditions`, `appendCondition`
|
|
47
|
+
|
|
48
|
+
#### Typed Events
|
|
49
|
+
- `Event<Type, Payload>` marker type for type-safe events
|
|
50
|
+
- `read<E>()` and `append<E>()` support generics
|
|
51
|
+
|
|
52
|
+
#### Union Types for QueryCondition
|
|
53
|
+
- `UnconstrainedCondition`: `{ type: 'X' }` — match all events of type
|
|
54
|
+
- `ConstrainedCondition`: `{ type: 'X', key: 'a', value: 'b' }` — match specific key
|
|
55
|
+
- Partial conditions like `{ type: 'X', key: 'a' }` are now TypeScript errors
|
|
56
|
+
|
|
57
|
+
#### Type Guard
|
|
58
|
+
- `isConstrainedCondition()` exported for storage implementations
|
|
59
|
+
|
|
60
|
+
#### Fluent Query API
|
|
61
|
+
- Chainable query builder: `store.query<E>()`
|
|
62
|
+
- Methods: `matchType()`, `matchKey()`, `fromPosition()`, `limit()`, `read()`
|
|
63
|
+
```typescript
|
|
64
|
+
const { events, appendCondition } = await store.query<CourseEvent>()
|
|
65
|
+
.matchType('CourseCreated')
|
|
66
|
+
.matchKey('StudentSubscribed', 'course', 'cs101')
|
|
67
|
+
.read();
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
|
|
72
|
+
- Empty `conditions: []` now returns all events (was: error)
|
|
73
|
+
- `appendCondition` is a plain object: `{ position: bigint, conditions: QueryCondition[] }`
|
|
74
|
+
|
|
75
|
+
### Documentation
|
|
76
|
+
|
|
77
|
+
- Landing page redesigned with tabbed code examples
|
|
78
|
+
- README updated to use `decide` pattern
|
|
79
|
+
- Removed npm install section (package not yet published)
|
|
80
|
+
|
|
81
|
+
## [0.1.0] - 2026-02-20
|
|
82
|
+
|
|
83
|
+
### Added
|
|
84
|
+
|
|
85
|
+
- Initial release
|
|
86
|
+
- DCB-inspired Event Store with config-based consistency keys
|
|
87
|
+
- Storage backends: SQLite, PostgreSQL, sql.js (browser), In-Memory
|
|
88
|
+
- Auto-reindex on config change
|
|
89
|
+
- Conflict detection with delta
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sebastian Bortz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
# BoundlessDB
|
|
2
|
+
|
|
3
|
+
A **DCB-inspired** event store library for TypeScript.
|
|
4
|
+
|
|
5
|
+
> *BoundlessDB* — because consistency boundaries should be dynamic, not fixed.
|
|
6
|
+
|
|
7
|
+
## 🎉 Try it Live!
|
|
8
|
+
|
|
9
|
+
**[Interactive Browser Demo](https://boundlessdb.dev/demo.html)** — No installation required!
|
|
10
|
+
|
|
11
|
+
The entire event store runs client-side in your browser using WebAssembly SQLite.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- 🚀 **Works in Browser** — Full client-side event sourcing via sql.js (WASM)
|
|
16
|
+
- 🔑 **No Streams** — Events organized via configurable consistency keys
|
|
17
|
+
- ⚙️ **Config-based Key Extraction** — Events remain pure business data
|
|
18
|
+
- 🎟️ **AppendCondition** — Simple, transparent optimistic concurrency control
|
|
19
|
+
- ⚡ **Conflict Detection with Delta** — Get exactly what changed since your read
|
|
20
|
+
- 🔄 **Auto-Reindex** — Change your config, keys are automatically rebuilt
|
|
21
|
+
- 💾 **SQLite, PostgreSQL & In-Memory** — Multiple storage backends
|
|
22
|
+
- 📦 **Embedded Library** — No separate server, runs in your process
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { createEventStore, SqliteStorage } from 'boundlessdb';
|
|
28
|
+
|
|
29
|
+
const store = createEventStore({
|
|
30
|
+
storage: new SqliteStorage(':memory:'),
|
|
31
|
+
consistency: {
|
|
32
|
+
eventTypes: {
|
|
33
|
+
CourseCreated: {
|
|
34
|
+
keys: [{ name: 'course', path: 'data.courseId' }]
|
|
35
|
+
},
|
|
36
|
+
StudentSubscribed: {
|
|
37
|
+
keys: [
|
|
38
|
+
{ name: 'course', path: 'data.courseId' },
|
|
39
|
+
{ name: 'student', path: 'data.studentId' }
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## How It Works
|
|
48
|
+
|
|
49
|
+
### 1️⃣ Event Appended
|
|
50
|
+
You append an event with business data:
|
|
51
|
+
```typescript
|
|
52
|
+
await store.append([{
|
|
53
|
+
type: 'StudentSubscribed',
|
|
54
|
+
data: { courseId: 'cs101', studentId: 'alice' }
|
|
55
|
+
}], result.appendCondition);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 2️⃣ Keys Extracted
|
|
59
|
+
Your config tells BoundlessDB which fields are consistency keys:
|
|
60
|
+
```typescript
|
|
61
|
+
consistency: {
|
|
62
|
+
eventTypes: {
|
|
63
|
+
StudentSubscribed: {
|
|
64
|
+
keys: [
|
|
65
|
+
{ name: 'course', path: 'data.courseId' },
|
|
66
|
+
{ name: 'student', path: 'data.studentId' }
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// → Extracts: course='cs101', student='alice'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 3️⃣ Index Updated
|
|
75
|
+
Keys are stored in a separate index table, linked to the event position:
|
|
76
|
+
```
|
|
77
|
+
event_keys: [pos:1, course, cs101], [pos:1, student, alice]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 4️⃣ Query by Keys
|
|
81
|
+
Find all events matching any combination of key conditions:
|
|
82
|
+
```typescript
|
|
83
|
+
const result = await store.read({
|
|
84
|
+
conditions: [
|
|
85
|
+
{ type: 'StudentSubscribed', key: 'course', value: 'cs101' }
|
|
86
|
+
]
|
|
87
|
+
});
|
|
88
|
+
// result.appendCondition captures: "I read all matching events up to position X"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## The DCB Pattern: Read → Decide → Write
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// 1️⃣ READ — Query events and get an appendCondition
|
|
95
|
+
const { events, appendCondition } = await store.read({
|
|
96
|
+
conditions: [
|
|
97
|
+
{ type: 'CourseCreated', key: 'course', value: 'cs101' },
|
|
98
|
+
{ type: 'StudentSubscribed', key: 'course', value: 'cs101' },
|
|
99
|
+
]
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// 2️⃣ DECIDE — Build state, run business logic
|
|
103
|
+
const state = events.reduce(evolve, initialState);
|
|
104
|
+
const newEvents = decide(command, state);
|
|
105
|
+
|
|
106
|
+
// 3️⃣ WRITE — Append with optimistic concurrency
|
|
107
|
+
const result = await store.append(newEvents, appendCondition);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Define Your Functions
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
const initialState = { enrolled: 0, capacity: 30 };
|
|
114
|
+
|
|
115
|
+
// evolve: (state, event) → new state
|
|
116
|
+
const evolve = (state, event) => {
|
|
117
|
+
switch (event.type) {
|
|
118
|
+
case 'StudentSubscribed':
|
|
119
|
+
return { ...state, enrolled: state.enrolled + 1 };
|
|
120
|
+
default:
|
|
121
|
+
return state;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// decide: (command, state) → events[]
|
|
126
|
+
const decide = (command, state) => {
|
|
127
|
+
if (state.enrolled >= state.capacity) {
|
|
128
|
+
throw new Error('Course is full!');
|
|
129
|
+
}
|
|
130
|
+
return [{ type: 'StudentSubscribed', data: command }];
|
|
131
|
+
};
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Handle Conflicts
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
if (result.conflict) {
|
|
138
|
+
// Someone else wrote while you were deciding
|
|
139
|
+
console.log('Events since your read:', result.conflictingEvents);
|
|
140
|
+
// Retry with result.appendCondition
|
|
141
|
+
} else {
|
|
142
|
+
console.log('Success at position', result.position);
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Fluent Query API
|
|
147
|
+
|
|
148
|
+
Build queries with a chainable API:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
const { events, appendCondition } = await store.query<CourseEvent>()
|
|
152
|
+
.matchType('CourseCreated') // all events of type
|
|
153
|
+
.matchKey('StudentSubscribed', 'course', 'cs101') // where key = value
|
|
154
|
+
.fromPosition(100n) // start from position
|
|
155
|
+
.limit(50) // limit results
|
|
156
|
+
.read();
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Methods
|
|
160
|
+
|
|
161
|
+
| Method | Description |
|
|
162
|
+
|--------|-------------|
|
|
163
|
+
| `matchType(type)` | Match all events of type (unconstrained) |
|
|
164
|
+
| `matchKey(type, key, value)` | Match events where key equals value (constrained) |
|
|
165
|
+
| `fromPosition(bigint)` | Start reading from position |
|
|
166
|
+
| `limit(number)` | Limit number of results |
|
|
167
|
+
| `read()` | Execute query, returns `QueryResult` |
|
|
168
|
+
|
|
169
|
+
The fluent API is equivalent to calling `store.read()` with conditions — use whichever style you prefer.
|
|
170
|
+
|
|
171
|
+
## AppendCondition
|
|
172
|
+
|
|
173
|
+
When you call `read()`, the result contains an `appendCondition` with:
|
|
174
|
+
- The **position** up to which events were read
|
|
175
|
+
- The **query conditions** you used
|
|
176
|
+
|
|
177
|
+
This is a simple, transparent object — no encoding, no magic:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// Option 1: Use appendCondition from read()
|
|
181
|
+
const result = await store.read({ conditions });
|
|
182
|
+
await store.append(newEvents, result.appendCondition);
|
|
183
|
+
|
|
184
|
+
// Option 2: Create conditions manually
|
|
185
|
+
await store.append(newEvents, {
|
|
186
|
+
position: 42n,
|
|
187
|
+
conditions: [{ type: 'UserCreated', key: 'username', value: 'alice' }]
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Option 3: Skip consistency check entirely
|
|
191
|
+
await store.append(newEvents, null);
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
This flexibility lets you:
|
|
195
|
+
- **Create uniqueness checks without reading first** (e.g., "username must be unique")
|
|
196
|
+
- **Build custom retry logic** by constructing conditions manually
|
|
197
|
+
- **Optimize performance** by skipping unnecessary reads
|
|
198
|
+
|
|
199
|
+
## Query Across Multiple Dimensions
|
|
200
|
+
|
|
201
|
+
Traditional streams give you ONE boundary. DCB lets you query ANY combination:
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// "Has Alice already enrolled in CS101?"
|
|
205
|
+
const result = await store.read({
|
|
206
|
+
conditions: [
|
|
207
|
+
{ type: 'StudentSubscribed', key: 'course', value: 'cs101' },
|
|
208
|
+
{ type: 'StudentSubscribed', key: 'student', value: 'alice' },
|
|
209
|
+
]
|
|
210
|
+
});
|
|
211
|
+
// Checks BOTH course AND student boundaries in one query!
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Config-based Key Extraction
|
|
215
|
+
|
|
216
|
+
Keys are extracted from event payloads via configuration — events stay pure:
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
const consistency = {
|
|
220
|
+
eventTypes: {
|
|
221
|
+
OrderPlaced: {
|
|
222
|
+
keys: [
|
|
223
|
+
{ name: 'order', path: 'data.orderId' },
|
|
224
|
+
{ name: 'customer', path: 'data.customer.id' },
|
|
225
|
+
{ name: 'month', path: 'data.timestamp', transform: 'MONTH' }
|
|
226
|
+
]
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Key Options
|
|
233
|
+
|
|
234
|
+
| Option | Description |
|
|
235
|
+
|--------|-------------|
|
|
236
|
+
| `name` | Key name for queries |
|
|
237
|
+
| `path` | Dot-notation path in event (e.g., `data.customer.id`) |
|
|
238
|
+
| `transform` | Transform the extracted value (see below) |
|
|
239
|
+
| `nullHandling` | `error` (default), `skip`, `default` |
|
|
240
|
+
| `defaultValue` | Value when `nullHandling: 'default'` |
|
|
241
|
+
|
|
242
|
+
### Transforms
|
|
243
|
+
|
|
244
|
+
Transforms modify the extracted value before indexing:
|
|
245
|
+
|
|
246
|
+
| Transform | Input | Output | Use Case |
|
|
247
|
+
|-----------|-------|--------|----------|
|
|
248
|
+
| `LOWER` | `"Alice@Email.COM"` | `"alice@email.com"` | Case-insensitive matching |
|
|
249
|
+
| `UPPER` | `"alice"` | `"ALICE"` | Normalized codes |
|
|
250
|
+
| `MONTH` | `"2026-02-20T14:30:00Z"` | `"2026-02"` | Monthly partitioning |
|
|
251
|
+
| `YEAR` | `"2026-02-20T14:30:00Z"` | `"2026"` | Yearly aggregation |
|
|
252
|
+
| `DATE` | `"2026-02-20T14:30:00Z"` | `"2026-02-20"` | Daily partitioning |
|
|
253
|
+
|
|
254
|
+
**Example: Time-based partitioning**
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
const consistency = {
|
|
258
|
+
eventTypes: {
|
|
259
|
+
OrderPlaced: {
|
|
260
|
+
keys: [
|
|
261
|
+
{ name: 'order', path: 'data.orderId' },
|
|
262
|
+
{ name: 'month', path: 'data.placedAt', transform: 'MONTH' }
|
|
263
|
+
]
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// Event: { type: 'OrderPlaced', data: { orderId: 'ORD-123', placedAt: '2026-02-20T14:30:00Z' } }
|
|
269
|
+
// Extracted keys: order="ORD-123", month="2026-02"
|
|
270
|
+
|
|
271
|
+
// Query all orders from February 2026:
|
|
272
|
+
const { events } = await store.read({
|
|
273
|
+
conditions: [{ type: 'OrderPlaced', key: 'month', value: '2026-02' }]
|
|
274
|
+
});
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
This is great for **Close the Books** patterns — query all events in a time period efficiently!
|
|
278
|
+
|
|
279
|
+
## Auto-Reindex on Config Change
|
|
280
|
+
|
|
281
|
+
The config is hashed and stored in the database. On startup:
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
stored_hash: "a1b2c3..." (from last run)
|
|
285
|
+
current_hash: "x9y8z7..." (from your config)
|
|
286
|
+
|
|
287
|
+
→ Hash mismatch detected!
|
|
288
|
+
→ Rebuilding key index...
|
|
289
|
+
→ ✅ Reindex complete: 1523 events, 4211 keys (847ms)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**Just change your config and restart.** No manual migration needed!
|
|
293
|
+
|
|
294
|
+
## Browser Usage
|
|
295
|
+
|
|
296
|
+
BoundlessDB works **entirely in the browser** with no server required:
|
|
297
|
+
|
|
298
|
+
```html
|
|
299
|
+
<script type="module">
|
|
300
|
+
import { createEventStore, SqlJsStorage } from './boundless.browser.js';
|
|
301
|
+
|
|
302
|
+
const store = createEventStore({
|
|
303
|
+
storage: new SqlJsStorage(),
|
|
304
|
+
consistency: {
|
|
305
|
+
eventTypes: {
|
|
306
|
+
TodoAdded: { keys: [{ name: 'list', path: 'data.listId' }] }
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Everything runs client-side!
|
|
312
|
+
</script>
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Build Browser Bundle
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
npm run build:browser
|
|
319
|
+
# → ui/public/boundless.browser.js (~100KB)
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Storage Backends
|
|
323
|
+
|
|
324
|
+
| Backend | Environment | Persistence |
|
|
325
|
+
|---------|-------------|-------------|
|
|
326
|
+
| `SqliteStorage` | Node.js | File or `:memory:` |
|
|
327
|
+
| `SqlJsStorage` | Browser | In-memory (WASM) |
|
|
328
|
+
| `PostgresStorage` | Node.js | PostgreSQL database |
|
|
329
|
+
| `InMemoryStorage` | Any | None (testing) |
|
|
330
|
+
|
|
331
|
+
### PostgreSQL Storage
|
|
332
|
+
|
|
333
|
+
For production deployments with PostgreSQL:
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
import { createEventStore, PostgresStorage } from 'boundlessdb';
|
|
337
|
+
|
|
338
|
+
const storage = new PostgresStorage('postgresql://user:pass@localhost/mydb');
|
|
339
|
+
await storage.init(); // Required: creates tables if they don't exist
|
|
340
|
+
|
|
341
|
+
const store = createEventStore({
|
|
342
|
+
storage,
|
|
343
|
+
consistency: { /* ... */ }
|
|
344
|
+
});
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**Note:** PostgreSQL support requires the `pg` package:
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
npm install pg
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Typed Events
|
|
354
|
+
|
|
355
|
+
Define type-safe events using the `Event` marker type:
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
import { Event, EventStore } from 'boundlessdb';
|
|
359
|
+
|
|
360
|
+
// Define your events
|
|
361
|
+
type ProductItemAdded = Event<'ProductItemAdded', {
|
|
362
|
+
cartId: string;
|
|
363
|
+
productId: string;
|
|
364
|
+
quantity: number;
|
|
365
|
+
}>;
|
|
366
|
+
|
|
367
|
+
type ProductItemRemoved = Event<'ProductItemRemoved', {
|
|
368
|
+
cartId: string;
|
|
369
|
+
productId: string;
|
|
370
|
+
}>;
|
|
371
|
+
|
|
372
|
+
// Create a union type for all cart events
|
|
373
|
+
type CartEvents = ProductItemAdded | ProductItemRemoved;
|
|
374
|
+
|
|
375
|
+
// Read with type safety
|
|
376
|
+
const result = await store.read<CartEvents>({
|
|
377
|
+
conditions: [{ type: 'ProductItemAdded', key: 'cart', value: 'cart-123' }]
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// TypeScript knows the event types!
|
|
381
|
+
for (const event of result.events) {
|
|
382
|
+
if (event.type === 'ProductItemAdded') {
|
|
383
|
+
console.log(event.data.quantity); // ✅ typed as number
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Query Conditions
|
|
389
|
+
|
|
390
|
+
Query conditions support both **constrained** (with key/value) and **unconstrained** (type-only) queries.
|
|
391
|
+
|
|
392
|
+
### Constrained Query
|
|
393
|
+
Match events of a type where a specific key has a specific value:
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
// Get ProductItemAdded events where cart='cart-123'
|
|
397
|
+
const result = await store.read({
|
|
398
|
+
conditions: [
|
|
399
|
+
{ type: 'ProductItemAdded', key: 'cart', value: 'cart-123' }
|
|
400
|
+
]
|
|
401
|
+
});
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Unconstrained Query
|
|
405
|
+
Omit `key` and `value` to match **all events of a type**:
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
// Get ALL ProductItemAdded events (regardless of cart)
|
|
409
|
+
const result = await store.read({
|
|
410
|
+
conditions: [
|
|
411
|
+
{ type: 'ProductItemAdded' } // no key/value = match all
|
|
412
|
+
]
|
|
413
|
+
});
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### Mixed Conditions
|
|
417
|
+
Combine constrained and unconstrained in one query (OR logic):
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
// "Give me the course definition + all enrollments for cs101"
|
|
421
|
+
const result = await store.read({
|
|
422
|
+
conditions: [
|
|
423
|
+
{ type: 'CourseCreated', key: 'course', value: 'cs101' },
|
|
424
|
+
{ type: 'StudentSubscribed', key: 'course', value: 'cs101' },
|
|
425
|
+
{ type: 'StudentUnsubscribed', key: 'course', value: 'cs101' },
|
|
426
|
+
]
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// "All courses + only Alice's enrollments"
|
|
430
|
+
const result = await store.read({
|
|
431
|
+
conditions: [
|
|
432
|
+
{ type: 'CourseCreated' }, // unconstrained: ALL courses
|
|
433
|
+
{ type: 'StudentSubscribed', key: 'student', value: 'alice' } // constrained: only Alice
|
|
434
|
+
]
|
|
435
|
+
});
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Same Type, Multiple Values
|
|
439
|
+
Query multiple values of the same key:
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
// Get ProductItemAdded for cart-1 OR cart-2
|
|
443
|
+
const result = await store.read({
|
|
444
|
+
conditions: [
|
|
445
|
+
{ type: 'ProductItemAdded', key: 'cart', value: 'cart-1' },
|
|
446
|
+
{ type: 'ProductItemAdded', key: 'cart', value: 'cart-2' }
|
|
447
|
+
]
|
|
448
|
+
});
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### Type Safety
|
|
452
|
+
With TypeScript, conditions are type-safe — you must provide either:
|
|
453
|
+
- **Only `type`** (unconstrained), or
|
|
454
|
+
- **`type` + `key` + `value`** (constrained)
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
// ✅ Valid
|
|
458
|
+
{ type: 'ProductItemAdded' }
|
|
459
|
+
{ type: 'ProductItemAdded', key: 'cart', value: 'cart-123' }
|
|
460
|
+
|
|
461
|
+
// ❌ TypeScript Error — key without value not allowed
|
|
462
|
+
{ type: 'ProductItemAdded', key: 'cart' }
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Empty Conditions
|
|
466
|
+
Empty conditions returns **all events** in the store:
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
// Get ALL events (useful for admin/debug/export)
|
|
470
|
+
const result = await store.read({ conditions: [] });
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
## API Reference
|
|
474
|
+
|
|
475
|
+
### `createEventStore(options)`
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
const store = createEventStore({
|
|
479
|
+
storage: SqliteStorage | SqlJsStorage | PostgresStorage | InMemoryStorage,
|
|
480
|
+
consistency: ConsistencyConfig, // Key extraction rules
|
|
481
|
+
});
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### `store.read<E>(query)`
|
|
485
|
+
|
|
486
|
+
```typescript
|
|
487
|
+
const result = await store.read<CartEvents>({
|
|
488
|
+
conditions: [{ type, key?, value? }],
|
|
489
|
+
fromPosition?: bigint,
|
|
490
|
+
limit?: number,
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
result.events // StoredEvent<E>[]
|
|
494
|
+
result.position // bigint
|
|
495
|
+
result.conditions // QueryCondition[]
|
|
496
|
+
result.appendCondition // AppendCondition (for store.append)
|
|
497
|
+
result.count // number
|
|
498
|
+
result.isEmpty() // boolean
|
|
499
|
+
result.first() // StoredEvent<E> | undefined
|
|
500
|
+
result.last() // StoredEvent<E> | undefined
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### `store.append<E>(events, condition)`
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
// With appendCondition from read()
|
|
507
|
+
const readResult = await store.read<CartEvents>({ conditions });
|
|
508
|
+
const result = await store.append<CartEvents>([newEvent], readResult.appendCondition);
|
|
509
|
+
|
|
510
|
+
// With manual AppendCondition
|
|
511
|
+
const result = await store.append<CartEvents>([newEvent], {
|
|
512
|
+
position: 42n,
|
|
513
|
+
conditions: [{ type: 'UserCreated', key: 'username', value: 'alice' }]
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// Without consistency check
|
|
517
|
+
const result = await store.append<CartEvents>([newEvent], null);
|
|
518
|
+
|
|
519
|
+
// Result handling
|
|
520
|
+
if (result.conflict) {
|
|
521
|
+
result.conflictingEvents; // StoredEvent<E>[] - what changed since your read
|
|
522
|
+
result.appendCondition; // Fresh condition for retry
|
|
523
|
+
} else {
|
|
524
|
+
result.position; // Position of last appended event
|
|
525
|
+
result.appendCondition; // Condition for next operation
|
|
526
|
+
}
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
## Development
|
|
530
|
+
|
|
531
|
+
```bash
|
|
532
|
+
npm install
|
|
533
|
+
npm test
|
|
534
|
+
npm run build
|
|
535
|
+
npm run build:browser
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
## Related
|
|
539
|
+
|
|
540
|
+
- [dcb.events](https://dcb.events) — Dynamic Consistency Boundaries
|
|
541
|
+
- [Giraflow](https://giraflow.dev) — Event Modeling visualization
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
545
|
+
Built with ❤️ for [Event Sourcing](https://www.eventstore.com/event-sourcing)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser shim for better-sqlite3
|
|
3
|
+
*
|
|
4
|
+
* This module throws a helpful error if someone tries to use SqliteStorage in the browser.
|
|
5
|
+
* Use SqlJsStorage instead for browser environments.
|
|
6
|
+
*/
|
|
7
|
+
declare class BrowserNotSupportedError extends Error {
|
|
8
|
+
constructor();
|
|
9
|
+
}
|
|
10
|
+
export default function Database(_path?: string): void;
|
|
11
|
+
export { BrowserNotSupportedError };
|
|
12
|
+
//# sourceMappingURL=better-sqlite3-shim.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"better-sqlite3-shim.d.ts","sourceRoot":"","sources":["../src/better-sqlite3-shim.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,cAAM,wBAAyB,SAAQ,KAAK;;CAQ3C;AAED,MAAM,CAAC,OAAO,UAAU,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,QAE9C;AAED,OAAO,EAAE,wBAAwB,EAAE,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser shim for better-sqlite3
|
|
3
|
+
*
|
|
4
|
+
* This module throws a helpful error if someone tries to use SqliteStorage in the browser.
|
|
5
|
+
* Use SqlJsStorage instead for browser environments.
|
|
6
|
+
*/
|
|
7
|
+
class BrowserNotSupportedError extends Error {
|
|
8
|
+
constructor() {
|
|
9
|
+
super('better-sqlite3 is not available in browser environments. ' +
|
|
10
|
+
'Use SqlJsStorage instead for browser-based event storage.');
|
|
11
|
+
this.name = 'BrowserNotSupportedError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export default function Database(_path) {
|
|
15
|
+
throw new BrowserNotSupportedError();
|
|
16
|
+
}
|
|
17
|
+
export { BrowserNotSupportedError };
|
|
18
|
+
//# sourceMappingURL=better-sqlite3-shim.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"better-sqlite3-shim.js","sourceRoot":"","sources":["../src/better-sqlite3-shim.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,wBAAyB,SAAQ,KAAK;IAC1C;QACE,KAAK,CACH,2DAA2D;YAC3D,2DAA2D,CAC5D,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAC;IACzC,CAAC;CACF;AAED,MAAM,CAAC,OAAO,UAAU,QAAQ,CAAC,KAAc;IAC7C,MAAM,IAAI,wBAAwB,EAAE,CAAC;AACvC,CAAC;AAED,OAAO,EAAE,wBAAwB,EAAE,CAAC"}
|