@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/dist/index.d.ts
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.js';
|
|
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';
|
|
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 };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var emmett = require('@event-driven-io/emmett');
|
|
4
|
+
|
|
5
|
+
// src/eventStore/types.ts
|
|
6
|
+
var ExpectedVersionConflictError = class _ExpectedVersionConflictError extends Error {
|
|
7
|
+
constructor(streamName, expected, actual) {
|
|
8
|
+
super(
|
|
9
|
+
`Expected version conflict for stream '${streamName}': expected ${String(expected)}, actual ${String(actual)}`
|
|
10
|
+
);
|
|
11
|
+
this.streamName = streamName;
|
|
12
|
+
this.expected = expected;
|
|
13
|
+
this.actual = actual;
|
|
14
|
+
this.name = "ExpectedVersionConflictError";
|
|
15
|
+
Object.setPrototypeOf(this, _ExpectedVersionConflictError.prototype);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// src/eventStore/utils.ts
|
|
20
|
+
function padVersion(version) {
|
|
21
|
+
return version.toString().padStart(10, "0");
|
|
22
|
+
}
|
|
23
|
+
function parseStreamName(streamName) {
|
|
24
|
+
const firstDashIndex = streamName.indexOf("-");
|
|
25
|
+
if (firstDashIndex === -1) {
|
|
26
|
+
return {
|
|
27
|
+
streamType: streamName,
|
|
28
|
+
streamId: ""
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
streamType: streamName.substring(0, firstDashIndex),
|
|
33
|
+
streamId: streamName.substring(firstDashIndex + 1)
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function timestampToDate(timestamp) {
|
|
37
|
+
return timestamp.toDate();
|
|
38
|
+
}
|
|
39
|
+
function assertExpectedVersionMatchesCurrent(streamName, expectedVersion, currentVersion) {
|
|
40
|
+
if (expectedVersion === emmett.NO_CONCURRENCY_CHECK) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (expectedVersion === emmett.STREAM_DOES_NOT_EXIST) {
|
|
44
|
+
if (currentVersion !== emmett.STREAM_DOES_NOT_EXIST) {
|
|
45
|
+
throw new ExpectedVersionConflictError(
|
|
46
|
+
streamName,
|
|
47
|
+
expectedVersion,
|
|
48
|
+
currentVersion
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (expectedVersion === emmett.STREAM_EXISTS) {
|
|
54
|
+
if (currentVersion === emmett.STREAM_DOES_NOT_EXIST) {
|
|
55
|
+
throw new ExpectedVersionConflictError(
|
|
56
|
+
streamName,
|
|
57
|
+
expectedVersion,
|
|
58
|
+
currentVersion
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const expectedBigInt = BigInt(expectedVersion);
|
|
64
|
+
if (currentVersion === emmett.STREAM_DOES_NOT_EXIST || currentVersion !== expectedBigInt) {
|
|
65
|
+
throw new ExpectedVersionConflictError(
|
|
66
|
+
streamName,
|
|
67
|
+
expectedVersion,
|
|
68
|
+
currentVersion
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function getCurrentStreamVersion(streamExists, version) {
|
|
73
|
+
if (!streamExists) {
|
|
74
|
+
return emmett.STREAM_DOES_NOT_EXIST;
|
|
75
|
+
}
|
|
76
|
+
return BigInt(version ?? -1);
|
|
77
|
+
}
|
|
78
|
+
function calculateNextVersion(currentVersion, eventCount) {
|
|
79
|
+
if (currentVersion === emmett.STREAM_DOES_NOT_EXIST) {
|
|
80
|
+
return BigInt(eventCount - 1);
|
|
81
|
+
}
|
|
82
|
+
return currentVersion + BigInt(eventCount);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/eventStore/firestoreEventStore.ts
|
|
86
|
+
var DEFAULT_COLLECTIONS = {
|
|
87
|
+
streams: "streams",
|
|
88
|
+
counters: "_counters"
|
|
89
|
+
};
|
|
90
|
+
var FirestoreEventStoreImpl = class {
|
|
91
|
+
constructor(firestore, options = {}) {
|
|
92
|
+
this.firestore = firestore;
|
|
93
|
+
this.collections = {
|
|
94
|
+
...DEFAULT_COLLECTIONS,
|
|
95
|
+
...options.collections
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
collections;
|
|
99
|
+
/**
|
|
100
|
+
* Read events from a stream
|
|
101
|
+
*/
|
|
102
|
+
async readStream(streamName, options = {}) {
|
|
103
|
+
const { from, to, maxCount } = options;
|
|
104
|
+
let query = this.firestore.collection(this.collections.streams).doc(streamName).collection("events").orderBy("streamVersion", "asc");
|
|
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
|
+
};
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Aggregate stream by applying events to state
|
|
133
|
+
*/
|
|
134
|
+
async aggregateStream(streamName, options) {
|
|
135
|
+
const { evolve, initialState, read } = options;
|
|
136
|
+
const events = await this.readStream(streamName, read);
|
|
137
|
+
const streamExists = events.length > 0;
|
|
138
|
+
const state = events.reduce(evolve, initialState());
|
|
139
|
+
const currentStreamVersion = streamExists ? events[events.length - 1].metadata.streamVersion : BigInt(0);
|
|
140
|
+
return {
|
|
141
|
+
state,
|
|
142
|
+
currentStreamVersion,
|
|
143
|
+
streamExists
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Append events to a stream with optimistic concurrency control
|
|
148
|
+
*/
|
|
149
|
+
async appendToStream(streamName, events, options = {}) {
|
|
150
|
+
if (events.length === 0) {
|
|
151
|
+
throw new Error("Cannot append empty event array");
|
|
152
|
+
}
|
|
153
|
+
const { expectedStreamVersion = emmett.NO_CONCURRENCY_CHECK } = options;
|
|
154
|
+
return await this.firestore.runTransaction(async (transaction) => {
|
|
155
|
+
return await this.appendToStreamInTransaction(
|
|
156
|
+
transaction,
|
|
157
|
+
streamName,
|
|
158
|
+
events,
|
|
159
|
+
expectedStreamVersion
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Internal method to append events within a transaction
|
|
165
|
+
*/
|
|
166
|
+
async appendToStreamInTransaction(transaction, streamName, events, expectedStreamVersion) {
|
|
167
|
+
const streamRef = this.firestore.collection(this.collections.streams).doc(streamName);
|
|
168
|
+
const streamDoc = await transaction.get(streamRef);
|
|
169
|
+
const streamExists = streamDoc.exists;
|
|
170
|
+
const streamData = streamDoc.data();
|
|
171
|
+
const currentVersion = getCurrentStreamVersion(
|
|
172
|
+
streamExists,
|
|
173
|
+
streamData?.version
|
|
174
|
+
);
|
|
175
|
+
assertExpectedVersionMatchesCurrent(
|
|
176
|
+
streamName,
|
|
177
|
+
expectedStreamVersion,
|
|
178
|
+
currentVersion
|
|
179
|
+
);
|
|
180
|
+
const counterRef = this.firestore.collection(this.collections.counters).doc("global_position");
|
|
181
|
+
const counterDoc = await transaction.get(counterRef);
|
|
182
|
+
let globalPosition = counterDoc.exists ? counterDoc.data()?.value ?? 0 : 0;
|
|
183
|
+
const baseVersion = currentVersion === emmett.STREAM_DOES_NOT_EXIST ? -1 : Number(currentVersion);
|
|
184
|
+
const TimestampClass = this.firestore.constructor;
|
|
185
|
+
const now = TimestampClass.Timestamp.now();
|
|
186
|
+
events.forEach((event, index) => {
|
|
187
|
+
const eventVersion = baseVersion + 1 + index;
|
|
188
|
+
const eventRef = streamRef.collection("events").doc(padVersion(eventVersion));
|
|
189
|
+
const metadata = event.metadata;
|
|
190
|
+
const eventDocument = {
|
|
191
|
+
type: event.type,
|
|
192
|
+
data: event.data,
|
|
193
|
+
...metadata && { metadata },
|
|
194
|
+
timestamp: now,
|
|
195
|
+
globalPosition: globalPosition++,
|
|
196
|
+
streamVersion: eventVersion
|
|
197
|
+
};
|
|
198
|
+
transaction.set(eventRef, eventDocument);
|
|
199
|
+
});
|
|
200
|
+
const newVersion = baseVersion + events.length;
|
|
201
|
+
const updatedMetadata = {
|
|
202
|
+
version: newVersion,
|
|
203
|
+
createdAt: streamData?.createdAt ?? now,
|
|
204
|
+
updatedAt: now
|
|
205
|
+
};
|
|
206
|
+
transaction.set(streamRef, updatedMetadata);
|
|
207
|
+
transaction.set(counterRef, {
|
|
208
|
+
value: globalPosition,
|
|
209
|
+
updatedAt: now
|
|
210
|
+
});
|
|
211
|
+
return {
|
|
212
|
+
nextExpectedStreamVersion: BigInt(newVersion),
|
|
213
|
+
createdNewStream: !streamExists
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
function getFirestoreEventStore(firestore, options) {
|
|
218
|
+
return new FirestoreEventStoreImpl(firestore, options);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
Object.defineProperty(exports, "NO_CONCURRENCY_CHECK", {
|
|
222
|
+
enumerable: true,
|
|
223
|
+
get: function () { return emmett.NO_CONCURRENCY_CHECK; }
|
|
224
|
+
});
|
|
225
|
+
Object.defineProperty(exports, "STREAM_DOES_NOT_EXIST", {
|
|
226
|
+
enumerable: true,
|
|
227
|
+
get: function () { return emmett.STREAM_DOES_NOT_EXIST; }
|
|
228
|
+
});
|
|
229
|
+
Object.defineProperty(exports, "STREAM_EXISTS", {
|
|
230
|
+
enumerable: true,
|
|
231
|
+
get: function () { return emmett.STREAM_EXISTS; }
|
|
232
|
+
});
|
|
233
|
+
exports.ExpectedVersionConflictError = ExpectedVersionConflictError;
|
|
234
|
+
exports.assertExpectedVersionMatchesCurrent = assertExpectedVersionMatchesCurrent;
|
|
235
|
+
exports.calculateNextVersion = calculateNextVersion;
|
|
236
|
+
exports.getCurrentStreamVersion = getCurrentStreamVersion;
|
|
237
|
+
exports.getFirestoreEventStore = getFirestoreEventStore;
|
|
238
|
+
exports.padVersion = padVersion;
|
|
239
|
+
exports.parseStreamName = parseStreamName;
|
|
240
|
+
exports.timestampToDate = timestampToDate;
|
|
241
|
+
//# sourceMappingURL=index.js.map
|
|
242
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/eventStore/types.ts","../src/eventStore/utils.ts","../src/eventStore/firestoreEventStore.ts"],"names":["NO_CONCURRENCY_CHECK","STREAM_DOES_NOT_EXIST","STREAM_EXISTS"],"mappings":"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","file":"index.js","sourcesContent":["import type { Firestore, Timestamp } from '@google-cloud/firestore';\nimport type { Event, ReadEvent, ReadEventMetadataWithGlobalPosition } from '@event-driven-io/emmett';\nimport {\n STREAM_DOES_NOT_EXIST,\n STREAM_EXISTS,\n NO_CONCURRENCY_CHECK,\n type ExpectedStreamVersion as EmmettExpectedStreamVersion,\n} from '@event-driven-io/emmett';\n\n/**\n * Expected version for stream operations\n * Uses Emmett's standard version constants for full compatibility\n * - number | bigint: Expect specific version\n * - STREAM_DOES_NOT_EXIST: Stream must not exist\n * - STREAM_EXISTS: Stream must exist (any version)\n * - NO_CONCURRENCY_CHECK: No version check\n */\nexport type ExpectedStreamVersion = EmmettExpectedStreamVersion<bigint>;\n\n// Re-export Emmett constants for convenience\nexport { STREAM_DOES_NOT_EXIST, STREAM_EXISTS, NO_CONCURRENCY_CHECK };\n\n/**\n * Options for appending events to a stream\n */\nexport interface AppendToStreamOptions {\n expectedStreamVersion?: ExpectedStreamVersion;\n}\n\n/**\n * Result of appending events to a stream\n */\nexport interface AppendToStreamResult {\n nextExpectedStreamVersion: bigint;\n createdNewStream: boolean;\n}\n\n/**\n * Options for reading events from a stream\n */\nexport interface ReadStreamOptions {\n from?: bigint;\n to?: bigint;\n maxCount?: number;\n}\n\n/**\n * Metadata stored in Firestore stream document\n */\nexport interface StreamMetadata {\n version: number;\n createdAt: Timestamp;\n updatedAt: Timestamp;\n}\n\n/**\n * Event document structure in Firestore\n */\nexport interface EventDocument {\n type: string;\n data: Record<string, unknown>;\n metadata?: Record<string, unknown>;\n timestamp: Timestamp;\n globalPosition: number;\n streamVersion: number;\n}\n\n/**\n * Firestore-specific read event metadata\n */\nexport interface FirestoreReadEventMetadata extends ReadEventMetadataWithGlobalPosition {\n streamName: string;\n streamVersion: bigint;\n timestamp: Date;\n}\n\n/**\n * Firestore read event\n */\nexport type FirestoreReadEvent<EventType extends Event = Event> = ReadEvent<\n EventType,\n FirestoreReadEventMetadata\n>;\n\n/**\n * Collection configuration for Firestore event store\n */\nexport interface CollectionConfig {\n streams: string;\n counters: string;\n}\n\n/**\n * Firestore event store options\n */\nexport interface FirestoreEventStoreOptions {\n collections?: Partial<CollectionConfig>;\n}\n\n/**\n * Firestore event store interface\n */\nexport interface FirestoreEventStore {\n /**\n * The underlying Firestore instance\n */\n readonly firestore: Firestore;\n\n /**\n * Collection names configuration\n */\n readonly collections: CollectionConfig;\n\n /**\n * Read events from a stream\n */\n readStream<EventType extends Event>(\n streamName: string,\n options?: ReadStreamOptions,\n ): Promise<FirestoreReadEvent<EventType>[]>;\n\n /**\n * Aggregate stream by applying events to state\n */\n aggregateStream<State, EventType extends Event>(\n streamName: string,\n options: {\n evolve: (state: State, event: FirestoreReadEvent<EventType>) => State;\n initialState: () => State;\n read?: ReadStreamOptions;\n },\n ): Promise<{\n state: State;\n currentStreamVersion: bigint;\n streamExists: boolean;\n }>;\n\n /**\n * Append events to a stream\n */\n appendToStream<EventType extends Event>(\n streamName: string,\n events: EventType[],\n options?: AppendToStreamOptions,\n ): Promise<AppendToStreamResult>;\n}\n\n/**\n * Error thrown when expected version doesn't match current version\n */\nexport class ExpectedVersionConflictError extends Error {\n constructor(\n public readonly streamName: string,\n public readonly expected: ExpectedStreamVersion,\n public readonly actual: bigint | typeof STREAM_DOES_NOT_EXIST,\n ) {\n super(\n `Expected version conflict for stream '${streamName}': expected ${String(expected)}, actual ${String(actual)}`,\n );\n this.name = 'ExpectedVersionConflictError';\n Object.setPrototypeOf(this, ExpectedVersionConflictError.prototype);\n }\n}\n","import type { Timestamp } from '@google-cloud/firestore';\nimport type { ExpectedStreamVersion } from './types';\nimport {\n STREAM_DOES_NOT_EXIST,\n STREAM_EXISTS,\n NO_CONCURRENCY_CHECK,\n ExpectedVersionConflictError,\n} from './types';\n\n/**\n * Pad version number with leading zeros for Firestore document IDs\n * This ensures automatic ordering by version in Firestore\n *\n * @param version - The version number to pad\n * @returns Zero-padded string of length 10\n *\n * @example\n * padVersion(0) // \"0000000000\"\n * padVersion(42) // \"0000000042\"\n * padVersion(12345) // \"0000012345\"\n */\nexport function padVersion(version: number | bigint): string {\n return version.toString().padStart(10, '0');\n}\n\n/**\n * Parse a stream name into type and ID components\n *\n * @param streamName - Stream name in format \"Type-id\" or \"Type-with-dashes-id\"\n * @returns Object with streamType and streamId\n *\n * @example\n * parseStreamName(\"User-123\") // { streamType: \"User\", streamId: \"123\" }\n * parseStreamName(\"ShoppingCart-abc-def-123\") // { streamType: \"ShoppingCart\", streamId: \"abc-def-123\" }\n */\nexport function parseStreamName(streamName: string): {\n streamType: string;\n streamId: string;\n} {\n const firstDashIndex = streamName.indexOf('-');\n\n if (firstDashIndex === -1) {\n return {\n streamType: streamName,\n streamId: '',\n };\n }\n\n return {\n streamType: streamName.substring(0, firstDashIndex),\n streamId: streamName.substring(firstDashIndex + 1),\n };\n}\n\n/**\n * Convert Firestore Timestamp to JavaScript Date\n *\n * @param timestamp - Firestore Timestamp\n * @returns JavaScript Date object\n */\nexport function timestampToDate(timestamp: Timestamp): Date {\n return timestamp.toDate();\n}\n\n/**\n * Validate expected version against current version\n *\n * @param streamName - Stream name for error messages\n * @param expectedVersion - Expected version constraint\n * @param currentVersion - Current stream version (or STREAM_DOES_NOT_EXIST if stream doesn't exist)\n * @throws ExpectedVersionConflictError if versions don't match\n */\nexport function assertExpectedVersionMatchesCurrent(\n streamName: string,\n expectedVersion: ExpectedStreamVersion,\n currentVersion: bigint | typeof STREAM_DOES_NOT_EXIST,\n): void {\n // NO_CONCURRENCY_CHECK - no validation needed\n if (expectedVersion === NO_CONCURRENCY_CHECK) {\n return;\n }\n\n // STREAM_DOES_NOT_EXIST - stream must not exist\n if (expectedVersion === STREAM_DOES_NOT_EXIST) {\n if (currentVersion !== STREAM_DOES_NOT_EXIST) {\n throw new ExpectedVersionConflictError(\n streamName,\n expectedVersion,\n currentVersion,\n );\n }\n return;\n }\n\n // STREAM_EXISTS - stream must exist\n if (expectedVersion === STREAM_EXISTS) {\n if (currentVersion === STREAM_DOES_NOT_EXIST) {\n throw new ExpectedVersionConflictError(\n streamName,\n expectedVersion,\n currentVersion,\n );\n }\n return;\n }\n\n // Specific version number\n const expectedBigInt = BigInt(expectedVersion);\n if (currentVersion === STREAM_DOES_NOT_EXIST || currentVersion !== expectedBigInt) {\n throw new ExpectedVersionConflictError(\n streamName,\n expectedVersion,\n currentVersion,\n );\n }\n}\n\n/**\n * Get the current stream version from metadata\n *\n * @param streamExists - Whether the stream document exists\n * @param version - Version number from Firestore (if stream exists)\n * @returns Current version as bigint or STREAM_DOES_NOT_EXIST\n */\nexport function getCurrentStreamVersion(\n streamExists: boolean,\n version?: number,\n): bigint | typeof STREAM_DOES_NOT_EXIST {\n if (!streamExists) {\n return STREAM_DOES_NOT_EXIST;\n }\n return BigInt(version ?? -1);\n}\n\n/**\n * Calculate the next expected stream version after appending events\n *\n * @param currentVersion - Current stream version\n * @param eventCount - Number of events being appended\n * @returns Next expected version as bigint\n */\nexport function calculateNextVersion(\n currentVersion: bigint | typeof STREAM_DOES_NOT_EXIST,\n eventCount: number,\n): bigint {\n if (currentVersion === STREAM_DOES_NOT_EXIST) {\n return BigInt(eventCount - 1);\n }\n // Type assertion needed because TypeScript doesn't narrow ExpectedStreamVersionGeneral properly\n return (currentVersion as bigint) + BigInt(eventCount);\n}\n","import type { Firestore, Transaction, Timestamp } from '@google-cloud/firestore';\nimport type { Event } from '@event-driven-io/emmett';\nimport type {\n AppendToStreamOptions,\n AppendToStreamResult,\n CollectionConfig,\n EventDocument,\n ExpectedStreamVersion,\n FirestoreEventStore,\n FirestoreEventStoreOptions,\n FirestoreReadEvent,\n ReadStreamOptions,\n StreamMetadata,\n} from './types';\nimport { NO_CONCURRENCY_CHECK, STREAM_DOES_NOT_EXIST } from './types';\nimport {\n assertExpectedVersionMatchesCurrent,\n getCurrentStreamVersion,\n padVersion,\n timestampToDate,\n} from './utils';\n\nconst DEFAULT_COLLECTIONS: CollectionConfig = {\n streams: 'streams',\n counters: '_counters',\n};\n\n/**\n * Firestore Event Store Implementation\n *\n * Stores events in Firestore using a subcollection pattern:\n * - /streams/{streamName} - Stream metadata (version, timestamps)\n * - /streams/{streamName}/events/{version} - Individual events\n * - /_counters/global_position - Global event counter\n */\nexport class FirestoreEventStoreImpl implements FirestoreEventStore {\n public readonly collections: CollectionConfig;\n\n constructor(\n public readonly firestore: Firestore,\n options: FirestoreEventStoreOptions = {},\n ) {\n this.collections = {\n ...DEFAULT_COLLECTIONS,\n ...options.collections,\n };\n }\n\n /**\n * Read events from a stream\n */\n async readStream<EventType extends Event>(\n streamName: string,\n options: ReadStreamOptions = {},\n ): Promise<FirestoreReadEvent<EventType>[]> {\n const { from, to, maxCount } = options;\n\n // Reference to events subcollection\n let query = this.firestore\n .collection(this.collections.streams)\n .doc(streamName)\n .collection('events')\n .orderBy('streamVersion', 'asc');\n\n // Apply range filters\n if (from !== undefined) {\n query = query.where('streamVersion', '>=', Number(from));\n }\n if (to !== undefined) {\n query = query.where('streamVersion', '<=', Number(to));\n }\n if (maxCount !== undefined && maxCount > 0) {\n query = query.limit(maxCount);\n }\n\n // Execute query\n const snapshot = await query.get();\n\n // Transform Firestore documents to events\n return snapshot.docs.map((doc) => {\n const data = doc.data() as EventDocument;\n return {\n type: data.type,\n data: data.data,\n metadata: {\n ...data.metadata,\n streamName,\n streamVersion: BigInt(data.streamVersion),\n streamPosition: BigInt(data.streamVersion),\n globalPosition: BigInt(data.globalPosition),\n timestamp: timestampToDate(data.timestamp),\n },\n } as FirestoreReadEvent<EventType>;\n });\n }\n\n /**\n * Aggregate stream by applying events to state\n */\n async aggregateStream<State, EventType extends Event>(\n streamName: string,\n options: {\n evolve: (state: State, event: FirestoreReadEvent<EventType>) => State;\n initialState: () => State;\n read?: ReadStreamOptions;\n },\n ): Promise<{\n state: State;\n currentStreamVersion: bigint;\n streamExists: boolean;\n }> {\n const { evolve, initialState, read } = options;\n const events = await this.readStream<EventType>(streamName, read);\n\n const streamExists = events.length > 0;\n const state = events.reduce(evolve, initialState());\n const currentStreamVersion = streamExists\n ? events[events.length - 1].metadata.streamVersion\n : BigInt(0);\n\n return {\n state,\n currentStreamVersion,\n streamExists,\n };\n }\n\n /**\n * Append events to a stream with optimistic concurrency control\n */\n async appendToStream<EventType extends Event>(\n streamName: string,\n events: EventType[],\n options: AppendToStreamOptions = {},\n ): Promise<AppendToStreamResult> {\n if (events.length === 0) {\n throw new Error('Cannot append empty event array');\n }\n\n const { expectedStreamVersion = NO_CONCURRENCY_CHECK } = options;\n\n // Execute in transaction for atomicity\n return await this.firestore.runTransaction(async (transaction) => {\n return await this.appendToStreamInTransaction(\n transaction,\n streamName,\n events,\n expectedStreamVersion,\n );\n });\n }\n\n /**\n * Internal method to append events within a transaction\n */\n private async appendToStreamInTransaction<EventType extends Event>(\n transaction: Transaction,\n streamName: string,\n events: EventType[],\n expectedStreamVersion: ExpectedStreamVersion,\n ): Promise<AppendToStreamResult> {\n // 1. Get stream metadata reference\n const streamRef = this.firestore\n .collection(this.collections.streams)\n .doc(streamName);\n\n const streamDoc = await transaction.get(streamRef);\n const streamExists = streamDoc.exists;\n const streamData = streamDoc.data() as StreamMetadata | undefined;\n\n // 2. Get current version and validate expected version\n const currentVersion = getCurrentStreamVersion(\n streamExists,\n streamData?.version,\n );\n\n assertExpectedVersionMatchesCurrent(\n streamName,\n expectedStreamVersion,\n currentVersion,\n );\n\n // 3. Get and increment global position counter\n const counterRef = this.firestore\n .collection(this.collections.counters)\n .doc('global_position');\n\n const counterDoc = await transaction.get(counterRef);\n let globalPosition = counterDoc.exists\n ? (counterDoc.data()?.value as number) ?? 0\n : 0;\n\n // 4. Calculate starting version for new events\n const baseVersion =\n currentVersion === STREAM_DOES_NOT_EXIST ? -1 : Number(currentVersion);\n\n // 5. Append events to subcollection\n const TimestampClass = this.firestore.constructor as unknown as { Timestamp: typeof Timestamp };\n const now = TimestampClass.Timestamp.now();\n\n events.forEach((event, index) => {\n const eventVersion = baseVersion + 1 + index;\n const eventRef = streamRef\n .collection('events')\n .doc(padVersion(eventVersion));\n\n const metadata = (event as { metadata?: Record<string, unknown> }).metadata;\n const eventDocument: EventDocument = {\n type: event.type,\n data: event.data,\n ...(metadata && { metadata }),\n timestamp: now,\n globalPosition: globalPosition++,\n streamVersion: eventVersion,\n };\n\n transaction.set(eventRef, eventDocument);\n });\n\n // 6. Update stream metadata\n const newVersion = baseVersion + events.length;\n const updatedMetadata: StreamMetadata = {\n version: newVersion,\n createdAt: streamData?.createdAt ?? now,\n updatedAt: now,\n };\n\n transaction.set(streamRef, updatedMetadata);\n\n // 7. Update global position counter\n transaction.set(counterRef, {\n value: globalPosition,\n updatedAt: now,\n });\n\n // 8. Return result\n return {\n nextExpectedStreamVersion: BigInt(newVersion),\n createdNewStream: !streamExists,\n };\n }\n}\n\n/**\n * Factory function to create a Firestore event store\n *\n * @param firestore - Firestore instance\n * @param options - Optional configuration\n * @returns Firestore event store instance\n *\n * @example\n * ```typescript\n * import { Firestore } from '@google-cloud/firestore';\n * import { getFirestoreEventStore } from '@emmett-community/emmett-google-firestore';\n *\n * const firestore = new Firestore({ projectId: 'my-project' });\n * const eventStore = getFirestoreEventStore(firestore);\n * ```\n */\nexport function getFirestoreEventStore(\n firestore: Firestore,\n options?: FirestoreEventStoreOptions,\n): FirestoreEventStore {\n return new FirestoreEventStoreImpl(firestore, options);\n}\n"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { NO_CONCURRENCY_CHECK, STREAM_DOES_NOT_EXIST, STREAM_EXISTS } from '@event-driven-io/emmett';
|
|
2
|
+
export { NO_CONCURRENCY_CHECK, STREAM_DOES_NOT_EXIST, STREAM_EXISTS } from '@event-driven-io/emmett';
|
|
3
|
+
|
|
4
|
+
// src/eventStore/types.ts
|
|
5
|
+
var ExpectedVersionConflictError = class _ExpectedVersionConflictError extends Error {
|
|
6
|
+
constructor(streamName, expected, actual) {
|
|
7
|
+
super(
|
|
8
|
+
`Expected version conflict for stream '${streamName}': expected ${String(expected)}, actual ${String(actual)}`
|
|
9
|
+
);
|
|
10
|
+
this.streamName = streamName;
|
|
11
|
+
this.expected = expected;
|
|
12
|
+
this.actual = actual;
|
|
13
|
+
this.name = "ExpectedVersionConflictError";
|
|
14
|
+
Object.setPrototypeOf(this, _ExpectedVersionConflictError.prototype);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/eventStore/utils.ts
|
|
19
|
+
function padVersion(version) {
|
|
20
|
+
return version.toString().padStart(10, "0");
|
|
21
|
+
}
|
|
22
|
+
function parseStreamName(streamName) {
|
|
23
|
+
const firstDashIndex = streamName.indexOf("-");
|
|
24
|
+
if (firstDashIndex === -1) {
|
|
25
|
+
return {
|
|
26
|
+
streamType: streamName,
|
|
27
|
+
streamId: ""
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
streamType: streamName.substring(0, firstDashIndex),
|
|
32
|
+
streamId: streamName.substring(firstDashIndex + 1)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function timestampToDate(timestamp) {
|
|
36
|
+
return timestamp.toDate();
|
|
37
|
+
}
|
|
38
|
+
function assertExpectedVersionMatchesCurrent(streamName, expectedVersion, currentVersion) {
|
|
39
|
+
if (expectedVersion === NO_CONCURRENCY_CHECK) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (expectedVersion === STREAM_DOES_NOT_EXIST) {
|
|
43
|
+
if (currentVersion !== STREAM_DOES_NOT_EXIST) {
|
|
44
|
+
throw new ExpectedVersionConflictError(
|
|
45
|
+
streamName,
|
|
46
|
+
expectedVersion,
|
|
47
|
+
currentVersion
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (expectedVersion === STREAM_EXISTS) {
|
|
53
|
+
if (currentVersion === STREAM_DOES_NOT_EXIST) {
|
|
54
|
+
throw new ExpectedVersionConflictError(
|
|
55
|
+
streamName,
|
|
56
|
+
expectedVersion,
|
|
57
|
+
currentVersion
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const expectedBigInt = BigInt(expectedVersion);
|
|
63
|
+
if (currentVersion === STREAM_DOES_NOT_EXIST || currentVersion !== expectedBigInt) {
|
|
64
|
+
throw new ExpectedVersionConflictError(
|
|
65
|
+
streamName,
|
|
66
|
+
expectedVersion,
|
|
67
|
+
currentVersion
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function getCurrentStreamVersion(streamExists, version) {
|
|
72
|
+
if (!streamExists) {
|
|
73
|
+
return STREAM_DOES_NOT_EXIST;
|
|
74
|
+
}
|
|
75
|
+
return BigInt(version ?? -1);
|
|
76
|
+
}
|
|
77
|
+
function calculateNextVersion(currentVersion, eventCount) {
|
|
78
|
+
if (currentVersion === STREAM_DOES_NOT_EXIST) {
|
|
79
|
+
return BigInt(eventCount - 1);
|
|
80
|
+
}
|
|
81
|
+
return currentVersion + BigInt(eventCount);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/eventStore/firestoreEventStore.ts
|
|
85
|
+
var DEFAULT_COLLECTIONS = {
|
|
86
|
+
streams: "streams",
|
|
87
|
+
counters: "_counters"
|
|
88
|
+
};
|
|
89
|
+
var FirestoreEventStoreImpl = class {
|
|
90
|
+
constructor(firestore, options = {}) {
|
|
91
|
+
this.firestore = firestore;
|
|
92
|
+
this.collections = {
|
|
93
|
+
...DEFAULT_COLLECTIONS,
|
|
94
|
+
...options.collections
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
collections;
|
|
98
|
+
/**
|
|
99
|
+
* Read events from a stream
|
|
100
|
+
*/
|
|
101
|
+
async readStream(streamName, options = {}) {
|
|
102
|
+
const { from, to, maxCount } = options;
|
|
103
|
+
let query = this.firestore.collection(this.collections.streams).doc(streamName).collection("events").orderBy("streamVersion", "asc");
|
|
104
|
+
if (from !== void 0) {
|
|
105
|
+
query = query.where("streamVersion", ">=", Number(from));
|
|
106
|
+
}
|
|
107
|
+
if (to !== void 0) {
|
|
108
|
+
query = query.where("streamVersion", "<=", Number(to));
|
|
109
|
+
}
|
|
110
|
+
if (maxCount !== void 0 && maxCount > 0) {
|
|
111
|
+
query = query.limit(maxCount);
|
|
112
|
+
}
|
|
113
|
+
const snapshot = await query.get();
|
|
114
|
+
return snapshot.docs.map((doc) => {
|
|
115
|
+
const data = doc.data();
|
|
116
|
+
return {
|
|
117
|
+
type: data.type,
|
|
118
|
+
data: data.data,
|
|
119
|
+
metadata: {
|
|
120
|
+
...data.metadata,
|
|
121
|
+
streamName,
|
|
122
|
+
streamVersion: BigInt(data.streamVersion),
|
|
123
|
+
streamPosition: BigInt(data.streamVersion),
|
|
124
|
+
globalPosition: BigInt(data.globalPosition),
|
|
125
|
+
timestamp: timestampToDate(data.timestamp)
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Aggregate stream by applying events to state
|
|
132
|
+
*/
|
|
133
|
+
async aggregateStream(streamName, options) {
|
|
134
|
+
const { evolve, initialState, read } = options;
|
|
135
|
+
const events = await this.readStream(streamName, read);
|
|
136
|
+
const streamExists = events.length > 0;
|
|
137
|
+
const state = events.reduce(evolve, initialState());
|
|
138
|
+
const currentStreamVersion = streamExists ? events[events.length - 1].metadata.streamVersion : BigInt(0);
|
|
139
|
+
return {
|
|
140
|
+
state,
|
|
141
|
+
currentStreamVersion,
|
|
142
|
+
streamExists
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Append events to a stream with optimistic concurrency control
|
|
147
|
+
*/
|
|
148
|
+
async appendToStream(streamName, events, options = {}) {
|
|
149
|
+
if (events.length === 0) {
|
|
150
|
+
throw new Error("Cannot append empty event array");
|
|
151
|
+
}
|
|
152
|
+
const { expectedStreamVersion = NO_CONCURRENCY_CHECK } = options;
|
|
153
|
+
return await this.firestore.runTransaction(async (transaction) => {
|
|
154
|
+
return await this.appendToStreamInTransaction(
|
|
155
|
+
transaction,
|
|
156
|
+
streamName,
|
|
157
|
+
events,
|
|
158
|
+
expectedStreamVersion
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Internal method to append events within a transaction
|
|
164
|
+
*/
|
|
165
|
+
async appendToStreamInTransaction(transaction, streamName, events, expectedStreamVersion) {
|
|
166
|
+
const streamRef = this.firestore.collection(this.collections.streams).doc(streamName);
|
|
167
|
+
const streamDoc = await transaction.get(streamRef);
|
|
168
|
+
const streamExists = streamDoc.exists;
|
|
169
|
+
const streamData = streamDoc.data();
|
|
170
|
+
const currentVersion = getCurrentStreamVersion(
|
|
171
|
+
streamExists,
|
|
172
|
+
streamData?.version
|
|
173
|
+
);
|
|
174
|
+
assertExpectedVersionMatchesCurrent(
|
|
175
|
+
streamName,
|
|
176
|
+
expectedStreamVersion,
|
|
177
|
+
currentVersion
|
|
178
|
+
);
|
|
179
|
+
const counterRef = this.firestore.collection(this.collections.counters).doc("global_position");
|
|
180
|
+
const counterDoc = await transaction.get(counterRef);
|
|
181
|
+
let globalPosition = counterDoc.exists ? counterDoc.data()?.value ?? 0 : 0;
|
|
182
|
+
const baseVersion = currentVersion === STREAM_DOES_NOT_EXIST ? -1 : Number(currentVersion);
|
|
183
|
+
const TimestampClass = this.firestore.constructor;
|
|
184
|
+
const now = TimestampClass.Timestamp.now();
|
|
185
|
+
events.forEach((event, index) => {
|
|
186
|
+
const eventVersion = baseVersion + 1 + index;
|
|
187
|
+
const eventRef = streamRef.collection("events").doc(padVersion(eventVersion));
|
|
188
|
+
const metadata = event.metadata;
|
|
189
|
+
const eventDocument = {
|
|
190
|
+
type: event.type,
|
|
191
|
+
data: event.data,
|
|
192
|
+
...metadata && { metadata },
|
|
193
|
+
timestamp: now,
|
|
194
|
+
globalPosition: globalPosition++,
|
|
195
|
+
streamVersion: eventVersion
|
|
196
|
+
};
|
|
197
|
+
transaction.set(eventRef, eventDocument);
|
|
198
|
+
});
|
|
199
|
+
const newVersion = baseVersion + events.length;
|
|
200
|
+
const updatedMetadata = {
|
|
201
|
+
version: newVersion,
|
|
202
|
+
createdAt: streamData?.createdAt ?? now,
|
|
203
|
+
updatedAt: now
|
|
204
|
+
};
|
|
205
|
+
transaction.set(streamRef, updatedMetadata);
|
|
206
|
+
transaction.set(counterRef, {
|
|
207
|
+
value: globalPosition,
|
|
208
|
+
updatedAt: now
|
|
209
|
+
});
|
|
210
|
+
return {
|
|
211
|
+
nextExpectedStreamVersion: BigInt(newVersion),
|
|
212
|
+
createdNewStream: !streamExists
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
function getFirestoreEventStore(firestore, options) {
|
|
217
|
+
return new FirestoreEventStoreImpl(firestore, options);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export { ExpectedVersionConflictError, assertExpectedVersionMatchesCurrent, calculateNextVersion, getCurrentStreamVersion, getFirestoreEventStore, padVersion, parseStreamName, timestampToDate };
|
|
221
|
+
//# sourceMappingURL=index.mjs.map
|
|
222
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/eventStore/types.ts","../src/eventStore/utils.ts","../src/eventStore/firestoreEventStore.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import type { Firestore, Timestamp } from '@google-cloud/firestore';\nimport type { Event, ReadEvent, ReadEventMetadataWithGlobalPosition } from '@event-driven-io/emmett';\nimport {\n STREAM_DOES_NOT_EXIST,\n STREAM_EXISTS,\n NO_CONCURRENCY_CHECK,\n type ExpectedStreamVersion as EmmettExpectedStreamVersion,\n} from '@event-driven-io/emmett';\n\n/**\n * Expected version for stream operations\n * Uses Emmett's standard version constants for full compatibility\n * - number | bigint: Expect specific version\n * - STREAM_DOES_NOT_EXIST: Stream must not exist\n * - STREAM_EXISTS: Stream must exist (any version)\n * - NO_CONCURRENCY_CHECK: No version check\n */\nexport type ExpectedStreamVersion = EmmettExpectedStreamVersion<bigint>;\n\n// Re-export Emmett constants for convenience\nexport { STREAM_DOES_NOT_EXIST, STREAM_EXISTS, NO_CONCURRENCY_CHECK };\n\n/**\n * Options for appending events to a stream\n */\nexport interface AppendToStreamOptions {\n expectedStreamVersion?: ExpectedStreamVersion;\n}\n\n/**\n * Result of appending events to a stream\n */\nexport interface AppendToStreamResult {\n nextExpectedStreamVersion: bigint;\n createdNewStream: boolean;\n}\n\n/**\n * Options for reading events from a stream\n */\nexport interface ReadStreamOptions {\n from?: bigint;\n to?: bigint;\n maxCount?: number;\n}\n\n/**\n * Metadata stored in Firestore stream document\n */\nexport interface StreamMetadata {\n version: number;\n createdAt: Timestamp;\n updatedAt: Timestamp;\n}\n\n/**\n * Event document structure in Firestore\n */\nexport interface EventDocument {\n type: string;\n data: Record<string, unknown>;\n metadata?: Record<string, unknown>;\n timestamp: Timestamp;\n globalPosition: number;\n streamVersion: number;\n}\n\n/**\n * Firestore-specific read event metadata\n */\nexport interface FirestoreReadEventMetadata extends ReadEventMetadataWithGlobalPosition {\n streamName: string;\n streamVersion: bigint;\n timestamp: Date;\n}\n\n/**\n * Firestore read event\n */\nexport type FirestoreReadEvent<EventType extends Event = Event> = ReadEvent<\n EventType,\n FirestoreReadEventMetadata\n>;\n\n/**\n * Collection configuration for Firestore event store\n */\nexport interface CollectionConfig {\n streams: string;\n counters: string;\n}\n\n/**\n * Firestore event store options\n */\nexport interface FirestoreEventStoreOptions {\n collections?: Partial<CollectionConfig>;\n}\n\n/**\n * Firestore event store interface\n */\nexport interface FirestoreEventStore {\n /**\n * The underlying Firestore instance\n */\n readonly firestore: Firestore;\n\n /**\n * Collection names configuration\n */\n readonly collections: CollectionConfig;\n\n /**\n * Read events from a stream\n */\n readStream<EventType extends Event>(\n streamName: string,\n options?: ReadStreamOptions,\n ): Promise<FirestoreReadEvent<EventType>[]>;\n\n /**\n * Aggregate stream by applying events to state\n */\n aggregateStream<State, EventType extends Event>(\n streamName: string,\n options: {\n evolve: (state: State, event: FirestoreReadEvent<EventType>) => State;\n initialState: () => State;\n read?: ReadStreamOptions;\n },\n ): Promise<{\n state: State;\n currentStreamVersion: bigint;\n streamExists: boolean;\n }>;\n\n /**\n * Append events to a stream\n */\n appendToStream<EventType extends Event>(\n streamName: string,\n events: EventType[],\n options?: AppendToStreamOptions,\n ): Promise<AppendToStreamResult>;\n}\n\n/**\n * Error thrown when expected version doesn't match current version\n */\nexport class ExpectedVersionConflictError extends Error {\n constructor(\n public readonly streamName: string,\n public readonly expected: ExpectedStreamVersion,\n public readonly actual: bigint | typeof STREAM_DOES_NOT_EXIST,\n ) {\n super(\n `Expected version conflict for stream '${streamName}': expected ${String(expected)}, actual ${String(actual)}`,\n );\n this.name = 'ExpectedVersionConflictError';\n Object.setPrototypeOf(this, ExpectedVersionConflictError.prototype);\n }\n}\n","import type { Timestamp } from '@google-cloud/firestore';\nimport type { ExpectedStreamVersion } from './types';\nimport {\n STREAM_DOES_NOT_EXIST,\n STREAM_EXISTS,\n NO_CONCURRENCY_CHECK,\n ExpectedVersionConflictError,\n} from './types';\n\n/**\n * Pad version number with leading zeros for Firestore document IDs\n * This ensures automatic ordering by version in Firestore\n *\n * @param version - The version number to pad\n * @returns Zero-padded string of length 10\n *\n * @example\n * padVersion(0) // \"0000000000\"\n * padVersion(42) // \"0000000042\"\n * padVersion(12345) // \"0000012345\"\n */\nexport function padVersion(version: number | bigint): string {\n return version.toString().padStart(10, '0');\n}\n\n/**\n * Parse a stream name into type and ID components\n *\n * @param streamName - Stream name in format \"Type-id\" or \"Type-with-dashes-id\"\n * @returns Object with streamType and streamId\n *\n * @example\n * parseStreamName(\"User-123\") // { streamType: \"User\", streamId: \"123\" }\n * parseStreamName(\"ShoppingCart-abc-def-123\") // { streamType: \"ShoppingCart\", streamId: \"abc-def-123\" }\n */\nexport function parseStreamName(streamName: string): {\n streamType: string;\n streamId: string;\n} {\n const firstDashIndex = streamName.indexOf('-');\n\n if (firstDashIndex === -1) {\n return {\n streamType: streamName,\n streamId: '',\n };\n }\n\n return {\n streamType: streamName.substring(0, firstDashIndex),\n streamId: streamName.substring(firstDashIndex + 1),\n };\n}\n\n/**\n * Convert Firestore Timestamp to JavaScript Date\n *\n * @param timestamp - Firestore Timestamp\n * @returns JavaScript Date object\n */\nexport function timestampToDate(timestamp: Timestamp): Date {\n return timestamp.toDate();\n}\n\n/**\n * Validate expected version against current version\n *\n * @param streamName - Stream name for error messages\n * @param expectedVersion - Expected version constraint\n * @param currentVersion - Current stream version (or STREAM_DOES_NOT_EXIST if stream doesn't exist)\n * @throws ExpectedVersionConflictError if versions don't match\n */\nexport function assertExpectedVersionMatchesCurrent(\n streamName: string,\n expectedVersion: ExpectedStreamVersion,\n currentVersion: bigint | typeof STREAM_DOES_NOT_EXIST,\n): void {\n // NO_CONCURRENCY_CHECK - no validation needed\n if (expectedVersion === NO_CONCURRENCY_CHECK) {\n return;\n }\n\n // STREAM_DOES_NOT_EXIST - stream must not exist\n if (expectedVersion === STREAM_DOES_NOT_EXIST) {\n if (currentVersion !== STREAM_DOES_NOT_EXIST) {\n throw new ExpectedVersionConflictError(\n streamName,\n expectedVersion,\n currentVersion,\n );\n }\n return;\n }\n\n // STREAM_EXISTS - stream must exist\n if (expectedVersion === STREAM_EXISTS) {\n if (currentVersion === STREAM_DOES_NOT_EXIST) {\n throw new ExpectedVersionConflictError(\n streamName,\n expectedVersion,\n currentVersion,\n );\n }\n return;\n }\n\n // Specific version number\n const expectedBigInt = BigInt(expectedVersion);\n if (currentVersion === STREAM_DOES_NOT_EXIST || currentVersion !== expectedBigInt) {\n throw new ExpectedVersionConflictError(\n streamName,\n expectedVersion,\n currentVersion,\n );\n }\n}\n\n/**\n * Get the current stream version from metadata\n *\n * @param streamExists - Whether the stream document exists\n * @param version - Version number from Firestore (if stream exists)\n * @returns Current version as bigint or STREAM_DOES_NOT_EXIST\n */\nexport function getCurrentStreamVersion(\n streamExists: boolean,\n version?: number,\n): bigint | typeof STREAM_DOES_NOT_EXIST {\n if (!streamExists) {\n return STREAM_DOES_NOT_EXIST;\n }\n return BigInt(version ?? -1);\n}\n\n/**\n * Calculate the next expected stream version after appending events\n *\n * @param currentVersion - Current stream version\n * @param eventCount - Number of events being appended\n * @returns Next expected version as bigint\n */\nexport function calculateNextVersion(\n currentVersion: bigint | typeof STREAM_DOES_NOT_EXIST,\n eventCount: number,\n): bigint {\n if (currentVersion === STREAM_DOES_NOT_EXIST) {\n return BigInt(eventCount - 1);\n }\n // Type assertion needed because TypeScript doesn't narrow ExpectedStreamVersionGeneral properly\n return (currentVersion as bigint) + BigInt(eventCount);\n}\n","import type { Firestore, Transaction, Timestamp } from '@google-cloud/firestore';\nimport type { Event } from '@event-driven-io/emmett';\nimport type {\n AppendToStreamOptions,\n AppendToStreamResult,\n CollectionConfig,\n EventDocument,\n ExpectedStreamVersion,\n FirestoreEventStore,\n FirestoreEventStoreOptions,\n FirestoreReadEvent,\n ReadStreamOptions,\n StreamMetadata,\n} from './types';\nimport { NO_CONCURRENCY_CHECK, STREAM_DOES_NOT_EXIST } from './types';\nimport {\n assertExpectedVersionMatchesCurrent,\n getCurrentStreamVersion,\n padVersion,\n timestampToDate,\n} from './utils';\n\nconst DEFAULT_COLLECTIONS: CollectionConfig = {\n streams: 'streams',\n counters: '_counters',\n};\n\n/**\n * Firestore Event Store Implementation\n *\n * Stores events in Firestore using a subcollection pattern:\n * - /streams/{streamName} - Stream metadata (version, timestamps)\n * - /streams/{streamName}/events/{version} - Individual events\n * - /_counters/global_position - Global event counter\n */\nexport class FirestoreEventStoreImpl implements FirestoreEventStore {\n public readonly collections: CollectionConfig;\n\n constructor(\n public readonly firestore: Firestore,\n options: FirestoreEventStoreOptions = {},\n ) {\n this.collections = {\n ...DEFAULT_COLLECTIONS,\n ...options.collections,\n };\n }\n\n /**\n * Read events from a stream\n */\n async readStream<EventType extends Event>(\n streamName: string,\n options: ReadStreamOptions = {},\n ): Promise<FirestoreReadEvent<EventType>[]> {\n const { from, to, maxCount } = options;\n\n // Reference to events subcollection\n let query = this.firestore\n .collection(this.collections.streams)\n .doc(streamName)\n .collection('events')\n .orderBy('streamVersion', 'asc');\n\n // Apply range filters\n if (from !== undefined) {\n query = query.where('streamVersion', '>=', Number(from));\n }\n if (to !== undefined) {\n query = query.where('streamVersion', '<=', Number(to));\n }\n if (maxCount !== undefined && maxCount > 0) {\n query = query.limit(maxCount);\n }\n\n // Execute query\n const snapshot = await query.get();\n\n // Transform Firestore documents to events\n return snapshot.docs.map((doc) => {\n const data = doc.data() as EventDocument;\n return {\n type: data.type,\n data: data.data,\n metadata: {\n ...data.metadata,\n streamName,\n streamVersion: BigInt(data.streamVersion),\n streamPosition: BigInt(data.streamVersion),\n globalPosition: BigInt(data.globalPosition),\n timestamp: timestampToDate(data.timestamp),\n },\n } as FirestoreReadEvent<EventType>;\n });\n }\n\n /**\n * Aggregate stream by applying events to state\n */\n async aggregateStream<State, EventType extends Event>(\n streamName: string,\n options: {\n evolve: (state: State, event: FirestoreReadEvent<EventType>) => State;\n initialState: () => State;\n read?: ReadStreamOptions;\n },\n ): Promise<{\n state: State;\n currentStreamVersion: bigint;\n streamExists: boolean;\n }> {\n const { evolve, initialState, read } = options;\n const events = await this.readStream<EventType>(streamName, read);\n\n const streamExists = events.length > 0;\n const state = events.reduce(evolve, initialState());\n const currentStreamVersion = streamExists\n ? events[events.length - 1].metadata.streamVersion\n : BigInt(0);\n\n return {\n state,\n currentStreamVersion,\n streamExists,\n };\n }\n\n /**\n * Append events to a stream with optimistic concurrency control\n */\n async appendToStream<EventType extends Event>(\n streamName: string,\n events: EventType[],\n options: AppendToStreamOptions = {},\n ): Promise<AppendToStreamResult> {\n if (events.length === 0) {\n throw new Error('Cannot append empty event array');\n }\n\n const { expectedStreamVersion = NO_CONCURRENCY_CHECK } = options;\n\n // Execute in transaction for atomicity\n return await this.firestore.runTransaction(async (transaction) => {\n return await this.appendToStreamInTransaction(\n transaction,\n streamName,\n events,\n expectedStreamVersion,\n );\n });\n }\n\n /**\n * Internal method to append events within a transaction\n */\n private async appendToStreamInTransaction<EventType extends Event>(\n transaction: Transaction,\n streamName: string,\n events: EventType[],\n expectedStreamVersion: ExpectedStreamVersion,\n ): Promise<AppendToStreamResult> {\n // 1. Get stream metadata reference\n const streamRef = this.firestore\n .collection(this.collections.streams)\n .doc(streamName);\n\n const streamDoc = await transaction.get(streamRef);\n const streamExists = streamDoc.exists;\n const streamData = streamDoc.data() as StreamMetadata | undefined;\n\n // 2. Get current version and validate expected version\n const currentVersion = getCurrentStreamVersion(\n streamExists,\n streamData?.version,\n );\n\n assertExpectedVersionMatchesCurrent(\n streamName,\n expectedStreamVersion,\n currentVersion,\n );\n\n // 3. Get and increment global position counter\n const counterRef = this.firestore\n .collection(this.collections.counters)\n .doc('global_position');\n\n const counterDoc = await transaction.get(counterRef);\n let globalPosition = counterDoc.exists\n ? (counterDoc.data()?.value as number) ?? 0\n : 0;\n\n // 4. Calculate starting version for new events\n const baseVersion =\n currentVersion === STREAM_DOES_NOT_EXIST ? -1 : Number(currentVersion);\n\n // 5. Append events to subcollection\n const TimestampClass = this.firestore.constructor as unknown as { Timestamp: typeof Timestamp };\n const now = TimestampClass.Timestamp.now();\n\n events.forEach((event, index) => {\n const eventVersion = baseVersion + 1 + index;\n const eventRef = streamRef\n .collection('events')\n .doc(padVersion(eventVersion));\n\n const metadata = (event as { metadata?: Record<string, unknown> }).metadata;\n const eventDocument: EventDocument = {\n type: event.type,\n data: event.data,\n ...(metadata && { metadata }),\n timestamp: now,\n globalPosition: globalPosition++,\n streamVersion: eventVersion,\n };\n\n transaction.set(eventRef, eventDocument);\n });\n\n // 6. Update stream metadata\n const newVersion = baseVersion + events.length;\n const updatedMetadata: StreamMetadata = {\n version: newVersion,\n createdAt: streamData?.createdAt ?? now,\n updatedAt: now,\n };\n\n transaction.set(streamRef, updatedMetadata);\n\n // 7. Update global position counter\n transaction.set(counterRef, {\n value: globalPosition,\n updatedAt: now,\n });\n\n // 8. Return result\n return {\n nextExpectedStreamVersion: BigInt(newVersion),\n createdNewStream: !streamExists,\n };\n }\n}\n\n/**\n * Factory function to create a Firestore event store\n *\n * @param firestore - Firestore instance\n * @param options - Optional configuration\n * @returns Firestore event store instance\n *\n * @example\n * ```typescript\n * import { Firestore } from '@google-cloud/firestore';\n * import { getFirestoreEventStore } from '@emmett-community/emmett-google-firestore';\n *\n * const firestore = new Firestore({ projectId: 'my-project' });\n * const eventStore = getFirestoreEventStore(firestore);\n * ```\n */\nexport function getFirestoreEventStore(\n firestore: Firestore,\n options?: FirestoreEventStoreOptions,\n): FirestoreEventStore {\n return new FirestoreEventStoreImpl(firestore, options);\n}\n"]}
|