@causa/runtime-google 0.39.1 → 1.0.0-rc.1

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.
Files changed (73) hide show
  1. package/README.md +7 -14
  2. package/dist/app-check/testing.d.ts +7 -3
  3. package/dist/app-check/testing.js +9 -6
  4. package/dist/firebase/module.d.ts +14 -2
  5. package/dist/firebase/module.js +45 -16
  6. package/dist/firebase/testing.d.ts +7 -4
  7. package/dist/firebase/testing.js +12 -7
  8. package/dist/firestore/testing.d.ts +32 -17
  9. package/dist/firestore/testing.js +45 -29
  10. package/dist/identity-platform/testing.d.ts +21 -6
  11. package/dist/identity-platform/testing.js +34 -9
  12. package/dist/pubsub/publisher.module.d.ts +1 -1
  13. package/dist/pubsub/{testing/fixture.d.ts → testing.d.ts} +64 -48
  14. package/dist/pubsub/{testing/fixture.js → testing.js} +106 -73
  15. package/dist/spanner/column.decorator.d.ts +1 -11
  16. package/dist/spanner/column.decorator.js +2 -7
  17. package/dist/spanner/conversion.d.ts +5 -18
  18. package/dist/spanner/conversion.js +45 -125
  19. package/dist/spanner/entity-manager.d.ts +19 -23
  20. package/dist/spanner/entity-manager.js +26 -60
  21. package/dist/spanner/table-cache.js +0 -3
  22. package/dist/spanner/testing.d.ts +37 -18
  23. package/dist/spanner/testing.js +75 -10
  24. package/dist/spanner/types.d.ts +0 -6
  25. package/dist/testing.d.ts +36 -2
  26. package/dist/testing.js +33 -2
  27. package/dist/transaction/firestore-pubsub/index.d.ts +3 -2
  28. package/dist/transaction/firestore-pubsub/index.js +2 -1
  29. package/dist/transaction/firestore-pubsub/nestjs-collection-resolver.d.ts +1 -1
  30. package/dist/transaction/firestore-pubsub/nestjs-collection-resolver.js +0 -1
  31. package/dist/transaction/firestore-pubsub/readonly-state-transaction.d.ts +37 -0
  32. package/dist/transaction/firestore-pubsub/readonly-state-transaction.js +46 -0
  33. package/dist/transaction/firestore-pubsub/runner.d.ts +6 -4
  34. package/dist/transaction/firestore-pubsub/runner.js +16 -7
  35. package/dist/transaction/firestore-pubsub/state-transaction.d.ts +11 -49
  36. package/dist/transaction/firestore-pubsub/state-transaction.js +19 -42
  37. package/dist/transaction/firestore-pubsub/transaction.d.ts +15 -3
  38. package/dist/transaction/firestore-pubsub/transaction.js +21 -3
  39. package/dist/transaction/firestore-pubsub/types.d.ts +36 -0
  40. package/dist/transaction/firestore-pubsub/types.js +1 -0
  41. package/dist/transaction/index.d.ts +0 -3
  42. package/dist/transaction/index.js +0 -3
  43. package/dist/transaction/spanner-outbox/index.d.ts +3 -1
  44. package/dist/transaction/spanner-outbox/index.js +3 -0
  45. package/dist/transaction/spanner-outbox/readonly-transaction.d.ts +22 -0
  46. package/dist/transaction/spanner-outbox/readonly-transaction.js +25 -0
  47. package/dist/transaction/spanner-outbox/runner.d.ts +7 -18
  48. package/dist/transaction/spanner-outbox/runner.js +13 -6
  49. package/dist/transaction/{spanner-utils.js → spanner-outbox/spanner-utils.js} +1 -1
  50. package/dist/transaction/spanner-outbox/state-transaction.d.ts +20 -0
  51. package/dist/transaction/spanner-outbox/state-transaction.js +34 -0
  52. package/dist/transaction/spanner-outbox/transaction.d.ts +28 -0
  53. package/dist/transaction/spanner-outbox/transaction.js +38 -0
  54. package/package.json +37 -34
  55. package/dist/pubsub/testing/index.d.ts +0 -4
  56. package/dist/pubsub/testing/index.js +0 -2
  57. package/dist/pubsub/testing/requester.d.ts +0 -50
  58. package/dist/pubsub/testing/requester.js +0 -53
  59. package/dist/testing/google-app-fixture.d.ts +0 -191
  60. package/dist/testing/google-app-fixture.js +0 -200
  61. package/dist/testing/index.d.ts +0 -1
  62. package/dist/testing/index.js +0 -1
  63. package/dist/transaction/spanner-pubsub/index.d.ts +0 -2
  64. package/dist/transaction/spanner-pubsub/index.js +0 -2
  65. package/dist/transaction/spanner-pubsub/module.d.ts +0 -14
  66. package/dist/transaction/spanner-pubsub/module.js +0 -21
  67. package/dist/transaction/spanner-pubsub/runner.d.ts +0 -23
  68. package/dist/transaction/spanner-pubsub/runner.js +0 -69
  69. package/dist/transaction/spanner-state-transaction.d.ts +0 -20
  70. package/dist/transaction/spanner-state-transaction.js +0 -35
  71. package/dist/transaction/spanner-transaction.d.ts +0 -16
  72. package/dist/transaction/spanner-transaction.js +0 -20
  73. /package/dist/transaction/{spanner-utils.d.ts → spanner-outbox/spanner-utils.d.ts} +0 -0
@@ -1,5 +1,21 @@
1
1
  import { Database, Instance, Spanner } from '@google-cloud/spanner';
2
2
  import * as uuid from 'uuid';
3
+ import { SpannerEntityManager } from './entity-manager.js';
4
+ /**
5
+ * Sets default values for the database creation parameters.
6
+ *
7
+ * @param options The options for which defaults should be set where needed.
8
+ * @returns The {@link CreateDatabaseParameters}.
9
+ */
10
+ function makeDatabaseParameters(options) {
11
+ const name = options.name ?? `test-${uuid.v4().slice(-10)}`;
12
+ const spanner = options.instance?.parent ?? options.spanner ?? new Spanner();
13
+ const instance = options.instance ?? spanner.instance(process.env.SPANNER_INSTANCE ?? '');
14
+ const sourceDatabaseName = options.sourceDatabaseName === null
15
+ ? null
16
+ : (options.sourceDatabaseName ?? process.env.SPANNER_DATABASE ?? null);
17
+ return { name, spanner, instance, sourceDatabaseName };
18
+ }
3
19
  /**
4
20
  * Creates a new database.
5
21
  * This will destroy the existing database if it exists.
@@ -8,10 +24,7 @@ import * as uuid from 'uuid';
8
24
  * @returns The database object.
9
25
  */
10
26
  export async function createDatabase(options = {}) {
11
- const name = options.name ?? `test-${uuid.v4().slice(-10)}`;
12
- const spanner = options.spanner ?? new Spanner();
13
- const instance = options.instance ?? spanner.instance(process.env.SPANNER_INSTANCE ?? '');
14
- const sourceDatabaseName = options.sourceDatabaseName ?? process.env.SPANNER_DATABASE;
27
+ const { name, sourceDatabaseName, instance } = makeDatabaseParameters(options);
15
28
  const [databases] = await instance.getDatabases();
16
29
  const existingDatabase = databases.find((d) => d.formattedName_.split('/').pop() === name);
17
30
  await existingDatabase?.delete();
@@ -27,11 +40,63 @@ export async function createDatabase(options = {}) {
27
40
  return database;
28
41
  }
29
42
  /**
30
- * Returns a {@link NestJsModuleOverrider} that overrides the {@link Database} provider with the provided database.
31
- *
32
- * @param database The temporary database to use.
33
- * @returns The {@link NestJsModuleOverrider} to override the {@link Database} provider.
43
+ * A {@link Fixture} that creates a temporary Spanner database and injects it into the NestJS application.
44
+ * The specified tables will be cleared after each test.
34
45
  */
35
- export function overrideDatabase(database) {
36
- return (builder) => builder.overrideProvider(Database).useValue(database);
46
+ export class SpannerFixture {
47
+ /**
48
+ * The name of the temporary database.
49
+ */
50
+ name;
51
+ /**
52
+ * If `sourceDatabaseName` is provided, its DDL will be copied into the new database, otherwise it will try to copy
53
+ * the DDL from `process.env.SPANNER_DATABASE`.
54
+ * If `null`, no schema will be set on the created database.
55
+ */
56
+ sourceDatabaseName;
57
+ /**
58
+ * The Spanner client to use for tests.
59
+ */
60
+ spanner;
61
+ /**
62
+ * The Spanner instance to use for tests.
63
+ * By default, a new Spanner client will be created using the `SPANNER_INSTANCE` environment variable.
64
+ */
65
+ instance;
66
+ /**
67
+ * Types of entities (Spanner tables) to clear.
68
+ */
69
+ types;
70
+ /**
71
+ * The {@link SpannerEntityManager} used to clear tables.
72
+ */
73
+ entityManager;
74
+ /**
75
+ * The temporary test database created by this fixture.
76
+ */
77
+ database;
78
+ constructor(options = {}) {
79
+ const { name, sourceDatabaseName, spanner, instance } = makeDatabaseParameters(options);
80
+ this.name = name;
81
+ this.sourceDatabaseName = sourceDatabaseName;
82
+ this.spanner = spanner;
83
+ this.instance = instance;
84
+ this.types = options.types ?? [];
85
+ }
86
+ async init() {
87
+ this.database = await createDatabase(this);
88
+ this.entityManager = new SpannerEntityManager(this.database);
89
+ return (builder) => builder.overrideProvider(Database).useValue(this.database);
90
+ }
91
+ async clear() {
92
+ await this.entityManager.transaction(async (transaction) => {
93
+ for (const entity of this.types) {
94
+ await this.entityManager.clear(entity, { transaction });
95
+ }
96
+ });
97
+ }
98
+ async delete() {
99
+ await this.database.delete();
100
+ this.spanner.close();
101
+ }
37
102
  }
@@ -1,10 +1,4 @@
1
1
  import type { SpannerReadOnlyTransaction, SpannerReadWriteTransaction } from './entity-manager.js';
2
- /**
3
- * A partial Spanner entity instance, where nested objects can also be partial.
4
- */
5
- export type RecursivePartialEntity<T> = T extends Date ? T : T extends object ? Partial<T> | {
6
- [P in keyof T]?: RecursivePartialEntity<T[P]>;
7
- } : T;
8
2
  /**
9
3
  * Option for a function that accepts a Spanner read-only transaction.
10
4
  */
package/dist/testing.d.ts CHANGED
@@ -1,7 +1,41 @@
1
+ import { type Fixture } from '@causa/runtime/nestjs/testing';
2
+ import type { Type } from '@nestjs/common';
3
+ import { FirestorePubSubTransactionRunner, SpannerOutboxTransactionRunner } from './transaction/index.js';
1
4
  export * from './app-check/testing.js';
2
5
  export * from './firebase/testing.js';
3
6
  export * from './firestore/testing.js';
4
7
  export * from './identity-platform/testing.js';
5
- export * from './pubsub/testing/index.js';
8
+ export * from './pubsub/testing.js';
6
9
  export * from './spanner/testing.js';
7
- export * from './testing/index.js';
10
+ /**
11
+ * Creates a NestJS application using the specified module, and sets up the fixture.
12
+ *
13
+ * @param appModule The NestJS module to create the application from.
14
+ * @param options Options when creating the GCP resources for the fixture.
15
+ * @returns The {@link GoogleAppFixture}.
16
+ */
17
+ export declare function createGoogleFixtures(options?: {
18
+ /**
19
+ * Temporary Pub/Sub topics to create using the {@link PubSubFixture}.
20
+ */
21
+ pubSubTopics?: Record<string, Type>;
22
+ /**
23
+ * Temporary Firestore collections to create and to clear during teardown.
24
+ */
25
+ firestoreTypes?: Type[];
26
+ /**
27
+ * Spanner entities to clear during teardown.
28
+ */
29
+ spannerTypes?: Type[];
30
+ /**
31
+ * Whether the `AppCheckGuard` should be disabled.
32
+ * Defaults to `true`.
33
+ */
34
+ disableAppCheck?: boolean;
35
+ /**
36
+ * The transaction runner to use with the created {@link VersionedEntityFixture}.
37
+ * Defaults to {@link SpannerOutboxTransactionRunner}.
38
+ * If `null`, no versioned entity fixture is created.
39
+ */
40
+ versionedEntityRunner?: Type<SpannerOutboxTransactionRunner> | Type<FirestorePubSubTransactionRunner> | null;
41
+ }): Fixture[];
package/dist/testing.js CHANGED
@@ -1,7 +1,38 @@
1
+ import {} from '@causa/runtime/nestjs/testing';
2
+ import { VersionedEntityFixture } from '@causa/runtime/testing';
3
+ import { FirestoreFixture } from './firestore/testing.js';
4
+ import { AuthUsersFixture } from './identity-platform/testing.js';
5
+ import { PubSubFixture } from './pubsub/testing.js';
6
+ import { SpannerFixture } from './spanner/testing.js';
7
+ import { AppCheckFixture, FirebaseFixture } from './testing.js';
8
+ import { FirestorePubSubTransactionRunner, SpannerOutboxTransactionRunner, } from './transaction/index.js';
1
9
  export * from './app-check/testing.js';
2
10
  export * from './firebase/testing.js';
3
11
  export * from './firestore/testing.js';
4
12
  export * from './identity-platform/testing.js';
5
- export * from './pubsub/testing/index.js';
13
+ export * from './pubsub/testing.js';
6
14
  export * from './spanner/testing.js';
7
- export * from './testing/index.js';
15
+ /**
16
+ * Creates a NestJS application using the specified module, and sets up the fixture.
17
+ *
18
+ * @param appModule The NestJS module to create the application from.
19
+ * @param options Options when creating the GCP resources for the fixture.
20
+ * @returns The {@link GoogleAppFixture}.
21
+ */
22
+ export function createGoogleFixtures(options = {}) {
23
+ const disableAppCheck = options.disableAppCheck ?? true;
24
+ const versionedEntityFixture = options.versionedEntityRunner !== null
25
+ ? [
26
+ new VersionedEntityFixture(options.versionedEntityRunner ?? SpannerOutboxTransactionRunner, PubSubFixture),
27
+ ]
28
+ : [];
29
+ return [
30
+ new FirebaseFixture(),
31
+ new AuthUsersFixture(),
32
+ new FirestoreFixture(options.firestoreTypes ?? []),
33
+ new SpannerFixture({ types: options.spannerTypes }),
34
+ new PubSubFixture(options.pubSubTopics ?? {}),
35
+ ...(disableAppCheck ? [new AppCheckFixture()] : []),
36
+ ...versionedEntityFixture,
37
+ ];
38
+ }
@@ -1,6 +1,7 @@
1
1
  export { FirestorePubSubTransactionModule } from './module.js';
2
+ export * from './readonly-state-transaction.js';
2
3
  export { FirestorePubSubTransactionRunner } from './runner.js';
3
4
  export { SoftDeletedFirestoreCollection } from './soft-deleted-collection.decorator.js';
4
5
  export { FirestoreStateTransaction } from './state-transaction.js';
5
- export type { FirestoreCollectionResolver, FirestoreCollectionsForDocumentType, } from './state-transaction.js';
6
- export { FirestorePubSubTransaction } from './transaction.js';
6
+ export * from './transaction.js';
7
+ export type { FirestoreCollectionResolver, FirestoreCollectionsForDocumentType, } from './types.js';
@@ -1,5 +1,6 @@
1
1
  export { FirestorePubSubTransactionModule } from './module.js';
2
+ export * from './readonly-state-transaction.js';
2
3
  export { FirestorePubSubTransactionRunner } from './runner.js';
3
4
  export { SoftDeletedFirestoreCollection } from './soft-deleted-collection.decorator.js';
4
5
  export { FirestoreStateTransaction } from './state-transaction.js';
5
- export { FirestorePubSubTransaction } from './transaction.js';
6
+ export * from './transaction.js';
@@ -1,6 +1,6 @@
1
1
  import { type Type } from '@nestjs/common';
2
2
  import { ModuleRef } from '@nestjs/core';
3
- import { type FirestoreCollectionResolver, type FirestoreCollectionsForDocumentType } from './state-transaction.js';
3
+ import type { FirestoreCollectionResolver, FirestoreCollectionsForDocumentType } from './types.js';
4
4
  /**
5
5
  * A {@link FirestoreCollectionResolver} that uses NestJS dependency injection to resolve Firestore collections.
6
6
  */
@@ -13,7 +13,6 @@ import { CollectionReference } from 'firebase-admin/firestore';
13
13
  import { makeFirestoreDataConverter } from '../../firestore/index.js';
14
14
  import { getFirestoreCollectionInjectionName } from '../../firestore/inject-collection.decorator.js';
15
15
  import { getSoftDeletedFirestoreCollectionMetadataForType } from './soft-deleted-collection.decorator.js';
16
- import {} from './state-transaction.js';
17
16
  /**
18
17
  * A {@link FirestoreCollectionResolver} that uses NestJS dependency injection to resolve Firestore collections.
19
18
  */
@@ -0,0 +1,37 @@
1
+ import type { ReadOnlyStateTransaction, ReadOnlyTransactionOption } from '@causa/runtime';
2
+ import type { Type } from '@nestjs/common';
3
+ import { Transaction } from 'firebase-admin/firestore';
4
+ import type { FirestoreCollectionResolver } from './types.js';
5
+ /**
6
+ * Option for a function that accepts a {@link FirestoreReadOnlyStateTransaction}.
7
+ */
8
+ export type FirestoreReadOnlyStateTransactionOption = ReadOnlyTransactionOption<FirestoreReadOnlyStateTransaction>;
9
+ /**
10
+ * A {@link ReadOnlyStateTransaction} that uses Firestore for state storage.
11
+ *
12
+ * This transaction handles soft-deleted documents if the class is decorated with `SoftDeletedFirestoreCollection`,
13
+ * which means that documents with a `deletedAt` field set to a non-null value are considered deleted.
14
+ *
15
+ * {@link FirestoreStateTransaction.get} will return the document from either the regular or soft-delete collection, as
16
+ * expected by {@link ReadOnlyStateTransaction.get}.
17
+ */
18
+ export declare class FirestoreReadOnlyStateTransaction implements ReadOnlyStateTransaction {
19
+ /**
20
+ * The Firestore transaction to use.
21
+ */
22
+ readonly firestoreTransaction: Transaction;
23
+ /**
24
+ * The resolver that provides the Firestore collections for a given document type.
25
+ */
26
+ readonly collectionResolver: FirestoreCollectionResolver;
27
+ constructor(
28
+ /**
29
+ * The Firestore transaction to use.
30
+ */
31
+ firestoreTransaction: Transaction,
32
+ /**
33
+ * The resolver that provides the Firestore collections for a given document type.
34
+ */
35
+ collectionResolver: FirestoreCollectionResolver);
36
+ get<T extends object>(type: Type<T>, entity: Partial<T>): Promise<T | undefined>;
37
+ }
@@ -0,0 +1,46 @@
1
+ import { Transaction } from 'firebase-admin/firestore';
2
+ import { getReferenceForFirestoreDocument } from '../../firestore/index.js';
3
+ /**
4
+ * A {@link ReadOnlyStateTransaction} that uses Firestore for state storage.
5
+ *
6
+ * This transaction handles soft-deleted documents if the class is decorated with `SoftDeletedFirestoreCollection`,
7
+ * which means that documents with a `deletedAt` field set to a non-null value are considered deleted.
8
+ *
9
+ * {@link FirestoreStateTransaction.get} will return the document from either the regular or soft-delete collection, as
10
+ * expected by {@link ReadOnlyStateTransaction.get}.
11
+ */
12
+ export class FirestoreReadOnlyStateTransaction {
13
+ firestoreTransaction;
14
+ collectionResolver;
15
+ constructor(
16
+ /**
17
+ * The Firestore transaction to use.
18
+ */
19
+ firestoreTransaction,
20
+ /**
21
+ * The resolver that provides the Firestore collections for a given document type.
22
+ */
23
+ collectionResolver) {
24
+ this.firestoreTransaction = firestoreTransaction;
25
+ this.collectionResolver = collectionResolver;
26
+ }
27
+ async get(type, entity) {
28
+ const { activeCollection, softDelete } = this.collectionResolver.getCollectionsForType(type);
29
+ const activeDocRef = getReferenceForFirestoreDocument(activeCollection, entity, type);
30
+ const activeSnapshot = await this.firestoreTransaction.get(activeDocRef);
31
+ if (activeSnapshot.exists) {
32
+ return activeSnapshot.data();
33
+ }
34
+ if (!softDelete) {
35
+ return undefined;
36
+ }
37
+ const deletedDocRef = getReferenceForFirestoreDocument(softDelete.collection, entity, type);
38
+ const deletedSnapshot = await this.firestoreTransaction.get(deletedDocRef);
39
+ if (!deletedSnapshot.exists) {
40
+ return undefined;
41
+ }
42
+ const deletedDocument = deletedSnapshot.data();
43
+ delete deletedDocument[softDelete.expirationField];
44
+ return deletedDocument;
45
+ }
46
+ }
@@ -1,9 +1,10 @@
1
- import { TransactionRunner } from '@causa/runtime';
1
+ import { TransactionRunner, type ReadWriteTransactionOptions, type TransactionFn } from '@causa/runtime';
2
2
  import { Logger } from '@causa/runtime/nestjs';
3
3
  import { Firestore } from '@google-cloud/firestore';
4
4
  import { PubSubPublisher } from '../../pubsub/index.js';
5
- import { type FirestoreCollectionResolver } from './state-transaction.js';
5
+ import { FirestoreReadOnlyStateTransaction } from './readonly-state-transaction.js';
6
6
  import { FirestorePubSubTransaction } from './transaction.js';
7
+ import type { FirestoreCollectionResolver } from './types.js';
7
8
  /**
8
9
  * A {@link TransactionRunner} that uses Firestore for state and Pub/Sub for events.
9
10
  * A Firestore transaction is used as the main transaction. If it succeeds, events are published to Pub/Sub outside of
@@ -11,11 +12,12 @@ import { FirestorePubSubTransaction } from './transaction.js';
11
12
  * This runner and the transaction use the {@link FirestoreStateTransaction}, which handles soft-deleted documents. All
12
13
  * entities that are written to the state should be decorated with the `SoftDeletedFirestoreCollection` decorator.
13
14
  */
14
- export declare class FirestorePubSubTransactionRunner extends TransactionRunner<FirestorePubSubTransaction> {
15
+ export declare class FirestorePubSubTransactionRunner extends TransactionRunner<FirestorePubSubTransaction, FirestoreReadOnlyStateTransaction> {
15
16
  readonly firestore: Firestore;
16
17
  readonly pubSubPublisher: PubSubPublisher;
17
18
  readonly collectionResolver: FirestoreCollectionResolver;
18
19
  private readonly logger;
19
20
  constructor(firestore: Firestore, pubSubPublisher: PubSubPublisher, collectionResolver: FirestoreCollectionResolver, logger: Logger);
20
- run<T>(runFn: (transaction: FirestorePubSubTransaction) => Promise<T>): Promise<[T]>;
21
+ protected runReadWrite<RT>(options: ReadWriteTransactionOptions, runFn: TransactionFn<FirestorePubSubTransaction, RT>): Promise<RT>;
22
+ protected runReadOnly<RT>(runFn: TransactionFn<FirestoreReadOnlyStateTransaction, RT>): Promise<RT>;
21
23
  }
@@ -8,13 +8,14 @@ var __metadata = (this && this.__metadata) || function (k, v) {
8
8
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
9
  };
10
10
  var FirestorePubSubTransactionRunner_1;
11
- import { BufferEventTransaction, TransactionRunner } from '@causa/runtime';
11
+ import { OutboxEventTransaction, TransactionRunner, } from '@causa/runtime';
12
12
  import { Logger } from '@causa/runtime/nestjs';
13
13
  import { Firestore } from '@google-cloud/firestore';
14
14
  import { Injectable } from '@nestjs/common';
15
15
  import { wrapFirestoreOperation } from '../../firestore/index.js';
16
16
  import { PubSubPublisher } from '../../pubsub/index.js';
17
- import { FirestoreStateTransaction, } from './state-transaction.js';
17
+ import { FirestoreReadOnlyStateTransaction } from './readonly-state-transaction.js';
18
+ import { FirestoreStateTransaction } from './state-transaction.js';
18
19
  import { FirestorePubSubTransaction } from './transaction.js';
19
20
  /**
20
21
  * A {@link TransactionRunner} that uses Firestore for state and Pub/Sub for events.
@@ -36,19 +37,27 @@ let FirestorePubSubTransactionRunner = FirestorePubSubTransactionRunner_1 = clas
36
37
  this.logger = logger;
37
38
  this.logger.setContext(FirestorePubSubTransactionRunner_1.name);
38
39
  }
39
- async run(runFn) {
40
+ async runReadWrite(options, runFn) {
40
41
  this.logger.info('Creating a Firestore Pub/Sub transaction.');
41
42
  const { result, eventTransaction } = await wrapFirestoreOperation(() => this.firestore.runTransaction(async (firestoreTransaction) => {
42
43
  const stateTransaction = new FirestoreStateTransaction(firestoreTransaction, this.collectionResolver);
43
- const eventTransaction = new BufferEventTransaction(this.pubSubPublisher);
44
+ const eventTransaction = new OutboxEventTransaction(this.pubSubPublisher, options.publishOptions);
44
45
  const transaction = new FirestorePubSubTransaction(stateTransaction, eventTransaction);
45
46
  const result = await runFn(transaction);
46
47
  this.logger.info('Committing the Firestore transaction.');
47
48
  return { result, eventTransaction };
48
49
  }));
49
- this.logger.info('Publishing Pub/Sub events.');
50
- await eventTransaction.commit();
51
- return [result];
50
+ if (eventTransaction.events.length > 0) {
51
+ this.logger.info('Publishing Pub/Sub events.');
52
+ await Promise.all(eventTransaction.events.map((e) => this.pubSubPublisher.publish(e)));
53
+ }
54
+ return result;
55
+ }
56
+ async runReadOnly(runFn) {
57
+ return await wrapFirestoreOperation(() => this.firestore.runTransaction(async (firestoreTransaction) => {
58
+ const transaction = new FirestoreReadOnlyStateTransaction(firestoreTransaction, this.collectionResolver);
59
+ return await runFn(transaction);
60
+ }, { readOnly: true }));
52
61
  }
53
62
  };
54
63
  FirestorePubSubTransactionRunner = FirestorePubSubTransactionRunner_1 = __decorate([
@@ -1,62 +1,24 @@
1
- import type { FindReplaceStateTransaction } from '@causa/runtime';
1
+ import type { StateTransaction } from '@causa/runtime';
2
2
  import type { Type } from '@nestjs/common';
3
- import { CollectionReference, Transaction } from 'firebase-admin/firestore';
4
- import type { SoftDeletedFirestoreCollectionMetadata } from './soft-deleted-collection.decorator.js';
3
+ import { FirestoreReadOnlyStateTransaction } from './readonly-state-transaction.js';
5
4
  /**
6
- * The Firestore collections that should be used to create document references for a given document type.
7
- */
8
- export type FirestoreCollectionsForDocumentType<T> = {
9
- /**
10
- * The regular collection, where documents are stored when they are not deleted.
11
- */
12
- readonly activeCollection: CollectionReference<T>;
13
- /**
14
- * Configuration about the soft-delete collection, where documents are stored when their `deletedAt` field is not
15
- * `null`. This can be `null` if the document type does not declare a soft-delete collection.
16
- */
17
- readonly softDelete: ({
18
- /**
19
- * The collection where soft-deleted documents are stored.
20
- */
21
- collection: CollectionReference<T>;
22
- } & Pick<SoftDeletedFirestoreCollectionMetadata, 'expirationDelay' | 'expirationField'>) | null;
23
- };
24
- /**
25
- * A resolver that returns the Firestore collections for a given document type.
26
- * This allows using the {@link FirestoreStateTransaction} in various contexts, such as in a NestJS module. It also
27
- * enables testing with temporary collections.
28
- */
29
- export interface FirestoreCollectionResolver {
30
- /**
31
- * Returns the Firestore collections for a given document type.
32
- *
33
- * @param documentType The type of document.
34
- * @returns The Firestore collections for the given document type.
35
- */
36
- getCollectionsForType<T>(documentType: Type<T>): FirestoreCollectionsForDocumentType<T>;
37
- }
38
- /**
39
- * A {@link FindReplaceStateTransaction} that uses Firestore for state storage.
5
+ * A {@link StateTransaction} that uses Firestore for state storage.
40
6
  *
41
7
  * This transaction handles soft-deleted documents if the class is decorated with `SoftDeletedFirestoreCollection`,
42
8
  * which means that documents with a `deletedAt` field set to a non-null value are considered deleted.
43
9
  * Soft-deleted documents are moved to a separate collection, where they are kept for a configurable amount of time
44
10
  * before being permanently deleted. See the `SoftDeletedFirestoreCollection` decorator for more information.
45
11
  *
46
- * {@link FirestoreStateTransaction.deleteWithSameKeyAs} will delete the document from any of the regular or soft-delete
47
- * collections. It does not throw an error if the document does not exist.
12
+ * {@link FirestoreStateTransaction.delete} will delete the document from any of the regular or soft-delete collections.
13
+ * It does not throw an error if the document does not exist.
48
14
  *
49
- * {@link FirestoreStateTransaction.findOneWithSameKeyAs} will return the document from either the regular or
50
- * soft-delete collection, as expected by {@link FindReplaceStateTransaction.findOneWithSameKeyAs}.
15
+ * {@link FirestoreStateTransaction.get} will return the document from either the regular or soft-delete collection, as
16
+ * expected by {@link StateTransaction.get}.
51
17
  *
52
- * {@link FirestoreStateTransaction.replace} will set the document in the relevant collection, either the regular or
18
+ * {@link FirestoreStateTransaction.set} will set the document in the relevant collection, either the regular or
53
19
  * soft-delete collection depending on the value of the `deletedAt` field.
54
20
  */
55
- export declare class FirestoreStateTransaction implements FindReplaceStateTransaction {
56
- readonly transaction: Transaction;
57
- readonly collectionResolver: FirestoreCollectionResolver;
58
- constructor(transaction: Transaction, collectionResolver: FirestoreCollectionResolver);
59
- deleteWithSameKeyAs<T extends object>(type: Type<T>, key: Partial<T>): Promise<void>;
60
- findOneWithSameKeyAs<T extends object>(type: Type<T>, entity: Partial<T>): Promise<T | undefined>;
61
- replace<T extends object>(entity: T): Promise<void>;
21
+ export declare class FirestoreStateTransaction extends FirestoreReadOnlyStateTransaction implements StateTransaction {
22
+ delete<T extends object>(typeOrEntity: Type<T> | T, key?: Partial<T>): Promise<void>;
23
+ set<T extends object>(entity: T): Promise<void>;
62
24
  }
@@ -1,65 +1,42 @@
1
1
  import { plainToInstance } from 'class-transformer';
2
- import { CollectionReference, Transaction } from 'firebase-admin/firestore';
3
2
  import { getReferenceForFirestoreDocument } from '../../firestore/index.js';
3
+ import { FirestoreReadOnlyStateTransaction } from './readonly-state-transaction.js';
4
4
  /**
5
- * A {@link FindReplaceStateTransaction} that uses Firestore for state storage.
5
+ * A {@link StateTransaction} that uses Firestore for state storage.
6
6
  *
7
7
  * This transaction handles soft-deleted documents if the class is decorated with `SoftDeletedFirestoreCollection`,
8
8
  * which means that documents with a `deletedAt` field set to a non-null value are considered deleted.
9
9
  * Soft-deleted documents are moved to a separate collection, where they are kept for a configurable amount of time
10
10
  * before being permanently deleted. See the `SoftDeletedFirestoreCollection` decorator for more information.
11
11
  *
12
- * {@link FirestoreStateTransaction.deleteWithSameKeyAs} will delete the document from any of the regular or soft-delete
13
- * collections. It does not throw an error if the document does not exist.
12
+ * {@link FirestoreStateTransaction.delete} will delete the document from any of the regular or soft-delete collections.
13
+ * It does not throw an error if the document does not exist.
14
14
  *
15
- * {@link FirestoreStateTransaction.findOneWithSameKeyAs} will return the document from either the regular or
16
- * soft-delete collection, as expected by {@link FindReplaceStateTransaction.findOneWithSameKeyAs}.
15
+ * {@link FirestoreStateTransaction.get} will return the document from either the regular or soft-delete collection, as
16
+ * expected by {@link StateTransaction.get}.
17
17
  *
18
- * {@link FirestoreStateTransaction.replace} will set the document in the relevant collection, either the regular or
18
+ * {@link FirestoreStateTransaction.set} will set the document in the relevant collection, either the regular or
19
19
  * soft-delete collection depending on the value of the `deletedAt` field.
20
20
  */
21
- export class FirestoreStateTransaction {
22
- transaction;
23
- collectionResolver;
24
- constructor(transaction, collectionResolver) {
25
- this.transaction = transaction;
26
- this.collectionResolver = collectionResolver;
27
- }
28
- async deleteWithSameKeyAs(type, key) {
21
+ export class FirestoreStateTransaction extends FirestoreReadOnlyStateTransaction {
22
+ async delete(typeOrEntity, key) {
23
+ const type = (key === undefined ? typeOrEntity.constructor : typeOrEntity);
24
+ key ??= typeOrEntity;
29
25
  const { activeCollection, softDelete } = this.collectionResolver.getCollectionsForType(type);
30
26
  const activeDocRef = getReferenceForFirestoreDocument(activeCollection, key, type);
31
- this.transaction.delete(activeDocRef);
27
+ this.firestoreTransaction.delete(activeDocRef);
32
28
  if (!softDelete) {
33
29
  return;
34
30
  }
35
31
  const deletedDocRef = getReferenceForFirestoreDocument(softDelete.collection, key, type);
36
- this.transaction.delete(deletedDocRef);
37
- }
38
- async findOneWithSameKeyAs(type, entity) {
39
- const { activeCollection, softDelete } = this.collectionResolver.getCollectionsForType(type);
40
- const activeDocRef = getReferenceForFirestoreDocument(activeCollection, entity, type);
41
- const activeSnapshot = await this.transaction.get(activeDocRef);
42
- if (activeSnapshot.exists) {
43
- return activeSnapshot.data();
44
- }
45
- if (!softDelete) {
46
- return undefined;
47
- }
48
- const deletedDocRef = getReferenceForFirestoreDocument(softDelete.collection, entity, type);
49
- const deletedSnapshot = await this.transaction.get(deletedDocRef);
50
- if (!deletedSnapshot.exists) {
51
- return undefined;
52
- }
53
- const deletedDocument = deletedSnapshot.data();
54
- delete deletedDocument[softDelete.expirationField];
55
- return deletedDocument;
32
+ this.firestoreTransaction.delete(deletedDocRef);
56
33
  }
57
- async replace(entity) {
34
+ async set(entity) {
58
35
  const documentType = entity.constructor;
59
36
  const { activeCollection, softDelete } = this.collectionResolver.getCollectionsForType(documentType);
60
37
  const activeDocRef = getReferenceForFirestoreDocument(activeCollection, entity);
61
38
  if (!softDelete) {
62
- this.transaction.set(activeDocRef, entity);
39
+ this.firestoreTransaction.set(activeDocRef, entity);
63
40
  return;
64
41
  }
65
42
  const deletedDocRef = getReferenceForFirestoreDocument(softDelete.collection, entity);
@@ -70,12 +47,12 @@ export class FirestoreStateTransaction {
70
47
  ...entity,
71
48
  [expirationField]: expiresAt,
72
49
  });
73
- this.transaction.delete(activeDocRef);
74
- this.transaction.set(deletedDocRef, deletedDoc);
50
+ this.firestoreTransaction.delete(activeDocRef);
51
+ this.firestoreTransaction.set(deletedDocRef, deletedDoc);
75
52
  }
76
53
  else {
77
- this.transaction.set(activeDocRef, entity);
78
- this.transaction.delete(deletedDocRef);
54
+ this.firestoreTransaction.set(activeDocRef, entity);
55
+ this.firestoreTransaction.delete(deletedDocRef);
79
56
  }
80
57
  }
81
58
  }
@@ -1,12 +1,24 @@
1
- import { BufferEventTransaction, Transaction } from '@causa/runtime';
1
+ import { OutboxEventTransaction, Transaction, type PublishOptions, type TransactionOption } from '@causa/runtime';
2
+ import type { Type } from '@nestjs/common';
2
3
  import { Transaction as FirestoreTransaction } from 'firebase-admin/firestore';
3
- import { FirestoreStateTransaction } from './state-transaction.js';
4
+ import type { FirestoreStateTransaction } from './state-transaction.js';
5
+ /**
6
+ * Option for a function that accepts a {@link FirestorePubSubTransaction}.
7
+ */
8
+ export type FirestoreOutboxTransactionOption = TransactionOption<FirestorePubSubTransaction>;
4
9
  /**
5
10
  * A {@link Transaction} that uses Firestore for state storage and Pub/Sub for event publishing.
6
11
  */
7
- export declare class FirestorePubSubTransaction extends Transaction<FirestoreStateTransaction, BufferEventTransaction> {
12
+ export declare class FirestorePubSubTransaction extends Transaction {
13
+ readonly stateTransaction: FirestoreStateTransaction;
14
+ private readonly eventTransaction;
15
+ constructor(stateTransaction: FirestoreStateTransaction, eventTransaction: OutboxEventTransaction, publishOptions?: PublishOptions);
8
16
  /**
9
17
  * The underlying {@link FirestoreTransaction} used by the state transaction.
10
18
  */
11
19
  get firestoreTransaction(): FirestoreTransaction;
20
+ set<T extends object>(entity: T): Promise<void>;
21
+ delete<T extends object>(type: Type<T> | T, key?: Partial<T>): Promise<void>;
22
+ get<T extends object>(type: Type<T>, entity: Partial<T>): Promise<T | undefined>;
23
+ publish(topic: string, event: object, options?: PublishOptions): Promise<void>;
12
24
  }
@@ -1,14 +1,32 @@
1
- import { BufferEventTransaction, Transaction } from '@causa/runtime';
1
+ import { OutboxEventTransaction, Transaction, } from '@causa/runtime';
2
2
  import { Transaction as FirestoreTransaction } from 'firebase-admin/firestore';
3
- import { FirestoreStateTransaction } from './state-transaction.js';
4
3
  /**
5
4
  * A {@link Transaction} that uses Firestore for state storage and Pub/Sub for event publishing.
6
5
  */
7
6
  export class FirestorePubSubTransaction extends Transaction {
7
+ stateTransaction;
8
+ eventTransaction;
9
+ constructor(stateTransaction, eventTransaction, publishOptions = {}) {
10
+ super(publishOptions);
11
+ this.stateTransaction = stateTransaction;
12
+ this.eventTransaction = eventTransaction;
13
+ }
8
14
  /**
9
15
  * The underlying {@link FirestoreTransaction} used by the state transaction.
10
16
  */
11
17
  get firestoreTransaction() {
12
- return this.stateTransaction.transaction;
18
+ return this.stateTransaction.firestoreTransaction;
19
+ }
20
+ set(entity) {
21
+ return this.stateTransaction.set(entity);
22
+ }
23
+ delete(type, key) {
24
+ return this.stateTransaction.delete(type, key);
25
+ }
26
+ get(type, entity) {
27
+ return this.stateTransaction.get(type, entity);
28
+ }
29
+ publish(topic, event, options) {
30
+ return this.eventTransaction.publish(topic, event, options);
13
31
  }
14
32
  }