@emmett-community/emmett-google-firestore 0.1.0 → 0.3.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/README.md +64 -30
- package/dist/index.d.mts +138 -4
- package/dist/index.d.ts +138 -4
- package/dist/index.js +133 -38
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +133 -38
- package/dist/index.mjs.map +1 -1
- package/package.json +20 -19
- package/dist/testing/index.d.mts +0 -115
- package/dist/testing/index.d.ts +0 -115
- package/dist/testing/index.js +0 -296
- package/dist/testing/index.js.map +0 -1
- package/dist/testing/index.mjs +0 -289
- package/dist/testing/index.mjs.map +0 -1
- package/dist/types-CHnx_sMk.d.mts +0 -122
- package/dist/types-CHnx_sMk.d.ts +0 -122
package/README.md
CHANGED
|
@@ -234,38 +234,17 @@ const state = await eventStore.aggregateStream(
|
|
|
234
234
|
|
|
235
235
|
## Testing
|
|
236
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
237
|
### Running Tests
|
|
262
238
|
|
|
263
239
|
```bash
|
|
264
240
|
# Unit tests
|
|
265
241
|
npm run test:unit
|
|
266
242
|
|
|
267
|
-
# Integration tests (
|
|
268
|
-
npm run test:
|
|
243
|
+
# Integration tests (in-memory)
|
|
244
|
+
npm run test:int
|
|
245
|
+
|
|
246
|
+
# E2E tests (Firestore Emulator via Testcontainers, requires Docker)
|
|
247
|
+
npm run test:e2e
|
|
269
248
|
|
|
270
249
|
# All tests
|
|
271
250
|
npm test
|
|
@@ -274,9 +253,17 @@ npm test
|
|
|
274
253
|
npm run test:coverage
|
|
275
254
|
```
|
|
276
255
|
|
|
256
|
+
Test files live in `test/` and are selected by filename suffix:
|
|
257
|
+
|
|
258
|
+
- `*.unit.spec.ts` (unit tests, pure logic)
|
|
259
|
+
- `*.int.spec.ts` (integration tests, in-memory Firestore)
|
|
260
|
+
- `*.e2e.spec.ts` (E2E tests, Firestore emulator via Testcontainers)
|
|
261
|
+
|
|
262
|
+
Support fixtures live under `test/support` (including Firebase emulator configs in `test/support/firebase`).
|
|
263
|
+
|
|
277
264
|
### Using Firestore Emulator
|
|
278
265
|
|
|
279
|
-
For local development and testing:
|
|
266
|
+
For local development and manual testing:
|
|
280
267
|
|
|
281
268
|
```bash
|
|
282
269
|
# Install Firebase CLI
|
|
@@ -284,9 +271,6 @@ npm install -g firebase-tools
|
|
|
284
271
|
|
|
285
272
|
# Start emulator
|
|
286
273
|
firebase emulators:start --only firestore
|
|
287
|
-
|
|
288
|
-
# Or use the provided script
|
|
289
|
-
./scripts/start-emulator.sh
|
|
290
274
|
```
|
|
291
275
|
|
|
292
276
|
Set environment variables:
|
|
@@ -296,6 +280,8 @@ export FIRESTORE_PROJECT_ID=test-project
|
|
|
296
280
|
export FIRESTORE_EMULATOR_HOST=localhost:8080
|
|
297
281
|
```
|
|
298
282
|
|
|
283
|
+
E2E tests start the emulator automatically via Testcontainers.
|
|
284
|
+
|
|
299
285
|
## Examples
|
|
300
286
|
|
|
301
287
|
### Complete Shopping Cart Example
|
|
@@ -507,6 +493,54 @@ try {
|
|
|
507
493
|
}
|
|
508
494
|
```
|
|
509
495
|
|
|
496
|
+
## Observability
|
|
497
|
+
|
|
498
|
+
The event store supports optional logging and OpenTelemetry tracing.
|
|
499
|
+
|
|
500
|
+
### Logging
|
|
501
|
+
|
|
502
|
+
Pass an optional logger compatible with Pino:
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
import pino from 'pino';
|
|
506
|
+
|
|
507
|
+
const eventStore = getFirestoreEventStore(firestore, {
|
|
508
|
+
observability: {
|
|
509
|
+
logger: pino({ level: 'debug' }),
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
**Logging points:**
|
|
515
|
+
|
|
516
|
+
- `info`: Initialization
|
|
517
|
+
- `debug`: I/O operations (queries, transactions)
|
|
518
|
+
- `warn`: Version conflicts (recoverable)
|
|
519
|
+
- `error`: Failures before rethrowing
|
|
520
|
+
|
|
521
|
+
**Note:** Event payloads are never logged.
|
|
522
|
+
|
|
523
|
+
### Tracing
|
|
524
|
+
|
|
525
|
+
The package uses `@opentelemetry/api` directly. Spans are created passively:
|
|
526
|
+
|
|
527
|
+
- If your application initializes OpenTelemetry, spans will be recorded
|
|
528
|
+
- If not, the tracing calls are no-ops with zero overhead
|
|
529
|
+
|
|
530
|
+
**Span names:**
|
|
531
|
+
|
|
532
|
+
- `emmett.firestore.read_stream`
|
|
533
|
+
- `emmett.firestore.append_to_stream`
|
|
534
|
+
|
|
535
|
+
**Attributes:**
|
|
536
|
+
|
|
537
|
+
- `emmett.stream_name`
|
|
538
|
+
- `emmett.event_count`
|
|
539
|
+
- `emmett.new_version`
|
|
540
|
+
- `emmett.created_new_stream`
|
|
541
|
+
|
|
542
|
+
To enable tracing, initialize OpenTelemetry in your application (this package never initializes tracing itself).
|
|
543
|
+
|
|
510
544
|
## TypeScript Support
|
|
511
545
|
|
|
512
546
|
The package is written in TypeScript and includes full type definitions:
|
package/dist/index.d.mts
CHANGED
|
@@ -1,9 +1,143 @@
|
|
|
1
1
|
import { Firestore, Timestamp } from '@google-cloud/firestore';
|
|
2
|
-
import {
|
|
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';
|
|
2
|
+
import { Event, ReadEvent, ReadEventMetadataWithGlobalPosition, ExpectedStreamVersion as ExpectedStreamVersion$1, STREAM_DOES_NOT_EXIST } from '@event-driven-io/emmett';
|
|
5
3
|
export { NO_CONCURRENCY_CHECK, STREAM_DOES_NOT_EXIST, STREAM_EXISTS } from '@event-driven-io/emmett';
|
|
6
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Minimal logger interface compatible with Pino and other loggers.
|
|
7
|
+
* All methods are optional to support varying logger implementations.
|
|
8
|
+
*/
|
|
9
|
+
interface Logger {
|
|
10
|
+
debug?(msg: string, data?: unknown): void;
|
|
11
|
+
info?(msg: string, data?: unknown): void;
|
|
12
|
+
warn?(msg: string, data?: unknown): void;
|
|
13
|
+
error?(msg: string, err?: unknown): void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Observability configuration options
|
|
17
|
+
*/
|
|
18
|
+
interface ObservabilityOptions {
|
|
19
|
+
/** Optional logger instance. If not provided, no logging occurs. */
|
|
20
|
+
logger?: Logger;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Expected version for stream operations
|
|
24
|
+
* Uses Emmett's standard version constants for full compatibility
|
|
25
|
+
* - number | bigint: Expect specific version
|
|
26
|
+
* - STREAM_DOES_NOT_EXIST: Stream must not exist
|
|
27
|
+
* - STREAM_EXISTS: Stream must exist (any version)
|
|
28
|
+
* - NO_CONCURRENCY_CHECK: No version check
|
|
29
|
+
*/
|
|
30
|
+
type ExpectedStreamVersion = ExpectedStreamVersion$1<bigint>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Options for appending events to a stream
|
|
34
|
+
*/
|
|
35
|
+
interface AppendToStreamOptions {
|
|
36
|
+
expectedStreamVersion?: ExpectedStreamVersion;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Result of appending events to a stream
|
|
40
|
+
*/
|
|
41
|
+
interface AppendToStreamResult {
|
|
42
|
+
nextExpectedStreamVersion: bigint;
|
|
43
|
+
createdNewStream: boolean;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Options for reading events from a stream
|
|
47
|
+
*/
|
|
48
|
+
interface ReadStreamOptions {
|
|
49
|
+
from?: bigint;
|
|
50
|
+
to?: bigint;
|
|
51
|
+
maxCount?: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Metadata stored in Firestore stream document
|
|
55
|
+
*/
|
|
56
|
+
interface StreamMetadata {
|
|
57
|
+
version: number;
|
|
58
|
+
createdAt: Timestamp;
|
|
59
|
+
updatedAt: Timestamp;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Event document structure in Firestore
|
|
63
|
+
*/
|
|
64
|
+
interface EventDocument {
|
|
65
|
+
type: string;
|
|
66
|
+
data: Record<string, unknown>;
|
|
67
|
+
metadata?: Record<string, unknown>;
|
|
68
|
+
timestamp: Timestamp;
|
|
69
|
+
globalPosition: number;
|
|
70
|
+
streamVersion: number;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Firestore-specific read event metadata
|
|
74
|
+
*/
|
|
75
|
+
interface FirestoreReadEventMetadata extends ReadEventMetadataWithGlobalPosition {
|
|
76
|
+
streamName: string;
|
|
77
|
+
streamVersion: bigint;
|
|
78
|
+
timestamp: Date;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Firestore read event
|
|
82
|
+
*/
|
|
83
|
+
type FirestoreReadEvent<EventType extends Event = Event> = ReadEvent<EventType, FirestoreReadEventMetadata>;
|
|
84
|
+
/**
|
|
85
|
+
* Collection configuration for Firestore event store
|
|
86
|
+
*/
|
|
87
|
+
interface CollectionConfig {
|
|
88
|
+
streams: string;
|
|
89
|
+
counters: string;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Firestore event store options
|
|
93
|
+
*/
|
|
94
|
+
interface FirestoreEventStoreOptions {
|
|
95
|
+
collections?: Partial<CollectionConfig>;
|
|
96
|
+
observability?: ObservabilityOptions;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Firestore event store interface
|
|
100
|
+
*/
|
|
101
|
+
interface FirestoreEventStore {
|
|
102
|
+
/**
|
|
103
|
+
* The underlying Firestore instance
|
|
104
|
+
*/
|
|
105
|
+
readonly firestore: Firestore;
|
|
106
|
+
/**
|
|
107
|
+
* Collection names configuration
|
|
108
|
+
*/
|
|
109
|
+
readonly collections: CollectionConfig;
|
|
110
|
+
/**
|
|
111
|
+
* Read events from a stream
|
|
112
|
+
*/
|
|
113
|
+
readStream<EventType extends Event>(streamName: string, options?: ReadStreamOptions): Promise<FirestoreReadEvent<EventType>[]>;
|
|
114
|
+
/**
|
|
115
|
+
* Aggregate stream by applying events to state
|
|
116
|
+
*/
|
|
117
|
+
aggregateStream<State, EventType extends Event>(streamName: string, options: {
|
|
118
|
+
evolve: (state: State, event: FirestoreReadEvent<EventType>) => State;
|
|
119
|
+
initialState: () => State;
|
|
120
|
+
read?: ReadStreamOptions;
|
|
121
|
+
}): Promise<{
|
|
122
|
+
state: State;
|
|
123
|
+
currentStreamVersion: bigint;
|
|
124
|
+
streamExists: boolean;
|
|
125
|
+
}>;
|
|
126
|
+
/**
|
|
127
|
+
* Append events to a stream
|
|
128
|
+
*/
|
|
129
|
+
appendToStream<EventType extends Event>(streamName: string, events: EventType[], options?: AppendToStreamOptions): Promise<AppendToStreamResult>;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Error thrown when expected version doesn't match current version
|
|
133
|
+
*/
|
|
134
|
+
declare class ExpectedVersionConflictError extends Error {
|
|
135
|
+
readonly streamName: string;
|
|
136
|
+
readonly expected: ExpectedStreamVersion;
|
|
137
|
+
readonly actual: bigint | typeof STREAM_DOES_NOT_EXIST;
|
|
138
|
+
constructor(streamName: string, expected: ExpectedStreamVersion, actual: bigint | typeof STREAM_DOES_NOT_EXIST);
|
|
139
|
+
}
|
|
140
|
+
|
|
7
141
|
/**
|
|
8
142
|
* Factory function to create a Firestore event store
|
|
9
143
|
*
|
|
@@ -82,4 +216,4 @@ declare function getCurrentStreamVersion(streamExists: boolean, version?: number
|
|
|
82
216
|
*/
|
|
83
217
|
declare function calculateNextVersion(currentVersion: bigint | typeof STREAM_DOES_NOT_EXIST, eventCount: number): bigint;
|
|
84
218
|
|
|
85
|
-
export { ExpectedStreamVersion, FirestoreEventStore, FirestoreEventStoreOptions, assertExpectedVersionMatchesCurrent, calculateNextVersion, getCurrentStreamVersion, getFirestoreEventStore, padVersion, parseStreamName, timestampToDate };
|
|
219
|
+
export { type AppendToStreamOptions, type AppendToStreamResult, type CollectionConfig, type EventDocument, type ExpectedStreamVersion, ExpectedVersionConflictError, type FirestoreEventStore, type FirestoreEventStoreOptions, type FirestoreReadEvent, type FirestoreReadEventMetadata, type Logger, type ObservabilityOptions, type ReadStreamOptions, type StreamMetadata, assertExpectedVersionMatchesCurrent, calculateNextVersion, getCurrentStreamVersion, getFirestoreEventStore, padVersion, parseStreamName, timestampToDate };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,143 @@
|
|
|
1
1
|
import { Firestore, Timestamp } from '@google-cloud/firestore';
|
|
2
|
-
import {
|
|
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.js';
|
|
4
|
-
import { STREAM_DOES_NOT_EXIST } from '@event-driven-io/emmett';
|
|
2
|
+
import { Event, ReadEvent, ReadEventMetadataWithGlobalPosition, ExpectedStreamVersion as ExpectedStreamVersion$1, STREAM_DOES_NOT_EXIST } from '@event-driven-io/emmett';
|
|
5
3
|
export { NO_CONCURRENCY_CHECK, STREAM_DOES_NOT_EXIST, STREAM_EXISTS } from '@event-driven-io/emmett';
|
|
6
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Minimal logger interface compatible with Pino and other loggers.
|
|
7
|
+
* All methods are optional to support varying logger implementations.
|
|
8
|
+
*/
|
|
9
|
+
interface Logger {
|
|
10
|
+
debug?(msg: string, data?: unknown): void;
|
|
11
|
+
info?(msg: string, data?: unknown): void;
|
|
12
|
+
warn?(msg: string, data?: unknown): void;
|
|
13
|
+
error?(msg: string, err?: unknown): void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Observability configuration options
|
|
17
|
+
*/
|
|
18
|
+
interface ObservabilityOptions {
|
|
19
|
+
/** Optional logger instance. If not provided, no logging occurs. */
|
|
20
|
+
logger?: Logger;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Expected version for stream operations
|
|
24
|
+
* Uses Emmett's standard version constants for full compatibility
|
|
25
|
+
* - number | bigint: Expect specific version
|
|
26
|
+
* - STREAM_DOES_NOT_EXIST: Stream must not exist
|
|
27
|
+
* - STREAM_EXISTS: Stream must exist (any version)
|
|
28
|
+
* - NO_CONCURRENCY_CHECK: No version check
|
|
29
|
+
*/
|
|
30
|
+
type ExpectedStreamVersion = ExpectedStreamVersion$1<bigint>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Options for appending events to a stream
|
|
34
|
+
*/
|
|
35
|
+
interface AppendToStreamOptions {
|
|
36
|
+
expectedStreamVersion?: ExpectedStreamVersion;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Result of appending events to a stream
|
|
40
|
+
*/
|
|
41
|
+
interface AppendToStreamResult {
|
|
42
|
+
nextExpectedStreamVersion: bigint;
|
|
43
|
+
createdNewStream: boolean;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Options for reading events from a stream
|
|
47
|
+
*/
|
|
48
|
+
interface ReadStreamOptions {
|
|
49
|
+
from?: bigint;
|
|
50
|
+
to?: bigint;
|
|
51
|
+
maxCount?: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Metadata stored in Firestore stream document
|
|
55
|
+
*/
|
|
56
|
+
interface StreamMetadata {
|
|
57
|
+
version: number;
|
|
58
|
+
createdAt: Timestamp;
|
|
59
|
+
updatedAt: Timestamp;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Event document structure in Firestore
|
|
63
|
+
*/
|
|
64
|
+
interface EventDocument {
|
|
65
|
+
type: string;
|
|
66
|
+
data: Record<string, unknown>;
|
|
67
|
+
metadata?: Record<string, unknown>;
|
|
68
|
+
timestamp: Timestamp;
|
|
69
|
+
globalPosition: number;
|
|
70
|
+
streamVersion: number;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Firestore-specific read event metadata
|
|
74
|
+
*/
|
|
75
|
+
interface FirestoreReadEventMetadata extends ReadEventMetadataWithGlobalPosition {
|
|
76
|
+
streamName: string;
|
|
77
|
+
streamVersion: bigint;
|
|
78
|
+
timestamp: Date;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Firestore read event
|
|
82
|
+
*/
|
|
83
|
+
type FirestoreReadEvent<EventType extends Event = Event> = ReadEvent<EventType, FirestoreReadEventMetadata>;
|
|
84
|
+
/**
|
|
85
|
+
* Collection configuration for Firestore event store
|
|
86
|
+
*/
|
|
87
|
+
interface CollectionConfig {
|
|
88
|
+
streams: string;
|
|
89
|
+
counters: string;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Firestore event store options
|
|
93
|
+
*/
|
|
94
|
+
interface FirestoreEventStoreOptions {
|
|
95
|
+
collections?: Partial<CollectionConfig>;
|
|
96
|
+
observability?: ObservabilityOptions;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Firestore event store interface
|
|
100
|
+
*/
|
|
101
|
+
interface FirestoreEventStore {
|
|
102
|
+
/**
|
|
103
|
+
* The underlying Firestore instance
|
|
104
|
+
*/
|
|
105
|
+
readonly firestore: Firestore;
|
|
106
|
+
/**
|
|
107
|
+
* Collection names configuration
|
|
108
|
+
*/
|
|
109
|
+
readonly collections: CollectionConfig;
|
|
110
|
+
/**
|
|
111
|
+
* Read events from a stream
|
|
112
|
+
*/
|
|
113
|
+
readStream<EventType extends Event>(streamName: string, options?: ReadStreamOptions): Promise<FirestoreReadEvent<EventType>[]>;
|
|
114
|
+
/**
|
|
115
|
+
* Aggregate stream by applying events to state
|
|
116
|
+
*/
|
|
117
|
+
aggregateStream<State, EventType extends Event>(streamName: string, options: {
|
|
118
|
+
evolve: (state: State, event: FirestoreReadEvent<EventType>) => State;
|
|
119
|
+
initialState: () => State;
|
|
120
|
+
read?: ReadStreamOptions;
|
|
121
|
+
}): Promise<{
|
|
122
|
+
state: State;
|
|
123
|
+
currentStreamVersion: bigint;
|
|
124
|
+
streamExists: boolean;
|
|
125
|
+
}>;
|
|
126
|
+
/**
|
|
127
|
+
* Append events to a stream
|
|
128
|
+
*/
|
|
129
|
+
appendToStream<EventType extends Event>(streamName: string, events: EventType[], options?: AppendToStreamOptions): Promise<AppendToStreamResult>;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Error thrown when expected version doesn't match current version
|
|
133
|
+
*/
|
|
134
|
+
declare class ExpectedVersionConflictError extends Error {
|
|
135
|
+
readonly streamName: string;
|
|
136
|
+
readonly expected: ExpectedStreamVersion;
|
|
137
|
+
readonly actual: bigint | typeof STREAM_DOES_NOT_EXIST;
|
|
138
|
+
constructor(streamName: string, expected: ExpectedStreamVersion, actual: bigint | typeof STREAM_DOES_NOT_EXIST);
|
|
139
|
+
}
|
|
140
|
+
|
|
7
141
|
/**
|
|
8
142
|
* Factory function to create a Firestore event store
|
|
9
143
|
*
|
|
@@ -82,4 +216,4 @@ declare function getCurrentStreamVersion(streamExists: boolean, version?: number
|
|
|
82
216
|
*/
|
|
83
217
|
declare function calculateNextVersion(currentVersion: bigint | typeof STREAM_DOES_NOT_EXIST, eventCount: number): bigint;
|
|
84
218
|
|
|
85
|
-
export { ExpectedStreamVersion, FirestoreEventStore, FirestoreEventStoreOptions, assertExpectedVersionMatchesCurrent, calculateNextVersion, getCurrentStreamVersion, getFirestoreEventStore, padVersion, parseStreamName, timestampToDate };
|
|
219
|
+
export { type AppendToStreamOptions, type AppendToStreamResult, type CollectionConfig, type EventDocument, type ExpectedStreamVersion, ExpectedVersionConflictError, type FirestoreEventStore, type FirestoreEventStoreOptions, type FirestoreReadEvent, type FirestoreReadEventMetadata, type Logger, type ObservabilityOptions, type ReadStreamOptions, type StreamMetadata, assertExpectedVersionMatchesCurrent, calculateNextVersion, getCurrentStreamVersion, getFirestoreEventStore, padVersion, parseStreamName, timestampToDate };
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var api = require('@opentelemetry/api');
|
|
3
4
|
var emmett = require('@event-driven-io/emmett');
|
|
4
5
|
|
|
5
|
-
// src/eventStore/
|
|
6
|
+
// src/eventStore/firestoreEventStore.ts
|
|
6
7
|
var ExpectedVersionConflictError = class _ExpectedVersionConflictError extends Error {
|
|
7
8
|
constructor(streamName, expected, actual) {
|
|
8
9
|
super(
|
|
@@ -83,6 +84,14 @@ function calculateNextVersion(currentVersion, eventCount) {
|
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
// src/eventStore/firestoreEventStore.ts
|
|
87
|
+
var tracer = api.trace.getTracer("@emmett-community/emmett-google-firestore");
|
|
88
|
+
function safeLog(logger, level, msg, data) {
|
|
89
|
+
if (!logger) return;
|
|
90
|
+
const logFn = logger[level];
|
|
91
|
+
if (typeof logFn === "function") {
|
|
92
|
+
logFn.call(logger, msg, data);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
86
95
|
var DEFAULT_COLLECTIONS = {
|
|
87
96
|
streams: "streams",
|
|
88
97
|
counters: "_counters"
|
|
@@ -94,39 +103,70 @@ var FirestoreEventStoreImpl = class {
|
|
|
94
103
|
...DEFAULT_COLLECTIONS,
|
|
95
104
|
...options.collections
|
|
96
105
|
};
|
|
106
|
+
this.logger = options.observability?.logger;
|
|
107
|
+
safeLog(this.logger, "info", "FirestoreEventStore initialized");
|
|
97
108
|
}
|
|
98
109
|
collections;
|
|
110
|
+
logger;
|
|
99
111
|
/**
|
|
100
112
|
* Read events from a stream
|
|
101
113
|
*/
|
|
102
114
|
async readStream(streamName, options = {}) {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
if (from !== void 0) {
|
|
106
|
-
query = query.where("streamVersion", ">=", Number(from));
|
|
107
|
-
}
|
|
108
|
-
if (to !== void 0) {
|
|
109
|
-
query = query.where("streamVersion", "<=", Number(to));
|
|
110
|
-
}
|
|
111
|
-
if (maxCount !== void 0 && maxCount > 0) {
|
|
112
|
-
query = query.limit(maxCount);
|
|
113
|
-
}
|
|
114
|
-
const snapshot = await query.get();
|
|
115
|
-
return snapshot.docs.map((doc) => {
|
|
116
|
-
const data = doc.data();
|
|
117
|
-
return {
|
|
118
|
-
type: data.type,
|
|
119
|
-
data: data.data,
|
|
120
|
-
metadata: {
|
|
121
|
-
...data.metadata,
|
|
122
|
-
streamName,
|
|
123
|
-
streamVersion: BigInt(data.streamVersion),
|
|
124
|
-
streamPosition: BigInt(data.streamVersion),
|
|
125
|
-
globalPosition: BigInt(data.globalPosition),
|
|
126
|
-
timestamp: timestampToDate(data.timestamp)
|
|
127
|
-
}
|
|
128
|
-
};
|
|
115
|
+
const span = tracer.startSpan("emmett.firestore.read_stream", {
|
|
116
|
+
attributes: { "emmett.stream_name": streamName }
|
|
129
117
|
});
|
|
118
|
+
try {
|
|
119
|
+
safeLog(this.logger, "debug", "Reading stream", {
|
|
120
|
+
streamName,
|
|
121
|
+
from: options.from?.toString(),
|
|
122
|
+
to: options.to?.toString(),
|
|
123
|
+
maxCount: options.maxCount
|
|
124
|
+
});
|
|
125
|
+
const { from, to, maxCount } = options;
|
|
126
|
+
let query = this.firestore.collection(this.collections.streams).doc(streamName).collection("events").orderBy("streamVersion", "asc");
|
|
127
|
+
if (from !== void 0) {
|
|
128
|
+
query = query.where("streamVersion", ">=", Number(from));
|
|
129
|
+
}
|
|
130
|
+
if (to !== void 0) {
|
|
131
|
+
query = query.where("streamVersion", "<=", Number(to));
|
|
132
|
+
}
|
|
133
|
+
if (maxCount !== void 0 && maxCount > 0) {
|
|
134
|
+
query = query.limit(maxCount);
|
|
135
|
+
}
|
|
136
|
+
const snapshot = await query.get();
|
|
137
|
+
const events = snapshot.docs.map((doc) => {
|
|
138
|
+
const data = doc.data();
|
|
139
|
+
return {
|
|
140
|
+
type: data.type,
|
|
141
|
+
data: data.data,
|
|
142
|
+
metadata: {
|
|
143
|
+
...data.metadata,
|
|
144
|
+
streamName,
|
|
145
|
+
streamVersion: BigInt(data.streamVersion),
|
|
146
|
+
streamPosition: BigInt(data.streamVersion),
|
|
147
|
+
globalPosition: BigInt(data.globalPosition),
|
|
148
|
+
timestamp: timestampToDate(data.timestamp)
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
span.setAttribute("emmett.event_count", events.length);
|
|
153
|
+
span.setStatus({ code: api.SpanStatusCode.OK });
|
|
154
|
+
safeLog(this.logger, "debug", "Stream read completed", {
|
|
155
|
+
streamName,
|
|
156
|
+
eventCount: events.length
|
|
157
|
+
});
|
|
158
|
+
return events;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
span.recordException(error);
|
|
161
|
+
span.setStatus({ code: api.SpanStatusCode.ERROR });
|
|
162
|
+
safeLog(this.logger, "error", "Failed to read stream", {
|
|
163
|
+
streamName,
|
|
164
|
+
error
|
|
165
|
+
});
|
|
166
|
+
throw error;
|
|
167
|
+
} finally {
|
|
168
|
+
span.end();
|
|
169
|
+
}
|
|
130
170
|
}
|
|
131
171
|
/**
|
|
132
172
|
* Aggregate stream by applying events to state
|
|
@@ -147,21 +187,66 @@ var FirestoreEventStoreImpl = class {
|
|
|
147
187
|
* Append events to a stream with optimistic concurrency control
|
|
148
188
|
*/
|
|
149
189
|
async appendToStream(streamName, events, options = {}) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return await this.appendToStreamInTransaction(
|
|
156
|
-
transaction,
|
|
157
|
-
streamName,
|
|
158
|
-
events,
|
|
159
|
-
expectedStreamVersion
|
|
160
|
-
);
|
|
190
|
+
const span = tracer.startSpan("emmett.firestore.append_to_stream", {
|
|
191
|
+
attributes: {
|
|
192
|
+
"emmett.stream_name": streamName,
|
|
193
|
+
"emmett.event_count": events.length
|
|
194
|
+
}
|
|
161
195
|
});
|
|
196
|
+
try {
|
|
197
|
+
if (events.length === 0) {
|
|
198
|
+
throw new Error("Cannot append empty event array");
|
|
199
|
+
}
|
|
200
|
+
const { expectedStreamVersion = emmett.NO_CONCURRENCY_CHECK } = options;
|
|
201
|
+
safeLog(this.logger, "debug", "Appending to stream", {
|
|
202
|
+
streamName,
|
|
203
|
+
eventCount: events.length,
|
|
204
|
+
eventTypes: events.map((e) => e.type),
|
|
205
|
+
expectedVersion: String(expectedStreamVersion)
|
|
206
|
+
});
|
|
207
|
+
const result = await this.firestore.runTransaction(async (transaction) => {
|
|
208
|
+
return await this.appendToStreamInTransaction(
|
|
209
|
+
transaction,
|
|
210
|
+
streamName,
|
|
211
|
+
events,
|
|
212
|
+
expectedStreamVersion
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
span.setAttribute("emmett.new_version", Number(result.nextExpectedStreamVersion));
|
|
216
|
+
span.setAttribute("emmett.created_new_stream", result.createdNewStream);
|
|
217
|
+
span.setStatus({ code: api.SpanStatusCode.OK });
|
|
218
|
+
safeLog(this.logger, "debug", "Append completed", {
|
|
219
|
+
streamName,
|
|
220
|
+
newVersion: result.nextExpectedStreamVersion.toString(),
|
|
221
|
+
createdNewStream: result.createdNewStream
|
|
222
|
+
});
|
|
223
|
+
return result;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
span.recordException(error);
|
|
226
|
+
span.setStatus({ code: api.SpanStatusCode.ERROR });
|
|
227
|
+
if (error instanceof ExpectedVersionConflictError) {
|
|
228
|
+
safeLog(this.logger, "warn", "Version conflict during append", {
|
|
229
|
+
streamName,
|
|
230
|
+
expected: String(error.expected),
|
|
231
|
+
actual: String(error.actual)
|
|
232
|
+
});
|
|
233
|
+
} else {
|
|
234
|
+
safeLog(this.logger, "error", "Failed to append to stream", {
|
|
235
|
+
streamName,
|
|
236
|
+
error
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
throw error;
|
|
240
|
+
} finally {
|
|
241
|
+
span.end();
|
|
242
|
+
}
|
|
162
243
|
}
|
|
163
244
|
/**
|
|
164
245
|
* Internal method to append events within a transaction
|
|
246
|
+
*
|
|
247
|
+
* Note: No separate span here - this method runs inside appendToStream's span,
|
|
248
|
+
* and Firestore transaction operations are atomic. The parent span captures
|
|
249
|
+
* the full transaction duration.
|
|
165
250
|
*/
|
|
166
251
|
async appendToStreamInTransaction(transaction, streamName, events, expectedStreamVersion) {
|
|
167
252
|
const streamRef = this.firestore.collection(this.collections.streams).doc(streamName);
|
|
@@ -172,6 +257,11 @@ var FirestoreEventStoreImpl = class {
|
|
|
172
257
|
streamExists,
|
|
173
258
|
streamData?.version
|
|
174
259
|
);
|
|
260
|
+
safeLog(this.logger, "debug", "Read stream metadata", {
|
|
261
|
+
streamName,
|
|
262
|
+
exists: streamExists,
|
|
263
|
+
currentVersion: currentVersion === emmett.STREAM_DOES_NOT_EXIST ? "none" : currentVersion.toString()
|
|
264
|
+
});
|
|
175
265
|
assertExpectedVersionMatchesCurrent(
|
|
176
266
|
streamName,
|
|
177
267
|
expectedStreamVersion,
|
|
@@ -208,6 +298,11 @@ var FirestoreEventStoreImpl = class {
|
|
|
208
298
|
value: globalPosition,
|
|
209
299
|
updatedAt: now
|
|
210
300
|
});
|
|
301
|
+
safeLog(this.logger, "debug", "Events written to transaction", {
|
|
302
|
+
streamName,
|
|
303
|
+
count: events.length,
|
|
304
|
+
newVersion
|
|
305
|
+
});
|
|
211
306
|
return {
|
|
212
307
|
nextExpectedStreamVersion: BigInt(newVersion),
|
|
213
308
|
createdNewStream: !streamExists
|