@causa/runtime-google 1.3.0 → 1.4.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.
- package/dist/firestore/collection.decorator.d.ts +4 -3
- package/dist/firestore/collection.decorator.js +4 -3
- package/dist/spanner/entity-manager.d.ts +35 -0
- package/dist/spanner/entity-manager.js +47 -25
- package/dist/spanner/table-cache.d.ts +12 -1
- package/dist/spanner/table-cache.js +42 -2
- package/dist/spanner/table.decorator.d.ts +3 -1
- package/dist/transaction/firestore-pubsub/readonly-state-transaction.d.ts +0 -19
- package/dist/transaction/firestore-pubsub/readonly-state-transaction.js +3 -23
- package/dist/transaction/firestore-pubsub/soft-deleted-collection.decorator.d.ts +18 -0
- package/dist/transaction/firestore-pubsub/soft-deleted-collection.decorator.js +21 -0
- package/dist/transaction/firestore-pubsub/state-transaction.js +3 -2
- package/dist/transaction/firestore-pubsub/transaction.d.ts +4 -1
- package/dist/transaction/firestore-pubsub/transaction.js +4 -0
- package/dist/transaction/spanner-outbox/transaction.d.ts +2 -1
- package/package.json +15 -15
|
@@ -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)
|
|
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
|
|
44
|
-
|
|
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(
|
|
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 {
|
|
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 {
|
|
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
|
|
48
|
-
return
|
|
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
|
|
59
|
-
return
|
|
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,57 @@ 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
|
+
const results = [];
|
|
287
|
+
for await (const row of this.queryStream(optionsOrStatement, statement)) {
|
|
288
|
+
results.push(row);
|
|
289
|
+
}
|
|
290
|
+
return results;
|
|
291
|
+
}
|
|
292
|
+
async *queryStream(optionsOrStatement, statement) {
|
|
298
293
|
const options = statement
|
|
299
294
|
? optionsOrStatement
|
|
300
295
|
: {};
|
|
301
296
|
const sqlStatement = statement ?? optionsOrStatement;
|
|
302
|
-
const { entityType, requestOptions } = options;
|
|
303
|
-
|
|
304
|
-
const
|
|
297
|
+
const { entityType, requestOptions, transaction } = options;
|
|
298
|
+
try {
|
|
299
|
+
const stream = (transaction ?? this.database).runStream({
|
|
305
300
|
...sqlStatement,
|
|
306
301
|
requestOptions,
|
|
307
302
|
json: true,
|
|
308
303
|
jsonOptions: { wrapNumbers: entityType != null },
|
|
309
304
|
});
|
|
310
|
-
|
|
311
|
-
|
|
305
|
+
for await (const row of stream) {
|
|
306
|
+
yield entityType ? spannerObjectToInstance(row, entityType) : row;
|
|
312
307
|
}
|
|
313
|
-
|
|
314
|
-
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
// If running in a transaction, the error will be caught by `snapshot()` or `transaction()`.
|
|
311
|
+
throw transaction ? error : (convertSpannerToEntityError(error) ?? error);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Runs the given SQL statement in the database, returning an async iterable of batches of results.
|
|
316
|
+
* By default, the statement is run in a {@link SpannerReadOnlyTransaction}. To perform a write operation, pass a
|
|
317
|
+
* {@link SpannerReadWriteTransaction} in the options.
|
|
318
|
+
*
|
|
319
|
+
* @param options Options for the operation.
|
|
320
|
+
* @param statement The SQL statement to run.
|
|
321
|
+
* @returns An async iterable that yields batches of rows returned by the query.
|
|
322
|
+
* If {@link QueryOptions.entityType} is set, the rows are converted to instances of that class.
|
|
323
|
+
*/
|
|
324
|
+
async *queryBatches(options, statement) {
|
|
325
|
+
const { batchSize } = options;
|
|
326
|
+
let batch = [];
|
|
327
|
+
for await (const item of this.queryStream(options, statement)) {
|
|
328
|
+
batch.push(item);
|
|
329
|
+
if (batch.length >= batchSize) {
|
|
330
|
+
yield batch;
|
|
331
|
+
batch = [];
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (batch.length > 0) {
|
|
335
|
+
yield batch;
|
|
336
|
+
}
|
|
315
337
|
}
|
|
316
338
|
// Types that can be used as hints to disambiguate query parameter array types.
|
|
317
339
|
static ParamTypeFloat64Array = {
|
|
@@ -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
|
|
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
|
|
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,21 +1,11 @@
|
|
|
1
1
|
import type { ReadOnlyStateTransaction, ReadOnlyTransactionOption } from '@causa/runtime';
|
|
2
2
|
import type { Type } from '@nestjs/common';
|
|
3
3
|
import { Transaction } from 'firebase-admin/firestore';
|
|
4
|
-
import { type SoftDeletedFirestoreCollectionMetadata } from './soft-deleted-collection.decorator.js';
|
|
5
4
|
import type { FirestoreCollectionResolver } from './types.js';
|
|
6
5
|
/**
|
|
7
6
|
* Option for a function that accepts a {@link FirestoreReadOnlyStateTransaction}.
|
|
8
7
|
*/
|
|
9
8
|
export type FirestoreReadOnlyStateTransactionOption = ReadOnlyTransactionOption<FirestoreReadOnlyStateTransaction>;
|
|
10
|
-
/**
|
|
11
|
-
* Information about soft-deletion for a given document.
|
|
12
|
-
*/
|
|
13
|
-
export type SoftDeleteInfo<T extends object> = Omit<SoftDeletedFirestoreCollectionMetadata, 'deletedDocumentsCollectionSuffix'> & {
|
|
14
|
-
/**
|
|
15
|
-
* The reference to the soft-deleted document.
|
|
16
|
-
*/
|
|
17
|
-
ref: FirebaseFirestore.DocumentReference<T>;
|
|
18
|
-
};
|
|
19
9
|
/**
|
|
20
10
|
* A {@link ReadOnlyStateTransaction} that uses Firestore for state storage.
|
|
21
11
|
*
|
|
@@ -43,14 +33,5 @@ export declare class FirestoreReadOnlyStateTransaction implements ReadOnlyStateT
|
|
|
43
33
|
* The resolver that provides the Firestore collections for a given document type.
|
|
44
34
|
*/
|
|
45
35
|
collectionResolver: FirestoreCollectionResolver);
|
|
46
|
-
/**
|
|
47
|
-
* Returns the soft-delete information for a given document, if the document type supports soft-deletion.
|
|
48
|
-
*
|
|
49
|
-
* @param activeDocRef The reference to the active document.
|
|
50
|
-
* @param type The type of the document.
|
|
51
|
-
* @returns The soft-delete information for the document, or `null` if the document type does not support
|
|
52
|
-
* soft-deletion.
|
|
53
|
-
*/
|
|
54
|
-
protected getSoftDeleteInfo<T extends object>(activeDocRef: FirebaseFirestore.DocumentReference<T>, type: Type<T>): SoftDeleteInfo<T> | null;
|
|
55
36
|
get<T extends object>(type: Type<T>, entity: Partial<T>): Promise<T | null>;
|
|
56
37
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Transaction } from 'firebase-admin/firestore';
|
|
2
|
-
import { getReferenceForFirestoreDocument
|
|
3
|
-
import {
|
|
2
|
+
import { getReferenceForFirestoreDocument } from '../../firestore/index.js';
|
|
3
|
+
import { getSoftDeleteInfo } from './soft-deleted-collection.decorator.js';
|
|
4
4
|
/**
|
|
5
5
|
* A {@link ReadOnlyStateTransaction} that uses Firestore for state storage.
|
|
6
6
|
*
|
|
@@ -25,26 +25,6 @@ export class FirestoreReadOnlyStateTransaction {
|
|
|
25
25
|
this.firestoreTransaction = firestoreTransaction;
|
|
26
26
|
this.collectionResolver = collectionResolver;
|
|
27
27
|
}
|
|
28
|
-
/**
|
|
29
|
-
* Returns the soft-delete information for a given document, if the document type supports soft-deletion.
|
|
30
|
-
*
|
|
31
|
-
* @param activeDocRef The reference to the active document.
|
|
32
|
-
* @param type The type of the document.
|
|
33
|
-
* @returns The soft-delete information for the document, or `null` if the document type does not support
|
|
34
|
-
* soft-deletion.
|
|
35
|
-
*/
|
|
36
|
-
getSoftDeleteInfo(activeDocRef, type) {
|
|
37
|
-
const softDeleteMetadata = getSoftDeletedFirestoreCollectionMetadataForType(type);
|
|
38
|
-
if (!softDeleteMetadata) {
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
const { deletedDocumentsCollectionSuffix: suffix, ...info } = softDeleteMetadata;
|
|
42
|
-
const softDeleteCollection = activeDocRef.firestore
|
|
43
|
-
.collection(`${activeDocRef.parent.path}${suffix}`)
|
|
44
|
-
.withConverter(makeFirestoreDataConverter(type));
|
|
45
|
-
const ref = softDeleteCollection.doc(activeDocRef.id);
|
|
46
|
-
return { ...info, ref };
|
|
47
|
-
}
|
|
48
28
|
async get(type, entity) {
|
|
49
29
|
const { activeCollection } = this.collectionResolver.getCollectionsForType(type);
|
|
50
30
|
const activeDocRef = getReferenceForFirestoreDocument(activeCollection, entity, type);
|
|
@@ -53,7 +33,7 @@ export class FirestoreReadOnlyStateTransaction {
|
|
|
53
33
|
if (activeDocument) {
|
|
54
34
|
return activeDocument;
|
|
55
35
|
}
|
|
56
|
-
const softDeleteInfo =
|
|
36
|
+
const softDeleteInfo = getSoftDeleteInfo(activeDocRef, type);
|
|
57
37
|
if (!softDeleteInfo) {
|
|
58
38
|
return null;
|
|
59
39
|
}
|
|
@@ -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
|
*
|
|
@@ -25,7 +26,7 @@ export class FirestoreStateTransaction extends FirestoreReadOnlyStateTransaction
|
|
|
25
26
|
const { activeCollection } = this.collectionResolver.getCollectionsForType(type);
|
|
26
27
|
const activeDocRef = getReferenceForFirestoreDocument(activeCollection, key, type);
|
|
27
28
|
this.firestoreTransaction.delete(activeDocRef);
|
|
28
|
-
const softDeleteInfo =
|
|
29
|
+
const softDeleteInfo = getSoftDeleteInfo(activeDocRef, type);
|
|
29
30
|
if (!softDeleteInfo) {
|
|
30
31
|
return;
|
|
31
32
|
}
|
|
@@ -35,7 +36,7 @@ export class FirestoreStateTransaction extends FirestoreReadOnlyStateTransaction
|
|
|
35
36
|
const documentType = entity.constructor;
|
|
36
37
|
const { activeCollection } = this.collectionResolver.getCollectionsForType(documentType);
|
|
37
38
|
const activeDocRef = getReferenceForFirestoreDocument(activeCollection, entity);
|
|
38
|
-
const softDeleteInfo =
|
|
39
|
+
const softDeleteInfo = getSoftDeleteInfo(activeDocRef, documentType);
|
|
39
40
|
if (!softDeleteInfo) {
|
|
40
41
|
this.firestoreTransaction.set(activeDocRef, entity);
|
|
41
42
|
return;
|
|
@@ -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
|
}
|
|
@@ -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.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "An extension to the Causa runtime SDK (`@causa/runtime`), providing Google-specific features.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -32,48 +32,48 @@
|
|
|
32
32
|
"test:cov": "npm run test -- --coverage"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@causa/runtime": "^1.
|
|
35
|
+
"@causa/runtime": "^1.4.0",
|
|
36
36
|
"@google-cloud/precise-date": "^5.0.0",
|
|
37
37
|
"@google-cloud/pubsub": "^5.2.0",
|
|
38
38
|
"@google-cloud/spanner": "^8.2.2",
|
|
39
39
|
"@google-cloud/tasks": "^6.2.1",
|
|
40
|
-
"@grpc/grpc-js": "^1.14.
|
|
41
|
-
"@nestjs/common": "^11.1.
|
|
40
|
+
"@grpc/grpc-js": "^1.14.1",
|
|
41
|
+
"@nestjs/common": "^11.1.9",
|
|
42
42
|
"@nestjs/config": "^4.0.2",
|
|
43
|
-
"@nestjs/core": "^11.1.
|
|
43
|
+
"@nestjs/core": "^11.1.9",
|
|
44
44
|
"@nestjs/passport": "^11.0.5",
|
|
45
45
|
"@nestjs/terminus": "^11.0.0",
|
|
46
46
|
"class-transformer": "^0.5.1",
|
|
47
47
|
"class-validator": "^0.14.2",
|
|
48
48
|
"express": "^5.1.0",
|
|
49
|
-
"firebase-admin": "^13.
|
|
49
|
+
"firebase-admin": "^13.6.0",
|
|
50
50
|
"jsonwebtoken": "^9.0.2",
|
|
51
51
|
"passport-http-bearer": "^1.0.1",
|
|
52
|
-
"pino": "^9.
|
|
52
|
+
"pino": "^9.14.0",
|
|
53
53
|
"reflect-metadata": "^0.2.2"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@nestjs/testing": "^11.1.
|
|
57
|
-
"@swc/core": "^1.
|
|
56
|
+
"@nestjs/testing": "^11.1.9",
|
|
57
|
+
"@swc/core": "^1.15.2",
|
|
58
58
|
"@swc/jest": "^0.2.39",
|
|
59
|
-
"@tsconfig/
|
|
59
|
+
"@tsconfig/node20": "^20.1.7",
|
|
60
60
|
"@types/jest": "^30.0.0",
|
|
61
61
|
"@types/jsonwebtoken": "^9.0.10",
|
|
62
|
-
"@types/node": "^
|
|
62
|
+
"@types/node": "^20.19.25",
|
|
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.
|
|
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
|
-
"jest-extended": "^
|
|
71
|
+
"jest-extended": "^7.0.0",
|
|
72
72
|
"pino-pretty": "^13.1.2",
|
|
73
|
-
"rimraf": "^6.0
|
|
73
|
+
"rimraf": "^6.1.0",
|
|
74
74
|
"supertest": "^7.1.4",
|
|
75
75
|
"typescript": "^5.9.3",
|
|
76
|
-
"typescript-eslint": "^8.46.
|
|
76
|
+
"typescript-eslint": "^8.46.4",
|
|
77
77
|
"uuid": "^13.0.0"
|
|
78
78
|
}
|
|
79
79
|
}
|