@emmett-community/emmett-google-firestore 0.1.0 → 0.2.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.
@@ -1,296 +0,0 @@
1
- 'use strict';
2
-
3
- var firestore = require('@google-cloud/firestore');
4
- var emmett = require('@event-driven-io/emmett');
5
-
6
- // src/testing/eventStoreSpec.ts
7
- var ExpectedVersionConflictError = class _ExpectedVersionConflictError extends Error {
8
- constructor(streamName, expected, actual) {
9
- super(
10
- `Expected version conflict for stream '${streamName}': expected ${String(expected)}, actual ${String(actual)}`
11
- );
12
- this.streamName = streamName;
13
- this.expected = expected;
14
- this.actual = actual;
15
- this.name = "ExpectedVersionConflictError";
16
- Object.setPrototypeOf(this, _ExpectedVersionConflictError.prototype);
17
- }
18
- };
19
-
20
- // src/eventStore/utils.ts
21
- function padVersion(version) {
22
- return version.toString().padStart(10, "0");
23
- }
24
- function timestampToDate(timestamp) {
25
- return timestamp.toDate();
26
- }
27
- function assertExpectedVersionMatchesCurrent(streamName, expectedVersion, currentVersion) {
28
- if (expectedVersion === emmett.NO_CONCURRENCY_CHECK) {
29
- return;
30
- }
31
- if (expectedVersion === emmett.STREAM_DOES_NOT_EXIST) {
32
- if (currentVersion !== emmett.STREAM_DOES_NOT_EXIST) {
33
- throw new ExpectedVersionConflictError(
34
- streamName,
35
- expectedVersion,
36
- currentVersion
37
- );
38
- }
39
- return;
40
- }
41
- if (expectedVersion === emmett.STREAM_EXISTS) {
42
- if (currentVersion === emmett.STREAM_DOES_NOT_EXIST) {
43
- throw new ExpectedVersionConflictError(
44
- streamName,
45
- expectedVersion,
46
- currentVersion
47
- );
48
- }
49
- return;
50
- }
51
- const expectedBigInt = BigInt(expectedVersion);
52
- if (currentVersion === emmett.STREAM_DOES_NOT_EXIST || currentVersion !== expectedBigInt) {
53
- throw new ExpectedVersionConflictError(
54
- streamName,
55
- expectedVersion,
56
- currentVersion
57
- );
58
- }
59
- }
60
- function getCurrentStreamVersion(streamExists, version) {
61
- if (!streamExists) {
62
- return emmett.STREAM_DOES_NOT_EXIST;
63
- }
64
- return BigInt(version ?? -1);
65
- }
66
-
67
- // src/eventStore/firestoreEventStore.ts
68
- var DEFAULT_COLLECTIONS = {
69
- streams: "streams",
70
- counters: "_counters"
71
- };
72
- var FirestoreEventStoreImpl = class {
73
- constructor(firestore, options = {}) {
74
- this.firestore = firestore;
75
- this.collections = {
76
- ...DEFAULT_COLLECTIONS,
77
- ...options.collections
78
- };
79
- }
80
- collections;
81
- /**
82
- * Read events from a stream
83
- */
84
- async readStream(streamName, options = {}) {
85
- const { from, to, maxCount } = options;
86
- let query = this.firestore.collection(this.collections.streams).doc(streamName).collection("events").orderBy("streamVersion", "asc");
87
- if (from !== void 0) {
88
- query = query.where("streamVersion", ">=", Number(from));
89
- }
90
- if (to !== void 0) {
91
- query = query.where("streamVersion", "<=", Number(to));
92
- }
93
- if (maxCount !== void 0 && maxCount > 0) {
94
- query = query.limit(maxCount);
95
- }
96
- const snapshot = await query.get();
97
- return snapshot.docs.map((doc) => {
98
- const data = doc.data();
99
- return {
100
- type: data.type,
101
- data: data.data,
102
- metadata: {
103
- ...data.metadata,
104
- streamName,
105
- streamVersion: BigInt(data.streamVersion),
106
- streamPosition: BigInt(data.streamVersion),
107
- globalPosition: BigInt(data.globalPosition),
108
- timestamp: timestampToDate(data.timestamp)
109
- }
110
- };
111
- });
112
- }
113
- /**
114
- * Aggregate stream by applying events to state
115
- */
116
- async aggregateStream(streamName, options) {
117
- const { evolve, initialState, read } = options;
118
- const events = await this.readStream(streamName, read);
119
- const streamExists = events.length > 0;
120
- const state = events.reduce(evolve, initialState());
121
- const currentStreamVersion = streamExists ? events[events.length - 1].metadata.streamVersion : BigInt(0);
122
- return {
123
- state,
124
- currentStreamVersion,
125
- streamExists
126
- };
127
- }
128
- /**
129
- * Append events to a stream with optimistic concurrency control
130
- */
131
- async appendToStream(streamName, events, options = {}) {
132
- if (events.length === 0) {
133
- throw new Error("Cannot append empty event array");
134
- }
135
- const { expectedStreamVersion = emmett.NO_CONCURRENCY_CHECK } = options;
136
- return await this.firestore.runTransaction(async (transaction) => {
137
- return await this.appendToStreamInTransaction(
138
- transaction,
139
- streamName,
140
- events,
141
- expectedStreamVersion
142
- );
143
- });
144
- }
145
- /**
146
- * Internal method to append events within a transaction
147
- */
148
- async appendToStreamInTransaction(transaction, streamName, events, expectedStreamVersion) {
149
- const streamRef = this.firestore.collection(this.collections.streams).doc(streamName);
150
- const streamDoc = await transaction.get(streamRef);
151
- const streamExists = streamDoc.exists;
152
- const streamData = streamDoc.data();
153
- const currentVersion = getCurrentStreamVersion(
154
- streamExists,
155
- streamData?.version
156
- );
157
- assertExpectedVersionMatchesCurrent(
158
- streamName,
159
- expectedStreamVersion,
160
- currentVersion
161
- );
162
- const counterRef = this.firestore.collection(this.collections.counters).doc("global_position");
163
- const counterDoc = await transaction.get(counterRef);
164
- let globalPosition = counterDoc.exists ? counterDoc.data()?.value ?? 0 : 0;
165
- const baseVersion = currentVersion === emmett.STREAM_DOES_NOT_EXIST ? -1 : Number(currentVersion);
166
- const TimestampClass = this.firestore.constructor;
167
- const now = TimestampClass.Timestamp.now();
168
- events.forEach((event, index) => {
169
- const eventVersion = baseVersion + 1 + index;
170
- const eventRef = streamRef.collection("events").doc(padVersion(eventVersion));
171
- const metadata = event.metadata;
172
- const eventDocument = {
173
- type: event.type,
174
- data: event.data,
175
- ...metadata && { metadata },
176
- timestamp: now,
177
- globalPosition: globalPosition++,
178
- streamVersion: eventVersion
179
- };
180
- transaction.set(eventRef, eventDocument);
181
- });
182
- const newVersion = baseVersion + events.length;
183
- const updatedMetadata = {
184
- version: newVersion,
185
- createdAt: streamData?.createdAt ?? now,
186
- updatedAt: now
187
- };
188
- transaction.set(streamRef, updatedMetadata);
189
- transaction.set(counterRef, {
190
- value: globalPosition,
191
- updatedAt: now
192
- });
193
- return {
194
- nextExpectedStreamVersion: BigInt(newVersion),
195
- createdNewStream: !streamExists
196
- };
197
- }
198
- };
199
- function getFirestoreEventStore(firestore, options) {
200
- return new FirestoreEventStoreImpl(firestore, options);
201
- }
202
-
203
- // src/testing/eventStoreSpec.ts
204
- function getTestFirestore(config) {
205
- const projectId = config?.projectId || process.env.FIRESTORE_PROJECT_ID || "test-project";
206
- const host = config?.host || process.env.FIRESTORE_EMULATOR_HOST || "localhost:8080";
207
- const ssl = config?.ssl ?? false;
208
- return new firestore.Firestore({
209
- projectId,
210
- host,
211
- ssl,
212
- customHeaders: {
213
- Authorization: "Bearer owner"
214
- }
215
- });
216
- }
217
- function getTestEventStore(config) {
218
- const firestore = getTestFirestore(config);
219
- return getFirestoreEventStore(firestore);
220
- }
221
- async function clearFirestore(firestore, batchSize = 100) {
222
- const collections = await firestore.listCollections();
223
- for (const collection of collections) {
224
- await deleteCollection(firestore, collection.id, batchSize);
225
- }
226
- }
227
- async function deleteCollection(firestore, collectionPath, batchSize = 100) {
228
- const collectionRef = firestore.collection(collectionPath);
229
- const query = collectionRef.limit(batchSize);
230
- return new Promise((resolve, reject) => {
231
- void deleteQueryBatch(firestore, query, resolve, reject);
232
- });
233
- }
234
- async function deleteQueryBatch(firestore, query, resolve, reject) {
235
- try {
236
- const snapshot = await query.get();
237
- if (snapshot.size === 0) {
238
- resolve();
239
- return;
240
- }
241
- const batch = firestore.batch();
242
- snapshot.docs.forEach((doc) => {
243
- batch.delete(doc.ref);
244
- });
245
- await batch.commit();
246
- process.nextTick(() => {
247
- void deleteQueryBatch(firestore, query, resolve, reject);
248
- });
249
- } catch (error) {
250
- reject(error);
251
- }
252
- }
253
- async function waitForEmulator(host = "localhost:8080", timeout = 3e4, interval = 100) {
254
- const startTime = Date.now();
255
- const firestore$1 = new firestore.Firestore({
256
- projectId: "test",
257
- host,
258
- ssl: false
259
- });
260
- while (Date.now() - startTime < timeout) {
261
- try {
262
- await firestore$1.listCollections();
263
- await firestore$1.terminate();
264
- return;
265
- } catch {
266
- await new Promise((resolve) => setTimeout(resolve, interval));
267
- }
268
- }
269
- await firestore$1.terminate();
270
- throw new Error(`Firestore emulator not ready after ${timeout}ms`);
271
- }
272
- function setupFirestoreTests(config) {
273
- const firestore = getTestFirestore(config);
274
- const eventStore = getFirestoreEventStore(firestore);
275
- const cleanup = async () => {
276
- await firestore.terminate();
277
- };
278
- const clearData = async () => {
279
- await clearFirestore(firestore);
280
- };
281
- return {
282
- firestore,
283
- eventStore,
284
- cleanup,
285
- clearData
286
- };
287
- }
288
-
289
- exports.clearFirestore = clearFirestore;
290
- exports.deleteCollection = deleteCollection;
291
- exports.getTestEventStore = getTestEventStore;
292
- exports.getTestFirestore = getTestFirestore;
293
- exports.setupFirestoreTests = setupFirestoreTests;
294
- exports.waitForEmulator = waitForEmulator;
295
- //# sourceMappingURL=index.js.map
296
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/eventStore/types.ts","../../src/eventStore/utils.ts","../../src/eventStore/firestoreEventStore.ts","../../src/testing/eventStoreSpec.ts"],"names":["NO_CONCURRENCY_CHECK","STREAM_DOES_NOT_EXIST","STREAM_EXISTS","Firestore","firestore"],"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","/**\n * Testing utilities for Firestore Event Store\n *\n * These utilities help set up and manage the Firestore Emulator\n * for testing purposes.\n */\n\nimport { Firestore } from '@google-cloud/firestore';\nimport type { FirestoreEventStore } from '../eventStore/types';\nimport { getFirestoreEventStore } from '../eventStore/firestoreEventStore';\n\n/**\n * Configuration for Firestore test environment\n */\nexport interface FirestoreTestConfig {\n projectId?: string;\n host?: string;\n ssl?: boolean;\n}\n\n/**\n * Get a Firestore instance configured for the emulator\n *\n * @param config - Optional configuration override\n * @returns Configured Firestore instance\n *\n * @example\n * ```typescript\n * const firestore = getTestFirestore();\n * // Use firestore for testing\n * await firestore.terminate();\n * ```\n */\nexport function getTestFirestore(config?: FirestoreTestConfig): Firestore {\n const projectId = config?.projectId || process.env.FIRESTORE_PROJECT_ID || 'test-project';\n const host = config?.host || process.env.FIRESTORE_EMULATOR_HOST || 'localhost:8080';\n const ssl = config?.ssl ?? false;\n\n return new Firestore({\n projectId,\n host,\n ssl,\n customHeaders: {\n Authorization: 'Bearer owner',\n },\n });\n}\n\n/**\n * Get a test event store instance connected to the emulator\n *\n * @param config - Optional configuration override\n * @returns FirestoreEventStore instance for testing\n *\n * @example\n * ```typescript\n * const eventStore = getTestEventStore();\n * // Use eventStore for testing\n * ```\n */\nexport function getTestEventStore(config?: FirestoreTestConfig): FirestoreEventStore {\n const firestore = getTestFirestore(config);\n return getFirestoreEventStore(firestore);\n}\n\n/**\n * Clear all data from a Firestore instance\n * Useful for cleaning up between tests\n *\n * @param firestore - Firestore instance to clear\n * @param batchSize - Number of documents to delete per batch\n *\n * @example\n * ```typescript\n * beforeEach(async () => {\n * await clearFirestore(firestore);\n * });\n * ```\n */\nexport async function clearFirestore(\n firestore: Firestore,\n batchSize = 100,\n): Promise<void> {\n const collections = await firestore.listCollections();\n\n for (const collection of collections) {\n await deleteCollection(firestore, collection.id, batchSize);\n }\n}\n\n/**\n * Delete a specific collection from Firestore\n *\n * @param firestore - Firestore instance\n * @param collectionPath - Path to the collection to delete\n * @param batchSize - Number of documents to delete per batch\n */\nexport async function deleteCollection(\n firestore: Firestore,\n collectionPath: string,\n batchSize = 100,\n): Promise<void> {\n const collectionRef = firestore.collection(collectionPath);\n const query = collectionRef.limit(batchSize);\n\n return new Promise((resolve, reject) => {\n void deleteQueryBatch(firestore, query, resolve, reject);\n });\n}\n\nasync function deleteQueryBatch(\n firestore: Firestore,\n query: FirebaseFirestore.Query,\n resolve: () => void,\n reject: (error: Error) => void,\n): Promise<void> {\n try {\n const snapshot = await query.get();\n\n if (snapshot.size === 0) {\n resolve();\n return;\n }\n\n const batch = firestore.batch();\n snapshot.docs.forEach((doc) => {\n batch.delete(doc.ref);\n });\n await batch.commit();\n\n process.nextTick(() => {\n void deleteQueryBatch(firestore, query, resolve, reject);\n });\n } catch (error) {\n reject(error as Error);\n }\n}\n\n/**\n * Wait for the Firestore emulator to be ready\n *\n * @param host - Emulator host (default: localhost:8080)\n * @param timeout - Maximum time to wait in milliseconds (default: 30000)\n * @param interval - Check interval in milliseconds (default: 100)\n * @returns Promise that resolves when emulator is ready\n *\n * @example\n * ```typescript\n * await waitForEmulator();\n * // Emulator is ready\n * ```\n */\nexport async function waitForEmulator(\n host = 'localhost:8080',\n timeout = 30000,\n interval = 100,\n): Promise<void> {\n const startTime = Date.now();\n const firestore = new Firestore({\n projectId: 'test',\n host,\n ssl: false,\n });\n\n while (Date.now() - startTime < timeout) {\n try {\n await firestore.listCollections();\n await firestore.terminate();\n return;\n } catch {\n await new Promise((resolve) => setTimeout(resolve, interval));\n }\n }\n\n await firestore.terminate();\n throw new Error(`Firestore emulator not ready after ${timeout}ms`);\n}\n\n/**\n * Setup function for Jest/Vitest tests with Firestore emulator\n *\n * @returns Object with firestore instance and cleanup function\n *\n * @example\n * ```typescript\n * describe('My Tests', () => {\n * const { firestore, cleanup } = setupFirestoreTests();\n *\n * afterAll(cleanup);\n *\n * beforeEach(async () => {\n * await clearFirestore(firestore);\n * });\n *\n * it('should work', async () => {\n * const eventStore = getFirestoreEventStore(firestore);\n * // ... test code\n * });\n * });\n * ```\n */\nexport function setupFirestoreTests(config?: FirestoreTestConfig): {\n firestore: Firestore;\n eventStore: FirestoreEventStore;\n cleanup: () => Promise<void>;\n clearData: () => Promise<void>;\n} {\n const firestore = getTestFirestore(config);\n const eventStore = getFirestoreEventStore(firestore);\n\n const cleanup = async () => {\n await firestore.terminate();\n };\n\n const clearData = async () => {\n await clearFirestore(firestore);\n };\n\n return {\n firestore,\n eventStore,\n cleanup,\n clearData,\n };\n}\n"]}
@@ -1,289 +0,0 @@
1
- import { Firestore } from '@google-cloud/firestore';
2
- import { NO_CONCURRENCY_CHECK, STREAM_DOES_NOT_EXIST, STREAM_EXISTS } from '@event-driven-io/emmett';
3
-
4
- // src/testing/eventStoreSpec.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 timestampToDate(timestamp) {
23
- return timestamp.toDate();
24
- }
25
- function assertExpectedVersionMatchesCurrent(streamName, expectedVersion, currentVersion) {
26
- if (expectedVersion === NO_CONCURRENCY_CHECK) {
27
- return;
28
- }
29
- if (expectedVersion === STREAM_DOES_NOT_EXIST) {
30
- if (currentVersion !== STREAM_DOES_NOT_EXIST) {
31
- throw new ExpectedVersionConflictError(
32
- streamName,
33
- expectedVersion,
34
- currentVersion
35
- );
36
- }
37
- return;
38
- }
39
- if (expectedVersion === STREAM_EXISTS) {
40
- if (currentVersion === STREAM_DOES_NOT_EXIST) {
41
- throw new ExpectedVersionConflictError(
42
- streamName,
43
- expectedVersion,
44
- currentVersion
45
- );
46
- }
47
- return;
48
- }
49
- const expectedBigInt = BigInt(expectedVersion);
50
- if (currentVersion === STREAM_DOES_NOT_EXIST || currentVersion !== expectedBigInt) {
51
- throw new ExpectedVersionConflictError(
52
- streamName,
53
- expectedVersion,
54
- currentVersion
55
- );
56
- }
57
- }
58
- function getCurrentStreamVersion(streamExists, version) {
59
- if (!streamExists) {
60
- return STREAM_DOES_NOT_EXIST;
61
- }
62
- return BigInt(version ?? -1);
63
- }
64
-
65
- // src/eventStore/firestoreEventStore.ts
66
- var DEFAULT_COLLECTIONS = {
67
- streams: "streams",
68
- counters: "_counters"
69
- };
70
- var FirestoreEventStoreImpl = class {
71
- constructor(firestore, options = {}) {
72
- this.firestore = firestore;
73
- this.collections = {
74
- ...DEFAULT_COLLECTIONS,
75
- ...options.collections
76
- };
77
- }
78
- collections;
79
- /**
80
- * Read events from a stream
81
- */
82
- async readStream(streamName, options = {}) {
83
- const { from, to, maxCount } = options;
84
- let query = this.firestore.collection(this.collections.streams).doc(streamName).collection("events").orderBy("streamVersion", "asc");
85
- if (from !== void 0) {
86
- query = query.where("streamVersion", ">=", Number(from));
87
- }
88
- if (to !== void 0) {
89
- query = query.where("streamVersion", "<=", Number(to));
90
- }
91
- if (maxCount !== void 0 && maxCount > 0) {
92
- query = query.limit(maxCount);
93
- }
94
- const snapshot = await query.get();
95
- return snapshot.docs.map((doc) => {
96
- const data = doc.data();
97
- return {
98
- type: data.type,
99
- data: data.data,
100
- metadata: {
101
- ...data.metadata,
102
- streamName,
103
- streamVersion: BigInt(data.streamVersion),
104
- streamPosition: BigInt(data.streamVersion),
105
- globalPosition: BigInt(data.globalPosition),
106
- timestamp: timestampToDate(data.timestamp)
107
- }
108
- };
109
- });
110
- }
111
- /**
112
- * Aggregate stream by applying events to state
113
- */
114
- async aggregateStream(streamName, options) {
115
- const { evolve, initialState, read } = options;
116
- const events = await this.readStream(streamName, read);
117
- const streamExists = events.length > 0;
118
- const state = events.reduce(evolve, initialState());
119
- const currentStreamVersion = streamExists ? events[events.length - 1].metadata.streamVersion : BigInt(0);
120
- return {
121
- state,
122
- currentStreamVersion,
123
- streamExists
124
- };
125
- }
126
- /**
127
- * Append events to a stream with optimistic concurrency control
128
- */
129
- async appendToStream(streamName, events, options = {}) {
130
- if (events.length === 0) {
131
- throw new Error("Cannot append empty event array");
132
- }
133
- const { expectedStreamVersion = NO_CONCURRENCY_CHECK } = options;
134
- return await this.firestore.runTransaction(async (transaction) => {
135
- return await this.appendToStreamInTransaction(
136
- transaction,
137
- streamName,
138
- events,
139
- expectedStreamVersion
140
- );
141
- });
142
- }
143
- /**
144
- * Internal method to append events within a transaction
145
- */
146
- async appendToStreamInTransaction(transaction, streamName, events, expectedStreamVersion) {
147
- const streamRef = this.firestore.collection(this.collections.streams).doc(streamName);
148
- const streamDoc = await transaction.get(streamRef);
149
- const streamExists = streamDoc.exists;
150
- const streamData = streamDoc.data();
151
- const currentVersion = getCurrentStreamVersion(
152
- streamExists,
153
- streamData?.version
154
- );
155
- assertExpectedVersionMatchesCurrent(
156
- streamName,
157
- expectedStreamVersion,
158
- currentVersion
159
- );
160
- const counterRef = this.firestore.collection(this.collections.counters).doc("global_position");
161
- const counterDoc = await transaction.get(counterRef);
162
- let globalPosition = counterDoc.exists ? counterDoc.data()?.value ?? 0 : 0;
163
- const baseVersion = currentVersion === STREAM_DOES_NOT_EXIST ? -1 : Number(currentVersion);
164
- const TimestampClass = this.firestore.constructor;
165
- const now = TimestampClass.Timestamp.now();
166
- events.forEach((event, index) => {
167
- const eventVersion = baseVersion + 1 + index;
168
- const eventRef = streamRef.collection("events").doc(padVersion(eventVersion));
169
- const metadata = event.metadata;
170
- const eventDocument = {
171
- type: event.type,
172
- data: event.data,
173
- ...metadata && { metadata },
174
- timestamp: now,
175
- globalPosition: globalPosition++,
176
- streamVersion: eventVersion
177
- };
178
- transaction.set(eventRef, eventDocument);
179
- });
180
- const newVersion = baseVersion + events.length;
181
- const updatedMetadata = {
182
- version: newVersion,
183
- createdAt: streamData?.createdAt ?? now,
184
- updatedAt: now
185
- };
186
- transaction.set(streamRef, updatedMetadata);
187
- transaction.set(counterRef, {
188
- value: globalPosition,
189
- updatedAt: now
190
- });
191
- return {
192
- nextExpectedStreamVersion: BigInt(newVersion),
193
- createdNewStream: !streamExists
194
- };
195
- }
196
- };
197
- function getFirestoreEventStore(firestore, options) {
198
- return new FirestoreEventStoreImpl(firestore, options);
199
- }
200
-
201
- // src/testing/eventStoreSpec.ts
202
- function getTestFirestore(config) {
203
- const projectId = config?.projectId || process.env.FIRESTORE_PROJECT_ID || "test-project";
204
- const host = config?.host || process.env.FIRESTORE_EMULATOR_HOST || "localhost:8080";
205
- const ssl = config?.ssl ?? false;
206
- return new Firestore({
207
- projectId,
208
- host,
209
- ssl,
210
- customHeaders: {
211
- Authorization: "Bearer owner"
212
- }
213
- });
214
- }
215
- function getTestEventStore(config) {
216
- const firestore = getTestFirestore(config);
217
- return getFirestoreEventStore(firestore);
218
- }
219
- async function clearFirestore(firestore, batchSize = 100) {
220
- const collections = await firestore.listCollections();
221
- for (const collection of collections) {
222
- await deleteCollection(firestore, collection.id, batchSize);
223
- }
224
- }
225
- async function deleteCollection(firestore, collectionPath, batchSize = 100) {
226
- const collectionRef = firestore.collection(collectionPath);
227
- const query = collectionRef.limit(batchSize);
228
- return new Promise((resolve, reject) => {
229
- void deleteQueryBatch(firestore, query, resolve, reject);
230
- });
231
- }
232
- async function deleteQueryBatch(firestore, query, resolve, reject) {
233
- try {
234
- const snapshot = await query.get();
235
- if (snapshot.size === 0) {
236
- resolve();
237
- return;
238
- }
239
- const batch = firestore.batch();
240
- snapshot.docs.forEach((doc) => {
241
- batch.delete(doc.ref);
242
- });
243
- await batch.commit();
244
- process.nextTick(() => {
245
- void deleteQueryBatch(firestore, query, resolve, reject);
246
- });
247
- } catch (error) {
248
- reject(error);
249
- }
250
- }
251
- async function waitForEmulator(host = "localhost:8080", timeout = 3e4, interval = 100) {
252
- const startTime = Date.now();
253
- const firestore = new Firestore({
254
- projectId: "test",
255
- host,
256
- ssl: false
257
- });
258
- while (Date.now() - startTime < timeout) {
259
- try {
260
- await firestore.listCollections();
261
- await firestore.terminate();
262
- return;
263
- } catch {
264
- await new Promise((resolve) => setTimeout(resolve, interval));
265
- }
266
- }
267
- await firestore.terminate();
268
- throw new Error(`Firestore emulator not ready after ${timeout}ms`);
269
- }
270
- function setupFirestoreTests(config) {
271
- const firestore = getTestFirestore(config);
272
- const eventStore = getFirestoreEventStore(firestore);
273
- const cleanup = async () => {
274
- await firestore.terminate();
275
- };
276
- const clearData = async () => {
277
- await clearFirestore(firestore);
278
- };
279
- return {
280
- firestore,
281
- eventStore,
282
- cleanup,
283
- clearData
284
- };
285
- }
286
-
287
- export { clearFirestore, deleteCollection, getTestEventStore, getTestFirestore, setupFirestoreTests, waitForEmulator };
288
- //# sourceMappingURL=index.mjs.map
289
- //# sourceMappingURL=index.mjs.map