@emmett-community/emmett-google-firestore 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/LICENSE +21 -0
- package/README.md +579 -0
- package/dist/index.d.mts +85 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.js +242 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +222 -0
- package/dist/index.mjs.map +1 -0
- package/dist/testing/index.d.mts +115 -0
- package/dist/testing/index.d.ts +115 -0
- package/dist/testing/index.js +296 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/index.mjs +289 -0
- package/dist/testing/index.mjs.map +1 -0
- package/dist/types-CHnx_sMk.d.mts +122 -0
- package/dist/types-CHnx_sMk.d.ts +122 -0
- package/package.json +96 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Emmett Community
|
|
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,579 @@
|
|
|
1
|
+
# @emmett-community/emmett-google-firestore
|
|
2
|
+
|
|
3
|
+
Google Firestore event store implementation for [Emmett](https://event-driven-io.github.io/emmett/), the Node.js event sourcing framework.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@emmett-community/emmett-google-firestore)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- ✅ **Event Storage & Retrieval** - Store and read events from Google Firestore
|
|
11
|
+
- ✅ **Optimistic Concurrency** - Built-in version conflict detection
|
|
12
|
+
- ✅ **Type-Safe** - Full TypeScript support with comprehensive types
|
|
13
|
+
- ✅ **Minimal Boilerplate** - Simple, intuitive API
|
|
14
|
+
- ✅ **Subcollection-based** - Efficient Firestore-native structure (no size limits!)
|
|
15
|
+
- ✅ **Global Event Ordering** - Maintain total ordering across all streams
|
|
16
|
+
- ✅ **Testing Utilities** - Helper functions for easy testing
|
|
17
|
+
- ✅ **Emmett Compatible** - Works seamlessly with the Emmett ecosystem
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @emmett-community/emmett-google-firestore @google-cloud/firestore
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { Firestore } from '@google-cloud/firestore';
|
|
29
|
+
import { getFirestoreEventStore } from '@emmett-community/emmett-google-firestore';
|
|
30
|
+
|
|
31
|
+
// Initialize Firestore
|
|
32
|
+
const firestore = new Firestore({
|
|
33
|
+
projectId: 'your-project-id',
|
|
34
|
+
keyFilename: 'path/to/service-account.json',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Create event store
|
|
38
|
+
const eventStore = getFirestoreEventStore(firestore);
|
|
39
|
+
|
|
40
|
+
// Define your events
|
|
41
|
+
type UserRegistered = Event<'UserRegistered', { userId: string; email: string }>;
|
|
42
|
+
type UserEvent = UserRegistered | /* other events */;
|
|
43
|
+
|
|
44
|
+
// Append events
|
|
45
|
+
await eventStore.appendToStream('User-123', [
|
|
46
|
+
{
|
|
47
|
+
type: 'UserRegistered',
|
|
48
|
+
data: { userId: '123', email: 'user@example.com' },
|
|
49
|
+
},
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
// Read events
|
|
53
|
+
const events = await eventStore.readStream<UserEvent>('User-123');
|
|
54
|
+
|
|
55
|
+
// Aggregate state
|
|
56
|
+
const state = await eventStore.aggregateStream(
|
|
57
|
+
'User-123',
|
|
58
|
+
evolve,
|
|
59
|
+
initialState,
|
|
60
|
+
);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## How It Works
|
|
64
|
+
|
|
65
|
+
### Firestore Structure
|
|
66
|
+
|
|
67
|
+
Events are stored using a **subcollection pattern** for optimal performance:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
/streams/ # Root collection
|
|
71
|
+
{streamName}/ # Stream document (metadata)
|
|
72
|
+
version: number
|
|
73
|
+
createdAt: Timestamp
|
|
74
|
+
updatedAt: Timestamp
|
|
75
|
+
|
|
76
|
+
/events/ # Subcollection (actual events)
|
|
77
|
+
0000000000: { type, data, ... } # Zero-padded version IDs
|
|
78
|
+
0000000001: { type, data, ... }
|
|
79
|
+
0000000002: { type, data, ... }
|
|
80
|
+
|
|
81
|
+
/_counters/ # System collection
|
|
82
|
+
global_position/
|
|
83
|
+
value: number
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Benefits of this structure:**
|
|
87
|
+
|
|
88
|
+
- ✅ No document size limits (Firestore 1MB limit doesn't apply to subcollections)
|
|
89
|
+
- ✅ Natural isolation per stream
|
|
90
|
+
- ✅ Automatic ordering (document IDs sort naturally)
|
|
91
|
+
- ✅ No composite indexes needed
|
|
92
|
+
- ✅ Efficient queries
|
|
93
|
+
|
|
94
|
+
### Optimistic Concurrency
|
|
95
|
+
|
|
96
|
+
The event store uses **optimistic locking** to prevent conflicts:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// Append with version check
|
|
100
|
+
await eventStore.appendToStream(
|
|
101
|
+
'User-123',
|
|
102
|
+
events,
|
|
103
|
+
{ expectedStreamVersion: 5 } // Will fail if version ≠ 5
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Or use special version markers
|
|
107
|
+
import { NO_STREAM, STREAM_EXISTS, ANY } from '@emmett-community/emmett-google-firestore';
|
|
108
|
+
|
|
109
|
+
// Stream must not exist
|
|
110
|
+
await eventStore.appendToStream('User-123', events, {
|
|
111
|
+
expectedStreamVersion: NO_STREAM
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Stream must exist (any version)
|
|
115
|
+
await eventStore.appendToStream('User-123', events, {
|
|
116
|
+
expectedStreamVersion: STREAM_EXISTS
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// No version check
|
|
120
|
+
await eventStore.appendToStream('User-123', events, {
|
|
121
|
+
expectedStreamVersion: ANY
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## API Reference
|
|
126
|
+
|
|
127
|
+
### `getFirestoreEventStore(firestore, options?)`
|
|
128
|
+
|
|
129
|
+
Creates a Firestore event store instance.
|
|
130
|
+
|
|
131
|
+
**Parameters:**
|
|
132
|
+
|
|
133
|
+
- `firestore`: Firestore instance
|
|
134
|
+
- `options`: Optional configuration
|
|
135
|
+
- `collections`: Custom collection names
|
|
136
|
+
- `streams`: Stream collection name (default: `"streams"`)
|
|
137
|
+
- `counters`: Counter collection name (default: `"_counters"`)
|
|
138
|
+
|
|
139
|
+
**Returns:** `FirestoreEventStore`
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
const eventStore = getFirestoreEventStore(firestore, {
|
|
143
|
+
collections: {
|
|
144
|
+
streams: 'my_streams',
|
|
145
|
+
counters: 'my_counters',
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### `eventStore.appendToStream(streamName, events, options?)`
|
|
151
|
+
|
|
152
|
+
Appends events to a stream.
|
|
153
|
+
|
|
154
|
+
**Parameters:**
|
|
155
|
+
|
|
156
|
+
- `streamName`: Stream identifier (e.g., `"User-123"`)
|
|
157
|
+
- `events`: Array of events to append
|
|
158
|
+
- `options`: Optional append options
|
|
159
|
+
- `expectedStreamVersion`: Version constraint
|
|
160
|
+
|
|
161
|
+
**Returns:** `Promise<AppendToStreamResult>`
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
const result = await eventStore.appendToStream(
|
|
165
|
+
'User-123',
|
|
166
|
+
[{ type: 'UserRegistered', data: {...} }],
|
|
167
|
+
{ expectedStreamVersion: 0 }
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
console.log(result.nextExpectedStreamVersion); // 1
|
|
171
|
+
console.log(result.createdNewStream); // true/false
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### `eventStore.readStream(streamName, options?)`
|
|
175
|
+
|
|
176
|
+
Reads events from a stream.
|
|
177
|
+
|
|
178
|
+
**Parameters:**
|
|
179
|
+
|
|
180
|
+
- `streamName`: Stream identifier
|
|
181
|
+
- `options`: Optional read options
|
|
182
|
+
- `from`: Start version (inclusive)
|
|
183
|
+
- `to`: End version (inclusive)
|
|
184
|
+
- `maxCount`: Maximum number of events to read
|
|
185
|
+
|
|
186
|
+
**Returns:** `Promise<FirestoreReadEvent[]>`
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
// Read all events
|
|
190
|
+
const events = await eventStore.readStream('User-123');
|
|
191
|
+
|
|
192
|
+
// Read from version 10 onwards
|
|
193
|
+
const events = await eventStore.readStream('User-123', { from: 10n });
|
|
194
|
+
|
|
195
|
+
// Read range
|
|
196
|
+
const events = await eventStore.readStream('User-123', {
|
|
197
|
+
from: 5n,
|
|
198
|
+
to: 10n
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Limit results
|
|
202
|
+
const events = await eventStore.readStream('User-123', {
|
|
203
|
+
maxCount: 100
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### `eventStore.aggregateStream(streamName, evolve, initialState, options?)`
|
|
208
|
+
|
|
209
|
+
Aggregates stream events into state.
|
|
210
|
+
|
|
211
|
+
**Parameters:**
|
|
212
|
+
|
|
213
|
+
- `streamName`: Stream identifier
|
|
214
|
+
- `evolve`: Function to apply events to state
|
|
215
|
+
- `initialState`: Function returning initial state
|
|
216
|
+
- `options`: Optional read options (same as `readStream`)
|
|
217
|
+
|
|
218
|
+
**Returns:** `Promise<State>`
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
const state = await eventStore.aggregateStream(
|
|
222
|
+
'User-123',
|
|
223
|
+
(state, event) => {
|
|
224
|
+
switch (event.type) {
|
|
225
|
+
case 'UserRegistered':
|
|
226
|
+
return { ...state, ...event.data };
|
|
227
|
+
default:
|
|
228
|
+
return state;
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
() => ({ status: 'empty' }),
|
|
232
|
+
);
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Testing
|
|
236
|
+
|
|
237
|
+
### Testing Utilities
|
|
238
|
+
|
|
239
|
+
The package includes utilities to make testing easier:
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
import {
|
|
243
|
+
setupFirestoreTests,
|
|
244
|
+
getTestFirestore,
|
|
245
|
+
clearFirestore,
|
|
246
|
+
} from '@emmett-community/emmett-google-firestore/testing';
|
|
247
|
+
|
|
248
|
+
describe('My Tests', () => {
|
|
249
|
+
const { firestore, eventStore, cleanup, clearData } = setupFirestoreTests();
|
|
250
|
+
|
|
251
|
+
afterAll(cleanup);
|
|
252
|
+
beforeEach(clearData);
|
|
253
|
+
|
|
254
|
+
it('should work', async () => {
|
|
255
|
+
await eventStore.appendToStream('test-stream', [/* events */]);
|
|
256
|
+
// ... assertions
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Running Tests
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
# Unit tests
|
|
265
|
+
npm run test:unit
|
|
266
|
+
|
|
267
|
+
# Integration tests (requires Firestore Emulator)
|
|
268
|
+
npm run test:integration
|
|
269
|
+
|
|
270
|
+
# All tests
|
|
271
|
+
npm test
|
|
272
|
+
|
|
273
|
+
# Coverage
|
|
274
|
+
npm run test:coverage
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Using Firestore Emulator
|
|
278
|
+
|
|
279
|
+
For local development and testing:
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
# Install Firebase CLI
|
|
283
|
+
npm install -g firebase-tools
|
|
284
|
+
|
|
285
|
+
# Start emulator
|
|
286
|
+
firebase emulators:start --only firestore
|
|
287
|
+
|
|
288
|
+
# Or use the provided script
|
|
289
|
+
./scripts/start-emulator.sh
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Set environment variables:
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
export FIRESTORE_PROJECT_ID=test-project
|
|
296
|
+
export FIRESTORE_EMULATOR_HOST=localhost:8080
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Examples
|
|
300
|
+
|
|
301
|
+
### Complete Shopping Cart Example
|
|
302
|
+
|
|
303
|
+
See [examples/shopping-cart](./examples/shopping-cart) for a full application including:
|
|
304
|
+
|
|
305
|
+
- Event-sourced shopping cart
|
|
306
|
+
- Express.js API with OpenAPI spec
|
|
307
|
+
- Docker Compose setup
|
|
308
|
+
- Unit, integration, and E2E tests
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
cd examples/shopping-cart
|
|
312
|
+
docker-compose up
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Basic Usage Example
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
import { Firestore } from '@google-cloud/firestore';
|
|
319
|
+
import { getFirestoreEventStore } from '@emmett-community/emmett-google-firestore';
|
|
320
|
+
import type { Event } from '@event-driven-io/emmett';
|
|
321
|
+
|
|
322
|
+
// Define events
|
|
323
|
+
type AccountOpened = Event<'AccountOpened', {
|
|
324
|
+
accountId: string;
|
|
325
|
+
initialBalance: number;
|
|
326
|
+
}>;
|
|
327
|
+
|
|
328
|
+
type MoneyDeposited = Event<'MoneyDeposited', {
|
|
329
|
+
accountId: string;
|
|
330
|
+
amount: number;
|
|
331
|
+
}>;
|
|
332
|
+
|
|
333
|
+
type BankAccountEvent = AccountOpened | MoneyDeposited;
|
|
334
|
+
|
|
335
|
+
// Define state
|
|
336
|
+
type BankAccount = {
|
|
337
|
+
accountId: string;
|
|
338
|
+
balance: number;
|
|
339
|
+
status: 'open' | 'closed';
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// Evolve function
|
|
343
|
+
const evolve = (state: BankAccount, event: BankAccountEvent): BankAccount => {
|
|
344
|
+
switch (event.type) {
|
|
345
|
+
case 'AccountOpened':
|
|
346
|
+
return {
|
|
347
|
+
accountId: event.data.accountId,
|
|
348
|
+
balance: event.data.initialBalance,
|
|
349
|
+
status: 'open',
|
|
350
|
+
};
|
|
351
|
+
case 'MoneyDeposited':
|
|
352
|
+
return {
|
|
353
|
+
...state,
|
|
354
|
+
balance: state.balance + event.data.amount,
|
|
355
|
+
};
|
|
356
|
+
default:
|
|
357
|
+
return state;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const initialState = (): BankAccount => ({
|
|
362
|
+
accountId: '',
|
|
363
|
+
balance: 0,
|
|
364
|
+
status: 'closed',
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// Usage
|
|
368
|
+
const firestore = new Firestore({ projectId: 'my-project' });
|
|
369
|
+
const eventStore = getFirestoreEventStore(firestore);
|
|
370
|
+
|
|
371
|
+
// Open account
|
|
372
|
+
await eventStore.appendToStream('BankAccount-123', [
|
|
373
|
+
{
|
|
374
|
+
type: 'AccountOpened',
|
|
375
|
+
data: { accountId: '123', initialBalance: 100 }
|
|
376
|
+
},
|
|
377
|
+
]);
|
|
378
|
+
|
|
379
|
+
// Deposit money
|
|
380
|
+
await eventStore.appendToStream('BankAccount-123', [
|
|
381
|
+
{
|
|
382
|
+
type: 'MoneyDeposited',
|
|
383
|
+
data: { accountId: '123', amount: 50 }
|
|
384
|
+
},
|
|
385
|
+
]);
|
|
386
|
+
|
|
387
|
+
// Get current state
|
|
388
|
+
const account = await eventStore.aggregateStream(
|
|
389
|
+
'BankAccount-123',
|
|
390
|
+
evolve,
|
|
391
|
+
initialState,
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
console.log(account.balance); // 150
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
## Configuration
|
|
398
|
+
|
|
399
|
+
### Custom Collection Names
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
const eventStore = getFirestoreEventStore(firestore, {
|
|
403
|
+
collections: {
|
|
404
|
+
streams: 'app_streams',
|
|
405
|
+
counters: 'app_counters',
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Firestore Emulator (Development)
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
const firestore = new Firestore({
|
|
414
|
+
projectId: 'demo-project',
|
|
415
|
+
host: 'localhost:8080',
|
|
416
|
+
ssl: false,
|
|
417
|
+
});
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Production Configuration
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
const firestore = new Firestore({
|
|
424
|
+
projectId: process.env.GCP_PROJECT_ID,
|
|
425
|
+
keyFilename: process.env.GCP_KEY_FILE,
|
|
426
|
+
// Optional: specify database
|
|
427
|
+
databaseId: '(default)',
|
|
428
|
+
});
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
## Architecture
|
|
432
|
+
|
|
433
|
+
### Event Sourcing Pattern
|
|
434
|
+
|
|
435
|
+
This package implements the **Event Sourcing** pattern:
|
|
436
|
+
|
|
437
|
+
1. **Commands** → Validate and create events
|
|
438
|
+
2. **Events** → Immutable facts that happened
|
|
439
|
+
3. **State** → Rebuilt by replaying events
|
|
440
|
+
|
|
441
|
+
```
|
|
442
|
+
Command → Decide → Events → Append to Firestore → Evolve → State
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### Firestore Transaction Flow
|
|
446
|
+
|
|
447
|
+
When appending events, the following happens atomically:
|
|
448
|
+
|
|
449
|
+
1. Read current stream version
|
|
450
|
+
2. Validate expected version
|
|
451
|
+
3. Increment global position counter
|
|
452
|
+
4. Append events to subcollection
|
|
453
|
+
5. Update stream metadata
|
|
454
|
+
6. Commit transaction
|
|
455
|
+
|
|
456
|
+
If any step fails or versions don't match, the entire transaction is rolled back.
|
|
457
|
+
|
|
458
|
+
## Performance Considerations
|
|
459
|
+
|
|
460
|
+
### Batch Size
|
|
461
|
+
|
|
462
|
+
Firestore transactions are limited to 500 operations. When appending many events:
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
// Good: Small batches
|
|
466
|
+
await eventStore.appendToStream('stream', events.slice(0, 100));
|
|
467
|
+
|
|
468
|
+
// Avoid: Very large batches (>400 events)
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### Query Optimization
|
|
472
|
+
|
|
473
|
+
```typescript
|
|
474
|
+
// Good: Use range queries
|
|
475
|
+
const recent = await eventStore.readStream('stream', {
|
|
476
|
+
from: lastKnownVersion,
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// Good: Limit results
|
|
480
|
+
const events = await eventStore.readStream('stream', {
|
|
481
|
+
maxCount: 100
|
|
482
|
+
});
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### Firestore Costs
|
|
486
|
+
|
|
487
|
+
- **Reads**: Each document read counts (events + metadata)
|
|
488
|
+
- **Writes**: Each event appended counts
|
|
489
|
+
- **Storage**: Charged per GB stored
|
|
490
|
+
|
|
491
|
+
Use the emulator for development to avoid costs!
|
|
492
|
+
|
|
493
|
+
## Error Handling
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
import { ExpectedVersionConflictError } from '@emmett-community/emmett-google-firestore';
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
await eventStore.appendToStream('stream', events, {
|
|
500
|
+
expectedStreamVersion: 5
|
|
501
|
+
});
|
|
502
|
+
} catch (error) {
|
|
503
|
+
if (error instanceof ExpectedVersionConflictError) {
|
|
504
|
+
console.log('Version conflict:', error.expected, 'vs', error.actual);
|
|
505
|
+
// Handle conflict (retry, merge, etc.)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
## TypeScript Support
|
|
511
|
+
|
|
512
|
+
The package is written in TypeScript and includes full type definitions:
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
import type {
|
|
516
|
+
FirestoreEventStore,
|
|
517
|
+
FirestoreReadEvent,
|
|
518
|
+
AppendToStreamOptions,
|
|
519
|
+
ExpectedStreamVersion,
|
|
520
|
+
} from '@emmett-community/emmett-google-firestore';
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
## Compatibility
|
|
524
|
+
|
|
525
|
+
- **Node.js**: >= 18.0.0
|
|
526
|
+
- **Emmett**: ^0.39.0
|
|
527
|
+
- **Firestore**: ^7.10.0
|
|
528
|
+
|
|
529
|
+
## Contributing
|
|
530
|
+
|
|
531
|
+
Contributions are welcome! Please:
|
|
532
|
+
|
|
533
|
+
1. Fork the repository
|
|
534
|
+
2. Create a feature branch
|
|
535
|
+
3. Add tests for new functionality
|
|
536
|
+
4. Ensure all tests pass
|
|
537
|
+
5. Submit a pull request
|
|
538
|
+
|
|
539
|
+
## Development
|
|
540
|
+
|
|
541
|
+
```bash
|
|
542
|
+
# Install dependencies
|
|
543
|
+
npm install
|
|
544
|
+
|
|
545
|
+
# Build
|
|
546
|
+
npm run build
|
|
547
|
+
|
|
548
|
+
# Run tests
|
|
549
|
+
npm test
|
|
550
|
+
|
|
551
|
+
# Lint
|
|
552
|
+
npm run lint
|
|
553
|
+
|
|
554
|
+
# Format
|
|
555
|
+
npm run format
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
## License
|
|
559
|
+
|
|
560
|
+
MIT © Emmett Community
|
|
561
|
+
|
|
562
|
+
## Resources
|
|
563
|
+
|
|
564
|
+
- [Emmett Documentation](https://event-driven-io.github.io/emmett/)
|
|
565
|
+
- [Event Sourcing Guide](https://event-driven.io/en/event_sourcing_basics/)
|
|
566
|
+
- [Google Firestore Docs](https://cloud.google.com/firestore/docs)
|
|
567
|
+
- [GitHub Repository](https://github.com/emmett-community/emmett-google-firestore)
|
|
568
|
+
|
|
569
|
+
## Support
|
|
570
|
+
|
|
571
|
+
- **Issues**: [GitHub Issues](https://github.com/emmett-community/emmett-google-firestore/issues)
|
|
572
|
+
- **Discussions**: [GitHub Discussions](https://github.com/emmett-community/emmett-google-firestore/discussions)
|
|
573
|
+
- **Emmett Discord**: [Join Discord](https://discord.gg/fTpqUTMmVa)
|
|
574
|
+
|
|
575
|
+
## Acknowledgments
|
|
576
|
+
|
|
577
|
+
- Built for the [Emmett](https://event-driven-io.github.io/emmett/) framework by [Oskar Dudycz](https://github.com/oskardudycz)
|
|
578
|
+
- Inspired by [emmett-mongodb](https://github.com/event-driven-io/emmett/tree/main/src/packages/emmett-mongodb)
|
|
579
|
+
- Part of the [Emmett Community](https://github.com/emmett-community)
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Firestore, Timestamp } from '@google-cloud/firestore';
|
|
2
|
+
import { F as FirestoreEventStoreOptions, a as FirestoreEventStore, E as ExpectedStreamVersion } from './types-CHnx_sMk.mjs';
|
|
3
|
+
export { A as AppendToStreamOptions, d as AppendToStreamResult, C as CollectionConfig, e as EventDocument, f as ExpectedVersionConflictError, b as FirestoreReadEvent, c as FirestoreReadEventMetadata, R as ReadStreamOptions, S as StreamMetadata } from './types-CHnx_sMk.mjs';
|
|
4
|
+
import { STREAM_DOES_NOT_EXIST } from '@event-driven-io/emmett';
|
|
5
|
+
export { NO_CONCURRENCY_CHECK, STREAM_DOES_NOT_EXIST, STREAM_EXISTS } from '@event-driven-io/emmett';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Factory function to create a Firestore event store
|
|
9
|
+
*
|
|
10
|
+
* @param firestore - Firestore instance
|
|
11
|
+
* @param options - Optional configuration
|
|
12
|
+
* @returns Firestore event store instance
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { Firestore } from '@google-cloud/firestore';
|
|
17
|
+
* import { getFirestoreEventStore } from '@emmett-community/emmett-google-firestore';
|
|
18
|
+
*
|
|
19
|
+
* const firestore = new Firestore({ projectId: 'my-project' });
|
|
20
|
+
* const eventStore = getFirestoreEventStore(firestore);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
declare function getFirestoreEventStore(firestore: Firestore, options?: FirestoreEventStoreOptions): FirestoreEventStore;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Pad version number with leading zeros for Firestore document IDs
|
|
27
|
+
* This ensures automatic ordering by version in Firestore
|
|
28
|
+
*
|
|
29
|
+
* @param version - The version number to pad
|
|
30
|
+
* @returns Zero-padded string of length 10
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* padVersion(0) // "0000000000"
|
|
34
|
+
* padVersion(42) // "0000000042"
|
|
35
|
+
* padVersion(12345) // "0000012345"
|
|
36
|
+
*/
|
|
37
|
+
declare function padVersion(version: number | bigint): string;
|
|
38
|
+
/**
|
|
39
|
+
* Parse a stream name into type and ID components
|
|
40
|
+
*
|
|
41
|
+
* @param streamName - Stream name in format "Type-id" or "Type-with-dashes-id"
|
|
42
|
+
* @returns Object with streamType and streamId
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* parseStreamName("User-123") // { streamType: "User", streamId: "123" }
|
|
46
|
+
* parseStreamName("ShoppingCart-abc-def-123") // { streamType: "ShoppingCart", streamId: "abc-def-123" }
|
|
47
|
+
*/
|
|
48
|
+
declare function parseStreamName(streamName: string): {
|
|
49
|
+
streamType: string;
|
|
50
|
+
streamId: string;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Convert Firestore Timestamp to JavaScript Date
|
|
54
|
+
*
|
|
55
|
+
* @param timestamp - Firestore Timestamp
|
|
56
|
+
* @returns JavaScript Date object
|
|
57
|
+
*/
|
|
58
|
+
declare function timestampToDate(timestamp: Timestamp): Date;
|
|
59
|
+
/**
|
|
60
|
+
* Validate expected version against current version
|
|
61
|
+
*
|
|
62
|
+
* @param streamName - Stream name for error messages
|
|
63
|
+
* @param expectedVersion - Expected version constraint
|
|
64
|
+
* @param currentVersion - Current stream version (or STREAM_DOES_NOT_EXIST if stream doesn't exist)
|
|
65
|
+
* @throws ExpectedVersionConflictError if versions don't match
|
|
66
|
+
*/
|
|
67
|
+
declare function assertExpectedVersionMatchesCurrent(streamName: string, expectedVersion: ExpectedStreamVersion, currentVersion: bigint | typeof STREAM_DOES_NOT_EXIST): void;
|
|
68
|
+
/**
|
|
69
|
+
* Get the current stream version from metadata
|
|
70
|
+
*
|
|
71
|
+
* @param streamExists - Whether the stream document exists
|
|
72
|
+
* @param version - Version number from Firestore (if stream exists)
|
|
73
|
+
* @returns Current version as bigint or STREAM_DOES_NOT_EXIST
|
|
74
|
+
*/
|
|
75
|
+
declare function getCurrentStreamVersion(streamExists: boolean, version?: number): bigint | typeof STREAM_DOES_NOT_EXIST;
|
|
76
|
+
/**
|
|
77
|
+
* Calculate the next expected stream version after appending events
|
|
78
|
+
*
|
|
79
|
+
* @param currentVersion - Current stream version
|
|
80
|
+
* @param eventCount - Number of events being appended
|
|
81
|
+
* @returns Next expected version as bigint
|
|
82
|
+
*/
|
|
83
|
+
declare function calculateNextVersion(currentVersion: bigint | typeof STREAM_DOES_NOT_EXIST, eventCount: number): bigint;
|
|
84
|
+
|
|
85
|
+
export { ExpectedStreamVersion, FirestoreEventStore, FirestoreEventStoreOptions, assertExpectedVersionMatchesCurrent, calculateNextVersion, getCurrentStreamVersion, getFirestoreEventStore, padVersion, parseStreamName, timestampToDate };
|