@causa/runtime-google 1.2.0 → 1.4.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.
@@ -11,12 +11,13 @@ export type FirestoreCollectionMetadata<T> = {
11
11
  name: string;
12
12
  /**
13
13
  * Returns the path of the document (relative to the collection) for the given partial document.
14
+ * If an array is returned, the segments are joined with `/` to form the full path.
15
+ * If any `undefined` or `null` value is returned, an error is thrown.
14
16
  *
15
17
  * @param document The (partial) document for which the path should be returned.
16
- * @returns The path of the document (relative to the collection), or `undefined` if the path cannot be derived from
17
- * the partial document.
18
+ * @returns The path of the document (relative to the collection).
18
19
  */
19
- path: (document: Partial<T>) => string | undefined;
20
+ path: (document: Partial<T>) => string | undefined | null | (string | undefined | null)[];
20
21
  };
21
22
  /**
22
23
  * Defines this class as a type of document stored in the given Firestore collection.
@@ -40,9 +40,10 @@ export function getFirestoreCollectionMetadataForType(documentType) {
40
40
  export function getReferenceForFirestoreDocument(collection, document, documentType) {
41
41
  documentType ??= document.constructor;
42
42
  const { path } = getFirestoreCollectionMetadataForType(documentType);
43
- const relativePath = path(document);
44
- if (!relativePath) {
43
+ const rawPath = path(document);
44
+ const pathArray = Array.isArray(rawPath) ? rawPath : [rawPath];
45
+ if (pathArray.some((p) => p == undefined)) {
45
46
  throw new Error(`The path of the '${documentType.name}' document cannot be obtained from the given object.`);
46
47
  }
47
- return collection.doc(relativePath);
48
+ return collection.doc(pathArray.join('/'));
48
49
  }
@@ -277,6 +277,41 @@ export declare class SpannerEntityManager {
277
277
  * @returns The rows returned by the query.
278
278
  */
279
279
  query<T>(statement: SqlStatement): Promise<T[]>;
280
+ /**
281
+ * Runs the given SQL statement in the database, returning an async iterable of the results.
282
+ * By default, the statement is run in a {@link SpannerReadOnlyTransaction}. To perform a write operation, pass a
283
+ * {@link SpannerReadWriteTransaction} in the options.
284
+ *
285
+ * @param options Options for the operation.
286
+ * @param statement The SQL statement to run.
287
+ * @returns An async iterable that yields the rows returned by the query.
288
+ * If {@link QueryOptions.entityType} is set, the rows are converted to instances of that class.
289
+ */
290
+ queryStream<T>(options: QueryOptions<T>, statement: SqlStatement): AsyncIterable<T>;
291
+ /**
292
+ * Runs the given SQL statement in the database, returning an async iterable of the results.
293
+ * The statement is run in a {@link SpannerReadOnlyTransaction}.
294
+ *
295
+ * @param statement The SQL statement to run.
296
+ * @returns An async iterable that yields the rows returned by the query.
297
+ */
298
+ queryStream<T>(statement: SqlStatement): AsyncIterable<T>;
299
+ /**
300
+ * Runs the given SQL statement in the database, returning an async iterable of batches of results.
301
+ * By default, the statement is run in a {@link SpannerReadOnlyTransaction}. To perform a write operation, pass a
302
+ * {@link SpannerReadWriteTransaction} in the options.
303
+ *
304
+ * @param options Options for the operation.
305
+ * @param statement The SQL statement to run.
306
+ * @returns An async iterable that yields batches of rows returned by the query.
307
+ * If {@link QueryOptions.entityType} is set, the rows are converted to instances of that class.
308
+ */
309
+ queryBatches<T>(options: QueryOptions<T> & {
310
+ /**
311
+ * The maximum number of items to include in each batch.
312
+ */
313
+ batchSize: number;
314
+ }, statement: SqlStatement): AsyncIterable<T[]>;
280
315
  static readonly ParamTypeFloat64Array: ParamType;
281
316
  static readonly ParamTypeInt64Array: ParamType;
282
317
  static readonly ParamTypeNumericArray: ParamType;
@@ -9,11 +9,11 @@ var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  };
10
10
  import { EntityNotFoundError } from '@causa/runtime';
11
11
  import { Database, Snapshot, Transaction } from '@google-cloud/spanner';
12
- import { Int, } from '@google-cloud/spanner/build/src/codec.js';
12
+ import {} from '@google-cloud/spanner/build/src/codec.js';
13
13
  import { Injectable } from '@nestjs/common';
14
14
  import { copyInstanceWithMissingColumnsToNull, instanceToSpannerObject, spannerObjectToInstance, updateInstanceByColumn, } from './conversion.js';
15
15
  import { convertSpannerToEntityError } from './error-converter.js';
16
- import { EntityMissingPrimaryKeyError, InvalidArgumentError, TransactionFinishedError, } from './errors.js';
16
+ import { InvalidArgumentError, TransactionFinishedError } from './errors.js';
17
17
  import { SpannerTableCache } from './table-cache.js';
18
18
  /**
19
19
  * A class that manages access to entities stored in a Cloud Spanner database.
@@ -44,8 +44,8 @@ let SpannerEntityManager = class SpannerEntityManager {
44
44
  */
45
45
  getPrimaryKey(entity, entityType) {
46
46
  entityType ??= entity.constructor;
47
- const obj = instanceToSpannerObject(entity, entityType);
48
- return this.getPrimaryKeyForSpannerObject(obj, entityType);
47
+ const { primaryKeyGetter } = this.tableCache.getMetadata(entityType);
48
+ return primaryKeyGetter(entity);
49
49
  }
50
50
  /**
51
51
  * Returns the primary key of the given Spanner object, assumed to be an entity of the given type.
@@ -55,20 +55,8 @@ let SpannerEntityManager = class SpannerEntityManager {
55
55
  * @returns The primary key of the entity.
56
56
  */
57
57
  getPrimaryKeyForSpannerObject(obj, entityType) {
58
- const { primaryKeyColumns } = this.tableCache.getMetadata(entityType);
59
- return primaryKeyColumns.map((c) => {
60
- const value = obj[c];
61
- if (value === undefined) {
62
- throw new EntityMissingPrimaryKeyError();
63
- }
64
- if (value instanceof Int) {
65
- return value.value;
66
- }
67
- if (value instanceof Date) {
68
- return value.toJSON();
69
- }
70
- return value;
71
- });
58
+ const entity = spannerObjectToInstance(obj, entityType);
59
+ return this.getPrimaryKey(entity, entityType);
72
60
  }
73
61
  /**
74
62
  * Returns the (quoted) name of the table for the given entity type.
@@ -295,23 +283,53 @@ let SpannerEntityManager = class SpannerEntityManager {
295
283
  await this.transaction(options, (transaction) => transaction.runUpdate(`DELETE FROM \`${tableName}\` WHERE TRUE`));
296
284
  }
297
285
  async query(optionsOrStatement, statement) {
286
+ return await Array.fromAsync(this.queryStream(optionsOrStatement, statement));
287
+ }
288
+ async *queryStream(optionsOrStatement, statement) {
298
289
  const options = statement
299
290
  ? optionsOrStatement
300
291
  : {};
301
292
  const sqlStatement = statement ?? optionsOrStatement;
302
- const { entityType, requestOptions } = options;
303
- return await this.snapshot({ transaction: options.transaction }, async (transaction) => {
304
- const [rows] = await transaction.run({
293
+ const { entityType, requestOptions, transaction } = options;
294
+ try {
295
+ const stream = (transaction ?? this.database).runStream({
305
296
  ...sqlStatement,
306
297
  requestOptions,
307
298
  json: true,
308
299
  jsonOptions: { wrapNumbers: entityType != null },
309
300
  });
310
- if (entityType) {
311
- return rows.map((row) => spannerObjectToInstance(row, entityType));
301
+ for await (const row of stream) {
302
+ yield entityType ? spannerObjectToInstance(row, entityType) : row;
312
303
  }
313
- return rows;
314
- });
304
+ }
305
+ catch (error) {
306
+ // If running in a transaction, the error will be caught by `snapshot()` or `transaction()`.
307
+ throw transaction ? error : (convertSpannerToEntityError(error) ?? error);
308
+ }
309
+ }
310
+ /**
311
+ * Runs the given SQL statement in the database, returning an async iterable of batches of results.
312
+ * By default, the statement is run in a {@link SpannerReadOnlyTransaction}. To perform a write operation, pass a
313
+ * {@link SpannerReadWriteTransaction} in the options.
314
+ *
315
+ * @param options Options for the operation.
316
+ * @param statement The SQL statement to run.
317
+ * @returns An async iterable that yields batches of rows returned by the query.
318
+ * If {@link QueryOptions.entityType} is set, the rows are converted to instances of that class.
319
+ */
320
+ async *queryBatches(options, statement) {
321
+ const { batchSize } = options;
322
+ let batch = [];
323
+ for await (const item of this.queryStream(options, statement)) {
324
+ batch.push(item);
325
+ if (batch.length >= batchSize) {
326
+ yield batch;
327
+ batch = [];
328
+ }
329
+ }
330
+ if (batch.length > 0) {
331
+ yield batch;
332
+ }
315
333
  }
316
334
  // Types that can be used as hints to disambiguate query parameter array types.
317
335
  static ParamTypeFloat64Array = {
@@ -1,7 +1,7 @@
1
1
  export { SpannerColumn } from './column.decorator.js';
2
- export { SPANNER_SESSION_POOL_OPTIONS_FOR_CLOUD_FUNCTIONS, SPANNER_SESSION_POOL_OPTIONS_FOR_SERVICE, catchSpannerDatabaseErrors, getDefaultSpannerDatabaseForCloudFunction, } from './database.js';
2
+ export { catchSpannerDatabaseErrors, getDefaultSpannerDatabaseForCloudFunction, SPANNER_SESSION_POOL_OPTIONS_FOR_CLOUD_FUNCTIONS, SPANNER_SESSION_POOL_OPTIONS_FOR_SERVICE, } from './database.js';
3
3
  export { SpannerEntityManager } from './entity-manager.js';
4
- export type { SpannerKey, SpannerReadOnlyTransaction, SpannerReadWriteTransaction, } from './entity-manager.js';
4
+ export type { SpannerKey, SpannerReadOnlyTransaction, SpannerReadWriteTransaction, SqlStatement, } from './entity-manager.js';
5
5
  export * from './errors.js';
6
6
  export { SpannerHealthIndicator } from './healthcheck.js';
7
7
  export { SpannerModule } from './module.js';
@@ -1,5 +1,5 @@
1
1
  export { SpannerColumn } from './column.decorator.js';
2
- export { SPANNER_SESSION_POOL_OPTIONS_FOR_CLOUD_FUNCTIONS, SPANNER_SESSION_POOL_OPTIONS_FOR_SERVICE, catchSpannerDatabaseErrors, getDefaultSpannerDatabaseForCloudFunction, } from './database.js';
2
+ export { catchSpannerDatabaseErrors, getDefaultSpannerDatabaseForCloudFunction, SPANNER_SESSION_POOL_OPTIONS_FOR_CLOUD_FUNCTIONS, SPANNER_SESSION_POOL_OPTIONS_FOR_SERVICE, } from './database.js';
3
3
  export { SpannerEntityManager } from './entity-manager.js';
4
4
  export * from './errors.js';
5
5
  export { SpannerHealthIndicator } from './healthcheck.js';
@@ -1,4 +1,9 @@
1
1
  import type { Type } from '@nestjs/common';
2
+ import type { SpannerKey } from './entity-manager.js';
3
+ /**
4
+ * A function that, given an entity, returns its primary key as a {@link SpannerKey}.
5
+ */
6
+ type SpannerPrimaryKeyGetter = (entity: any) => SpannerKey;
2
7
  /**
3
8
  * An object providing metadata about a Spanner table and its columns.
4
9
  */
@@ -8,9 +13,14 @@ export type CachedSpannerTableMetadata = {
8
13
  */
9
14
  tableName: string;
10
15
  /**
11
- * The (ordered) list of columns that are part of the primary key.
16
+ * The list of columns that are used to compute the primary key.
17
+ * For primary keys with generated columns, this includes the columns off of which the key is composed.
12
18
  */
13
19
  primaryKeyColumns: string[];
20
+ /**
21
+ * A function that, given an entity, returns its primary key as a {@link SpannerKey}.
22
+ */
23
+ primaryKeyGetter: SpannerPrimaryKeyGetter;
14
24
  /**
15
25
  * A map from class property names to Spanner column names.
16
26
  */
@@ -43,3 +53,4 @@ export declare class SpannerTableCache {
43
53
  */
44
54
  getMetadata(entityType: Type): CachedSpannerTableMetadata;
45
55
  }
56
+ export {};
@@ -1,6 +1,34 @@
1
+ import { Float, Float32, Int, Numeric } from '@google-cloud/spanner';
1
2
  import { getSpannerColumnsMetadata } from './column.decorator.js';
2
- import { InvalidEntityDefinitionError } from './errors.js';
3
+ import { EntityMissingPrimaryKeyError, InvalidEntityDefinitionError, } from './errors.js';
3
4
  import { getSpannerTableMetadataFromType } from './table.decorator.js';
5
+ /**
6
+ * Constructs a {@link SpannerPrimaryKeyGetter} for the given property paths.
7
+ *
8
+ * @param paths The paths to resolve in the entity to get primary key values.
9
+ * @returns The {@link SpannerPrimaryKeyGetter}.
10
+ */
11
+ function makePrimaryKeyGetter(paths) {
12
+ return (entity) => paths.map((path) => {
13
+ const value = path.reduce((v, k) => v?.[k], entity);
14
+ if (value === undefined) {
15
+ throw new EntityMissingPrimaryKeyError();
16
+ }
17
+ if (value instanceof Int ||
18
+ value instanceof Float ||
19
+ value instanceof Float32 ||
20
+ value instanceof Numeric) {
21
+ return value.value.toString();
22
+ }
23
+ if (value instanceof Date) {
24
+ return value.toJSON();
25
+ }
26
+ if (typeof value === 'bigint' || typeof value === 'number') {
27
+ return value.toString();
28
+ }
29
+ return value;
30
+ });
31
+ }
4
32
  /**
5
33
  * A cache storing the {@link CachedSpannerTableMetadata} for each entity type (class).
6
34
  */
@@ -21,7 +49,6 @@ export class SpannerTableCache {
21
49
  throw new InvalidEntityDefinitionError(entityType);
22
50
  }
23
51
  const tableName = tableMetadata.name;
24
- const primaryKeyColumns = tableMetadata.primaryKey;
25
52
  const columnsMetadata = getSpannerColumnsMetadata(entityType);
26
53
  const softDeleteColumns = Object.values(columnsMetadata).filter((metadata) => metadata.softDelete);
27
54
  if (softDeleteColumns.length > 1) {
@@ -29,9 +56,22 @@ export class SpannerTableCache {
29
56
  }
30
57
  const softDeleteColumn = softDeleteColumns[0]?.name ?? null;
31
58
  const columnNames = Object.fromEntries(Object.entries(columnsMetadata).map(([prop, { name }]) => [prop, name]));
59
+ const primaryKeyColumnsSet = new Set();
60
+ const primaryPropertyPaths = tableMetadata.primaryKey.map((path) => {
61
+ const [columnName, ...nested] = path.split('.');
62
+ primaryKeyColumnsSet.add(columnName);
63
+ const property = Object.entries(columnNames).find(([, name]) => name === columnName);
64
+ if (!property) {
65
+ throw new InvalidEntityDefinitionError(entityType, `Primary key column '${columnName}' does not exist.`);
66
+ }
67
+ return [property[0], ...nested];
68
+ });
69
+ const primaryKeyColumns = [...primaryKeyColumnsSet];
70
+ const primaryKeyGetter = makePrimaryKeyGetter(primaryPropertyPaths);
32
71
  return {
33
72
  tableName,
34
73
  primaryKeyColumns,
74
+ primaryKeyGetter,
35
75
  softDeleteColumn,
36
76
  columnNames,
37
77
  };
@@ -9,7 +9,9 @@ export type SpannerTableMetadata = {
9
9
  */
10
10
  name: string;
11
11
  /**
12
- * The (ordered) list of columns in the class defining the primary key.
12
+ * The (ordered) list of columns in the table defining the primary key.
13
+ * Column names should be used rather than property names (if they differ).
14
+ * Nested columns can be used, using dot notation (e.g., `address.city`).
13
15
  */
14
16
  primaryKey: string[];
15
17
  };
@@ -1,5 +1,6 @@
1
1
  import { Transaction } from 'firebase-admin/firestore';
2
2
  import { getReferenceForFirestoreDocument } from '../../firestore/index.js';
3
+ import { getSoftDeleteInfo } from './soft-deleted-collection.decorator.js';
3
4
  /**
4
5
  * A {@link ReadOnlyStateTransaction} that uses Firestore for state storage.
5
6
  *
@@ -25,23 +26,23 @@ export class FirestoreReadOnlyStateTransaction {
25
26
  this.collectionResolver = collectionResolver;
26
27
  }
27
28
  async get(type, entity) {
28
- const { activeCollection, softDelete } = this.collectionResolver.getCollectionsForType(type);
29
+ const { activeCollection } = this.collectionResolver.getCollectionsForType(type);
29
30
  const activeDocRef = getReferenceForFirestoreDocument(activeCollection, entity, type);
30
31
  const activeSnapshot = await this.firestoreTransaction.get(activeDocRef);
31
32
  const activeDocument = activeSnapshot.data();
32
33
  if (activeDocument) {
33
34
  return activeDocument;
34
35
  }
35
- if (!softDelete) {
36
+ const softDeleteInfo = getSoftDeleteInfo(activeDocRef, type);
37
+ if (!softDeleteInfo) {
36
38
  return null;
37
39
  }
38
- const deletedDocRef = getReferenceForFirestoreDocument(softDelete.collection, entity, type);
39
- const deletedSnapshot = await this.firestoreTransaction.get(deletedDocRef);
40
+ const deletedSnapshot = await this.firestoreTransaction.get(softDeleteInfo.ref);
40
41
  const deletedDocument = deletedSnapshot.data();
41
42
  if (!deletedDocument) {
42
43
  return null;
43
44
  }
44
- delete deletedDocument[softDelete.expirationField];
45
+ delete deletedDocument[softDeleteInfo.expirationField];
45
46
  return deletedDocument;
46
47
  }
47
48
  }
@@ -24,6 +24,15 @@ export type SoftDeletedFirestoreCollectionMetadata = {
24
24
  */
25
25
  deletedDocumentsCollectionSuffix: string;
26
26
  };
27
+ /**
28
+ * Information about soft-deletion for a given document.
29
+ */
30
+ export type SoftDeleteInfo<T extends object> = Omit<SoftDeletedFirestoreCollectionMetadata, 'deletedDocumentsCollectionSuffix'> & {
31
+ /**
32
+ * The reference to the soft-deleted document.
33
+ */
34
+ ref: FirebaseFirestore.DocumentReference<T>;
35
+ };
27
36
  /**
28
37
  * Defines this class as a type of Firestore document stored that can be soft-deleted.
29
38
  * Documents should be {@link VersionedEntity} that are considered soft-deleted when their `deletedAt` field is set.
@@ -39,3 +48,12 @@ export declare function SoftDeletedFirestoreCollection(metadata?: Partial<SoftDe
39
48
  * @returns The metadata for the soft-deleted Firestore collection, or `null` if the type is not decorated.
40
49
  */
41
50
  export declare function getSoftDeletedFirestoreCollectionMetadataForType(documentType: Type): SoftDeletedFirestoreCollectionMetadata | null;
51
+ /**
52
+ * Returns the soft-delete information for a given document, if the document type supports soft-deletion.
53
+ *
54
+ * @param activeDocRef The reference to the active document.
55
+ * @param type The type of the document.
56
+ * @returns The soft-delete information for the document, or `null` if the document type does not support
57
+ * soft-deletion.
58
+ */
59
+ export declare function getSoftDeleteInfo<T extends object>(activeDocRef: FirebaseFirestore.DocumentReference<T>, type: Type<T>): SoftDeleteInfo<T> | null;
@@ -1,4 +1,5 @@
1
1
  import 'reflect-metadata';
2
+ import { makeFirestoreDataConverter } from '../../firestore/index.js';
2
3
  /**
3
4
  * The name of the metadata key used to store the soft-deleted Firestore collection metadata.
4
5
  */
@@ -30,3 +31,23 @@ export function getSoftDeletedFirestoreCollectionMetadataForType(documentType) {
30
31
  const metadata = Reflect.getOwnMetadata(FIRESTORE_SOFT_DELETED_COLLECTION_METADATA_KEY, documentType);
31
32
  return metadata ?? null;
32
33
  }
34
+ /**
35
+ * Returns the soft-delete information for a given document, if the document type supports soft-deletion.
36
+ *
37
+ * @param activeDocRef The reference to the active document.
38
+ * @param type The type of the document.
39
+ * @returns The soft-delete information for the document, or `null` if the document type does not support
40
+ * soft-deletion.
41
+ */
42
+ export function getSoftDeleteInfo(activeDocRef, type) {
43
+ const softDeleteMetadata = getSoftDeletedFirestoreCollectionMetadataForType(type);
44
+ if (!softDeleteMetadata) {
45
+ return null;
46
+ }
47
+ const { deletedDocumentsCollectionSuffix: suffix, ...info } = softDeleteMetadata;
48
+ const softDeleteCollection = activeDocRef.firestore
49
+ .collection(`${activeDocRef.parent.path}${suffix}`)
50
+ .withConverter(makeFirestoreDataConverter(type));
51
+ const ref = softDeleteCollection.doc(activeDocRef.id);
52
+ return { ...info, ref };
53
+ }
@@ -1,6 +1,7 @@
1
1
  import { plainToInstance } from 'class-transformer';
2
2
  import { getReferenceForFirestoreDocument } from '../../firestore/index.js';
3
3
  import { FirestoreReadOnlyStateTransaction } from './readonly-state-transaction.js';
4
+ import { getSoftDeleteInfo } from './soft-deleted-collection.decorator.js';
4
5
  /**
5
6
  * A {@link StateTransaction} that uses Firestore for state storage.
6
7
  *
@@ -22,26 +23,26 @@ export class FirestoreStateTransaction extends FirestoreReadOnlyStateTransaction
22
23
  async delete(typeOrEntity, key) {
23
24
  const type = (key === undefined ? typeOrEntity.constructor : typeOrEntity);
24
25
  key ??= typeOrEntity;
25
- const { activeCollection, softDelete } = this.collectionResolver.getCollectionsForType(type);
26
+ const { activeCollection } = this.collectionResolver.getCollectionsForType(type);
26
27
  const activeDocRef = getReferenceForFirestoreDocument(activeCollection, key, type);
27
28
  this.firestoreTransaction.delete(activeDocRef);
28
- if (!softDelete) {
29
+ const softDeleteInfo = getSoftDeleteInfo(activeDocRef, type);
30
+ if (!softDeleteInfo) {
29
31
  return;
30
32
  }
31
- const deletedDocRef = getReferenceForFirestoreDocument(softDelete.collection, key, type);
32
- this.firestoreTransaction.delete(deletedDocRef);
33
+ this.firestoreTransaction.delete(softDeleteInfo.ref);
33
34
  }
34
35
  async set(entity) {
35
36
  const documentType = entity.constructor;
36
- const { activeCollection, softDelete } = this.collectionResolver.getCollectionsForType(documentType);
37
+ const { activeCollection } = this.collectionResolver.getCollectionsForType(documentType);
37
38
  const activeDocRef = getReferenceForFirestoreDocument(activeCollection, entity);
38
- if (!softDelete) {
39
+ const softDeleteInfo = getSoftDeleteInfo(activeDocRef, documentType);
40
+ if (!softDeleteInfo) {
39
41
  this.firestoreTransaction.set(activeDocRef, entity);
40
42
  return;
41
43
  }
42
- const deletedDocRef = getReferenceForFirestoreDocument(softDelete.collection, entity);
44
+ const { ref: deletedDocRef, expirationDelay, expirationField, } = softDeleteInfo;
43
45
  if ('deletedAt' in entity && entity.deletedAt instanceof Date) {
44
- const { expirationDelay, expirationField } = softDelete;
45
46
  const expiresAt = new Date(entity.deletedAt.getTime() + expirationDelay);
46
47
  const deletedDoc = plainToInstance(documentType, {
47
48
  ...entity,
@@ -1,7 +1,9 @@
1
1
  import { OutboxEventTransaction, Transaction, type PublishOptions, type TransactionOption } from '@causa/runtime';
2
2
  import type { Type } from '@nestjs/common';
3
3
  import { Transaction as FirestoreTransaction } from 'firebase-admin/firestore';
4
+ import { FirestoreReadOnlyStateTransaction } from './readonly-state-transaction.js';
4
5
  import type { FirestoreStateTransaction } from './state-transaction.js';
6
+ import type { FirestoreCollectionResolver } from './types.js';
5
7
  /**
6
8
  * Option for a function that accepts a {@link FirestorePubSubTransaction}.
7
9
  */
@@ -9,7 +11,7 @@ export type FirestoreOutboxTransactionOption = TransactionOption<FirestorePubSub
9
11
  /**
10
12
  * A {@link Transaction} that uses Firestore for state storage and Pub/Sub for event publishing.
11
13
  */
12
- export declare class FirestorePubSubTransaction extends Transaction {
14
+ export declare class FirestorePubSubTransaction extends Transaction implements FirestoreReadOnlyStateTransaction {
13
15
  readonly stateTransaction: FirestoreStateTransaction;
14
16
  private readonly eventTransaction;
15
17
  constructor(stateTransaction: FirestoreStateTransaction, eventTransaction: OutboxEventTransaction, publishOptions?: PublishOptions);
@@ -17,6 +19,7 @@ export declare class FirestorePubSubTransaction extends Transaction {
17
19
  * The underlying {@link FirestoreTransaction} used by the state transaction.
18
20
  */
19
21
  get firestoreTransaction(): FirestoreTransaction;
22
+ get collectionResolver(): FirestoreCollectionResolver;
20
23
  set<T extends object>(entity: T): Promise<void>;
21
24
  delete<T extends object>(type: Type<T> | T, key?: Partial<T>): Promise<void>;
22
25
  get<T extends object>(type: Type<T>, entity: Partial<T>): Promise<T | null>;
@@ -1,5 +1,6 @@
1
1
  import { OutboxEventTransaction, Transaction, } from '@causa/runtime';
2
2
  import { Transaction as FirestoreTransaction } from 'firebase-admin/firestore';
3
+ import { FirestoreReadOnlyStateTransaction } from './readonly-state-transaction.js';
3
4
  /**
4
5
  * A {@link Transaction} that uses Firestore for state storage and Pub/Sub for event publishing.
5
6
  */
@@ -17,6 +18,9 @@ export class FirestorePubSubTransaction extends Transaction {
17
18
  get firestoreTransaction() {
18
19
  return this.stateTransaction.firestoreTransaction;
19
20
  }
21
+ get collectionResolver() {
22
+ return this.stateTransaction.collectionResolver;
23
+ }
20
24
  set(entity) {
21
25
  return this.stateTransaction.set(entity);
22
26
  }
@@ -12,6 +12,8 @@ export type FirestoreCollectionsForDocumentType<T> = {
12
12
  /**
13
13
  * Configuration about the soft-delete collection, where documents are stored when their `deletedAt` field is not
14
14
  * `null`. This can be `null` if the document type does not declare a soft-delete collection.
15
+ *
16
+ * @deprecated Use `SoftDeleteInfo` in `FirestoreReadOnlyStateTransaction.getSoftDeleteInfo` instead.
15
17
  */
16
18
  readonly softDelete: ({
17
19
  /**
@@ -1,6 +1,7 @@
1
1
  import { OutboxEventTransaction, Transaction, type OutboxTransaction, type PublishOptions, type TransactionOption } from '@causa/runtime';
2
2
  import type { Type } from '@nestjs/common';
3
3
  import { SpannerEntityManager, type SpannerReadWriteTransaction } from '../../spanner/index.js';
4
+ import type { SpannerReadOnlyStateTransaction } from './readonly-transaction.js';
4
5
  import type { SpannerStateTransaction } from './state-transaction.js';
5
6
  /**
6
7
  * Option for a function that accepts a {@link SpannerOutboxTransaction}.
@@ -9,7 +10,7 @@ export type SpannerOutboxTransactionOption = TransactionOption<SpannerOutboxTran
9
10
  /**
10
11
  * A {@link Transaction} that uses Spanner for state (and outbox) storage, and Pub/Sub for event publishing.
11
12
  */
12
- export declare class SpannerOutboxTransaction extends Transaction implements OutboxTransaction {
13
+ export declare class SpannerOutboxTransaction extends Transaction implements OutboxTransaction, SpannerReadOnlyStateTransaction {
13
14
  readonly stateTransaction: SpannerStateTransaction;
14
15
  readonly eventTransaction: OutboxEventTransaction;
15
16
  constructor(stateTransaction: SpannerStateTransaction, eventTransaction: OutboxEventTransaction, publishOptions?: PublishOptions);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@causa/runtime-google",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "An extension to the Causa runtime SDK (`@causa/runtime`), providing Google-specific features.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,15 +32,15 @@
32
32
  "test:cov": "npm run test -- --coverage"
33
33
  },
34
34
  "dependencies": {
35
- "@causa/runtime": "^1.1.0",
35
+ "@causa/runtime": "^1.4.0",
36
36
  "@google-cloud/precise-date": "^5.0.0",
37
37
  "@google-cloud/pubsub": "^5.2.0",
38
- "@google-cloud/spanner": "^8.2.1",
39
- "@google-cloud/tasks": "^6.2.0",
38
+ "@google-cloud/spanner": "^8.2.2",
39
+ "@google-cloud/tasks": "^6.2.1",
40
40
  "@grpc/grpc-js": "^1.14.0",
41
- "@nestjs/common": "^11.1.6",
41
+ "@nestjs/common": "^11.1.8",
42
42
  "@nestjs/config": "^4.0.2",
43
- "@nestjs/core": "^11.1.6",
43
+ "@nestjs/core": "^11.1.8",
44
44
  "@nestjs/passport": "^11.0.5",
45
45
  "@nestjs/terminus": "^11.0.0",
46
46
  "class-transformer": "^0.5.1",
@@ -49,31 +49,31 @@
49
49
  "firebase-admin": "^13.5.0",
50
50
  "jsonwebtoken": "^9.0.2",
51
51
  "passport-http-bearer": "^1.0.1",
52
- "pino": "^9.12.0",
52
+ "pino": "^9.14.0",
53
53
  "reflect-metadata": "^0.2.2"
54
54
  },
55
55
  "devDependencies": {
56
- "@nestjs/testing": "^11.1.6",
57
- "@swc/core": "^1.13.5",
56
+ "@nestjs/testing": "^11.1.8",
57
+ "@swc/core": "^1.15.0",
58
58
  "@swc/jest": "^0.2.39",
59
59
  "@tsconfig/node22": "^22.0.2",
60
60
  "@types/jest": "^30.0.0",
61
61
  "@types/jsonwebtoken": "^9.0.10",
62
- "@types/node": "^22.18.8",
62
+ "@types/node": "^22.19.0",
63
63
  "@types/passport-http-bearer": "^1.0.42",
64
64
  "@types/supertest": "^6.0.3",
65
65
  "@types/uuid": "^11.0.0",
66
66
  "dotenv": "^17.2.3",
67
- "eslint": "^9.36.0",
67
+ "eslint": "^9.39.1",
68
68
  "eslint-config-prettier": "^10.1.8",
69
69
  "eslint-plugin-prettier": "^5.5.4",
70
70
  "jest": "^30.2.0",
71
71
  "jest-extended": "^6.0.0",
72
- "pino-pretty": "^13.1.1",
73
- "rimraf": "^6.0.1",
72
+ "pino-pretty": "^13.1.2",
73
+ "rimraf": "^6.1.0",
74
74
  "supertest": "^7.1.4",
75
75
  "typescript": "^5.9.3",
76
- "typescript-eslint": "^8.45.0",
76
+ "typescript-eslint": "^8.46.3",
77
77
  "uuid": "^13.0.0"
78
78
  }
79
79
  }