@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
@@ -7,38 +7,23 @@ import { getSpannerColumnsMetadata, } from './column.decorator.js';
7
7
  *
8
8
  * @param spannerObject The object returned by Spanner that should be converted back to a class instance.
9
9
  * @param type The class for the object.
10
- * @param options Used for recursion and should not be set directly.
11
10
  * @returns The created object.
12
11
  */
13
- function spannerObjectToInstanceWithOptions(spannerObject, type, options = {}) {
12
+ export function spannerObjectToInstance(spannerObject, type) {
14
13
  const columnsMetadata = getSpannerColumnsMetadata(type);
15
- const columnNamePrefix = options.columnNamePrefix ?? '';
16
- const plain = {};
17
- let hasAtLeastOneNonNullValue = false;
18
- Object.entries(columnsMetadata).forEach(([property, columnMetadata]) => {
19
- const columnName = `${columnNamePrefix}${columnMetadata.name}`;
20
- if (columnMetadata.nestedType) {
21
- plain[property] = spannerObjectToInstanceWithOptions(spannerObject, columnMetadata.nestedType, {
22
- columnNamePrefix: `${columnName}_`,
23
- nullifyInstance: columnMetadata.nullifyNested,
24
- });
25
- }
26
- else if (columnMetadata.isJson) {
27
- plain[property] = spannerObject[columnName];
28
- }
29
- else if (Array.isArray(spannerObject[columnName])) {
30
- plain[property] = spannerObject[columnName].map((v) => spannerValueToJavaScript(v, columnMetadata));
31
- }
32
- else {
33
- plain[property] = spannerValueToJavaScript(spannerObject[columnName], columnMetadata);
34
- }
35
- if (plain[property] != null) {
36
- hasAtLeastOneNonNullValue = true;
37
- }
38
- });
39
- return hasAtLeastOneNonNullValue || !options.nullifyInstance
40
- ? plainToInstance(type, plain)
41
- : null;
14
+ const plain = Object.fromEntries(Object.entries(columnsMetadata).map(([property, columnMetadata]) => {
15
+ const columnName = columnMetadata.name;
16
+ const value = spannerObject[columnName];
17
+ if (columnMetadata.isJson) {
18
+ return [property, value];
19
+ }
20
+ if (Array.isArray(value)) {
21
+ const values = value.map((v) => spannerValueToJavaScript(v, columnMetadata));
22
+ return [property, values];
23
+ }
24
+ return [property, spannerValueToJavaScript(value, columnMetadata)];
25
+ }));
26
+ return plainToInstance(type, plain);
42
27
  }
43
28
  /**
44
29
  * Converts a scalar value returned by Spanner to a regular JavaScript value.
@@ -54,17 +39,15 @@ function spannerValueToJavaScript(value, columnMetadata) {
54
39
  // Floats are always safe and can always be unwrapped.
55
40
  return value.valueOf();
56
41
  }
57
- else if (value instanceof Int) {
42
+ if (value instanceof Int) {
58
43
  if (columnMetadata.isBigInt) {
59
44
  return BigInt(value.value);
60
45
  }
61
- else {
62
- // This unwraps the string to an integer number, but might throw if the integer is above what can be represented
63
- // safely as a float.
64
- return value.valueOf();
65
- }
46
+ // This unwraps the string to an integer number, but might throw if the integer is above what can be represented
47
+ // safely as a float.
48
+ return value.valueOf();
66
49
  }
67
- else if (value instanceof Date &&
50
+ if (value instanceof Date &&
68
51
  // `PreciseDate` extends `Date`. This was previously used to handle conflicting versions of `PreciseDate`.
69
52
  value.constructor.name === PreciseDate.name &&
70
53
  !columnMetadata.isPreciseDate) {
@@ -74,17 +57,6 @@ function spannerValueToJavaScript(value, columnMetadata) {
74
57
  }
75
58
  return value;
76
59
  }
77
- /**
78
- * Creates a typed class instance from an object returned by the Spanner API.
79
- *
80
- * @param spannerObject The object returned by Spanner that should be converted back to a class instance.
81
- * @param type The class for the object.
82
- * @returns The created object.
83
- */
84
- export function spannerObjectToInstance(spannerObject, type) {
85
- // This is okay as `null` can only be returned when the internal option `nullifyInstance` is set.
86
- return spannerObjectToInstanceWithOptions(spannerObject, type);
87
- }
88
60
  /**
89
61
  * Converts a value such that it is safe to pass to the Spanner client.
90
62
  * Numeric values and arrays of numeric values are wrapped using Spanner classes.
@@ -103,50 +75,19 @@ function makeSpannerValue(value, metadata) {
103
75
  ? value.map((v) => new Int(v.toString()))
104
76
  : new Int(value.toString());
105
77
  }
106
- else if (metadata.isJson) {
78
+ if (metadata.isJson) {
107
79
  return JSON.stringify(value);
108
80
  }
109
- else if (typeof value === 'number') {
81
+ if (typeof value === 'number') {
110
82
  return new Float(value);
111
83
  }
112
- else if (Array.isArray(value) &&
84
+ if (Array.isArray(value) &&
113
85
  value.length > 0 &&
114
86
  typeof value[0] === 'number') {
115
87
  return value.map((v) => new Float(v));
116
88
  }
117
89
  return value;
118
90
  }
119
- /**
120
- * Converts a class object to a plain JavaScript object that can be passed to the Spanner API.
121
- *
122
- * @param instance The object to convert to a Spanner object.
123
- * @param type The type of the object to convert.
124
- * @param columnNamePrefix The prefix to add to column names. Used for recursion and should not be set directly.
125
- * @returns A generic JavaScript object that can be passed to the Spanner API.
126
- */
127
- export function instanceToSpannerObjectInternal(instance, type, columnNamePrefix = '') {
128
- const columnsMetadata = getSpannerColumnsMetadata(type);
129
- let spannerObject = {};
130
- Object.entries(columnsMetadata).forEach(([property, columnMetadata]) => {
131
- const columnName = `${columnNamePrefix}${columnMetadata.name}`;
132
- // When the current property value is `undefined`, column(s) values should not be set.
133
- if (instance !== null && instance[property] === undefined) {
134
- return;
135
- }
136
- // When the whole instance is `null`, all its child properties should be set to null.
137
- const propertyValue = instance === null ? null : instance[property];
138
- if (columnMetadata.nestedType) {
139
- spannerObject = {
140
- ...spannerObject,
141
- ...instanceToSpannerObjectInternal(propertyValue, columnMetadata.nestedType, `${columnName}_`),
142
- };
143
- }
144
- else {
145
- spannerObject[columnName] = makeSpannerValue(propertyValue, columnMetadata);
146
- }
147
- });
148
- return spannerObject;
149
- }
150
91
  /**
151
92
  * Converts a class object to a plain JavaScript object that can be passed to the Spanner API.
152
93
  *
@@ -155,11 +96,18 @@ export function instanceToSpannerObjectInternal(instance, type, columnNamePrefix
155
96
  * @returns A generic JavaScript object that can be passed to the Spanner API.
156
97
  */
157
98
  export function instanceToSpannerObject(instance, type) {
158
- return instanceToSpannerObjectInternal(instance, type);
99
+ const columnsMetadata = getSpannerColumnsMetadata(type);
100
+ return Object.fromEntries(Object.entries(columnsMetadata)
101
+ .map(([p, m]) => [m, instance[p]])
102
+ // When the current property value is `undefined`, the column value should not be set.
103
+ .filter(([, v]) => v !== undefined)
104
+ .map(([metadata, value]) => [
105
+ metadata.name,
106
+ makeSpannerValue(value, metadata),
107
+ ]));
159
108
  }
160
109
  /**
161
- * Copies an instance, recursively setting all columns that are not defined in the instance to `null`.
162
- * Columns with the {@link SpannerColumnMetadata.nullifyNested} option set to `true` are also set to `null`.
110
+ * Copies an instance, setting all columns that are not defined in the instance to `null`.
163
111
  *
164
112
  * @param instance The instance to copy.
165
113
  * @param type The type of the instance.
@@ -167,30 +115,14 @@ export function instanceToSpannerObject(instance, type) {
167
115
  */
168
116
  export function copyInstanceWithMissingColumnsToNull(instance, type) {
169
117
  const columnsMetadata = getSpannerColumnsMetadata(type);
170
- const plain = {};
171
- Object.entries(columnsMetadata).forEach(([property, columnMetadata]) => {
172
- const instanceValue = instance == null ? null : instance[property];
173
- if (columnMetadata.nestedType) {
174
- if (columnMetadata.nullifyNested && instanceValue == null) {
175
- plain[property] = null;
176
- }
177
- else {
178
- plain[property] = copyInstanceWithMissingColumnsToNull(instanceValue, columnMetadata.nestedType);
179
- }
180
- }
181
- else if (instanceValue !== undefined) {
182
- plain[property] = instanceValue;
183
- }
184
- else {
185
- plain[property] = null;
186
- }
187
- });
118
+ const plain = Object.fromEntries(Object.keys(columnsMetadata).map((property) => {
119
+ const value = instance[property];
120
+ return [property, value === undefined ? null : value];
121
+ }));
188
122
  return plainToInstance(type, plain);
189
123
  }
190
124
  /**
191
- * Recursively updates an instance with the values from the update.
192
- * Updates are applied "column-wise", which means that recursion stops at properties decorated as columns.
193
- * For example, JSON values are not affected.
125
+ * Updates an instance with the values from the update.
194
126
  *
195
127
  * @param instance The instance to update. It should be a full, typed, instance, unless `type` is passed as well.
196
128
  * @param update The update to apply to the instance.
@@ -200,25 +132,13 @@ export function copyInstanceWithMissingColumnsToNull(instance, type) {
200
132
  export function updateInstanceByColumn(instance, update, type) {
201
133
  type ??= instance.constructor;
202
134
  const columnsMetadata = getSpannerColumnsMetadata(type);
203
- const plain = {};
204
- Object.entries(columnsMetadata).forEach(([property, columnMetadata]) => {
205
- const instanceValue = instance == null ? null : instance[property];
206
- const updateValue = update == null ? update : update[property];
207
- if (updateValue === undefined) {
208
- plain[property] = instanceValue;
209
- return;
210
- }
211
- if (columnMetadata.nestedType) {
212
- if (columnMetadata.nullifyNested && updateValue === null) {
213
- plain[property] = null;
214
- }
215
- else {
216
- plain[property] = updateInstanceByColumn(instanceValue, updateValue, columnMetadata.nestedType);
217
- }
218
- }
219
- else if (updateValue !== undefined) {
220
- plain[property] = updateValue;
221
- }
222
- });
135
+ const plain = Object.fromEntries(Object.keys(columnsMetadata).map((property) => {
136
+ const instanceValue = instance[property];
137
+ const updateValue = update[property];
138
+ return [
139
+ property,
140
+ updateValue === undefined ? instanceValue : updateValue,
141
+ ];
142
+ }));
223
143
  return plainToInstance(type, plain);
224
144
  }
@@ -3,7 +3,7 @@ import { type Type as ParamType } from '@google-cloud/spanner/build/src/codec.js
3
3
  import type { TimestampBounds } from '@google-cloud/spanner/build/src/transaction.js';
4
4
  import { type Type } from '@nestjs/common';
5
5
  import { SpannerTableCache } from './table-cache.js';
6
- import type { RecursivePartialEntity, SpannerReadOnlyTransactionOption, SpannerReadWriteTransactionOption } from './types.js';
6
+ import type { SpannerReadOnlyTransactionOption, SpannerReadWriteTransactionOption } from './types.js';
7
7
  /**
8
8
  * Any Spanner transaction that can be used for reading.
9
9
  */
@@ -19,7 +19,7 @@ export type SpannerKey = (string | null)[];
19
19
  /**
20
20
  * Options for {@link SpannerEntityManager.snapshot}.
21
21
  */
22
- type SnapshotOptions = {
22
+ type SnapshotOptions = SpannerReadOnlyTransactionOption | {
23
23
  /**
24
24
  * Sets how the timestamp will be selected when creating the snapshot.
25
25
  */
@@ -29,6 +29,10 @@ type SnapshotOptions = {
29
29
  * A function that can be passed to the {@link SpannerEntityManager.snapshot} method.
30
30
  */
31
31
  export type SnapshotFunction<T> = (snapshot: SpannerReadOnlyTransaction) => Promise<T>;
32
+ /**
33
+ * A function that can be passed to the {@link SpannerEntityManager.transaction} method.
34
+ */
35
+ export type SpannerTransactionFunction<T> = (transaction: SpannerReadWriteTransaction) => Promise<T>;
32
36
  /**
33
37
  * A SQL statement run using {@link SpannerEntityManager.query}.
34
38
  */
@@ -98,7 +102,7 @@ export declare class SpannerEntityManager {
98
102
  * constructor).
99
103
  * @returns The primary key of the entity.
100
104
  */
101
- getPrimaryKey<T>(entity: T | RecursivePartialEntity<T>, entityType?: Type<T>): SpannerKey;
105
+ getPrimaryKey<T>(entity: T | Partial<T>, entityType?: Type<T>): SpannerKey;
102
106
  /**
103
107
  * Returns the primary key of the given Spanner object, assumed to be an entity of the given type.
104
108
  *
@@ -189,7 +193,17 @@ export declare class SpannerEntityManager {
189
193
  * @param runFn The function to run in the transaction.
190
194
  * @returns The return value of the function.
191
195
  */
192
- transaction<T>(runFn: (transaction: SpannerReadWriteTransaction) => Promise<T>): Promise<T>;
196
+ transaction<T>(runFn: SpannerTransactionFunction<T>): Promise<T>;
197
+ /**
198
+ * Runs the provided function in a (read write) {@link SpannerReadWriteTransaction}.
199
+ * The function itself should not commit or rollback the transaction.
200
+ * If the function throws an error, the transaction will be rolled back.
201
+ *
202
+ * @param options The options to use when creating the transaction.
203
+ * @param runFn The function to run in the transaction.
204
+ * @returns The return value of the function.
205
+ */
206
+ transaction<T>(options: SpannerReadWriteTransactionOption, runFn: SpannerTransactionFunction<T>): Promise<T>;
193
207
  /**
194
208
  * Runs the provided function in a {@link SpannerReadOnlyTransaction}.
195
209
  * The snapshot will be automatically released when the function returns.
@@ -288,7 +302,7 @@ export declare class SpannerEntityManager {
288
302
  * @param options Options for the operation.
289
303
  * @returns The updated entity.
290
304
  */
291
- update<T>(entityType: Type<T>, update: RecursivePartialEntity<T>, options?: SpannerReadWriteTransactionOption & Pick<FindOptions, 'includeSoftDeletes'> & {
305
+ update<T>(entityType: Type<T>, update: Partial<T>, options?: SpannerReadWriteTransactionOption & Pick<FindOptions, 'includeSoftDeletes'> & {
292
306
  /**
293
307
  * A function that will be called with the entity before it is updated.
294
308
  * This function can throw an error to prevent the update.
@@ -316,23 +330,5 @@ export declare class SpannerEntityManager {
316
330
  */
317
331
  validateFn?: (entity: T) => void;
318
332
  }): Promise<T>;
319
- /**
320
- * Runs the given "read-write" function on a transaction. If a transaction is not passed, a new
321
- * {@link SpannerReadWriteTransaction} is created instead.
322
- *
323
- * @param transaction The transaction to use. If `undefined`, a new transaction is created.
324
- * @param fn The function to run on the transaction.
325
- * @returns The result of the function.
326
- */
327
- runInExistingOrNewTransaction<T>(transaction: SpannerReadWriteTransaction | undefined, fn: (transaction: SpannerReadWriteTransaction) => Promise<T>): Promise<T>;
328
- /**
329
- * Runs the given "read-only" function on a transaction. If a transaction is not passed, a new
330
- * {@link SpannerReadOnlyTransaction} is created instead.
331
- *
332
- * @param transaction The transaction to use. If `undefined`, a new {@link SpannerReadOnlyTransaction} is created.
333
- * @param fn The function to run on the transaction.
334
- * @returns The result of the function.
335
- */
336
- runInExistingOrNewReadOnlyTransaction<T>(transaction: SpannerReadOnlyTransaction | undefined, fn: SnapshotFunction<T>): Promise<T>;
337
333
  }
338
334
  export {};
@@ -133,7 +133,7 @@ let SpannerEntityManager = class SpannerEntityManager {
133
133
  }
134
134
  const { tableName, columns: allColumns, primaryKeyColumns, softDeleteColumn, } = this.tableCache.getMetadata(entityType);
135
135
  const columns = options.columns ?? (options.index ? primaryKeyColumns : allColumns);
136
- return await this.runInExistingOrNewReadOnlyTransaction(options.transaction, async (transaction) => {
136
+ return await this.snapshot({ transaction: options.transaction }, async (transaction) => {
137
137
  const [rows] = await transaction.read(tableName, {
138
138
  keys: [key],
139
139
  columns,
@@ -201,16 +201,15 @@ let SpannerEntityManager = class SpannerEntityManager {
201
201
  }
202
202
  return entity;
203
203
  }
204
- /**
205
- * Runs the provided function in a (read write) {@link SpannerReadWriteTransaction}.
206
- * The function itself should not commit or rollback the transaction.
207
- * If the function throws an error, the transaction will be rolled back.
208
- *
209
- * @param runFn The function to run in the transaction.
210
- * @returns The return value of the function.
211
- */
212
- async transaction(runFn) {
204
+ async transaction(optionsOrRunFn, runFn) {
205
+ const options = runFn
206
+ ? optionsOrRunFn
207
+ : {};
208
+ runFn ??= optionsOrRunFn;
213
209
  try {
210
+ if (options.transaction) {
211
+ return await runFn(options.transaction);
212
+ }
214
213
  return await this.database.runTransactionAsync(async (transaction) => {
215
214
  try {
216
215
  const result = await runFn(transaction);
@@ -238,14 +237,19 @@ let SpannerEntityManager = class SpannerEntityManager {
238
237
  }
239
238
  }
240
239
  async snapshot(optionsOrRunFn, runFn) {
241
- const snapshotFn = typeof optionsOrRunFn === 'function'
242
- ? optionsOrRunFn
243
- : runFn;
244
- const options = typeof optionsOrRunFn === 'object' ? optionsOrRunFn : {};
240
+ const options = runFn ? optionsOrRunFn : {};
241
+ runFn ??= optionsOrRunFn;
245
242
  let snapshot;
246
243
  try {
247
- [snapshot] = await this.database.getSnapshot(options.timestampBounds);
248
- return await snapshotFn(snapshot);
244
+ let transaction;
245
+ if ('transaction' in options) {
246
+ transaction = options.transaction;
247
+ }
248
+ if (!transaction) {
249
+ [snapshot] = await this.database.getSnapshot('timestampBounds' in options ? options.timestampBounds : undefined);
250
+ transaction = snapshot;
251
+ }
252
+ return await runFn(transaction);
249
253
  }
250
254
  catch (error) {
251
255
  throw convertSpannerToEntityError(error) ?? error;
@@ -262,7 +266,7 @@ let SpannerEntityManager = class SpannerEntityManager {
262
266
  */
263
267
  async clear(entityType, options = {}) {
264
268
  const { quotedTableName } = this.tableCache.getMetadata(entityType);
265
- await this.runInExistingOrNewTransaction(options.transaction, (transaction) => transaction.runUpdate({
269
+ await this.transaction(options, (transaction) => transaction.runUpdate({
266
270
  sql: `DELETE FROM ${quotedTableName} WHERE TRUE`,
267
271
  }));
268
272
  }
@@ -272,7 +276,7 @@ let SpannerEntityManager = class SpannerEntityManager {
272
276
  : {};
273
277
  const sqlStatement = statement ?? optionsOrStatement;
274
278
  const { entityType } = options;
275
- return await this.runInExistingOrNewReadOnlyTransaction(options.transaction, async (transaction) => {
279
+ return await this.snapshot({ transaction: options.transaction }, async (transaction) => {
276
280
  const [rows] = await transaction.run({
277
281
  ...sqlStatement,
278
282
  json: true,
@@ -349,7 +353,7 @@ let SpannerEntityManager = class SpannerEntityManager {
349
353
  */
350
354
  async insert(entity, options = {}) {
351
355
  const objs = this.entitiesToSpannerObjects(entity);
352
- await this.runInExistingOrNewTransaction(options.transaction, async (transaction) => Object.entries(objs).forEach(([tableName, objs]) => transaction.insert(tableName, objs)));
356
+ await this.transaction(options, async (transaction) => Object.entries(objs).forEach(([tableName, objs]) => transaction.insert(tableName, objs)));
353
357
  }
354
358
  /**
355
359
  * Replaces the given entities in the database.
@@ -364,7 +368,7 @@ let SpannerEntityManager = class SpannerEntityManager {
364
368
  */
365
369
  async replace(entity, options = {}) {
366
370
  const objs = this.entitiesToSpannerObjects(entity);
367
- await this.runInExistingOrNewTransaction(options.transaction, async (transaction) => Object.entries(objs).forEach(([tableName, objs]) => transaction.replace(tableName, objs)));
371
+ await this.transaction(options, async (transaction) => Object.entries(objs).forEach(([tableName, objs]) => transaction.replace(tableName, objs)));
368
372
  }
369
373
  /**
370
374
  * Updates the given entity in the database.
@@ -386,7 +390,7 @@ let SpannerEntityManager = class SpannerEntityManager {
386
390
  async update(entityType, update, options = {}) {
387
391
  const primaryKey = this.getPrimaryKey(update, entityType);
388
392
  const { tableName } = this.tableCache.getMetadata(entityType);
389
- return await this.runInExistingOrNewTransaction(options.transaction, async (transaction) => {
393
+ return await this.transaction({ transaction: options.transaction }, async (transaction) => {
390
394
  const existingEntity = await this.findOneByKey(entityType, primaryKey, {
391
395
  transaction,
392
396
  includeSoftDeletes: options.includeSoftDeletes,
@@ -427,7 +431,7 @@ let SpannerEntityManager = class SpannerEntityManager {
427
431
  key = [key];
428
432
  }
429
433
  const { tableName } = this.tableCache.getMetadata(entityType);
430
- return await this.runInExistingOrNewTransaction(options.transaction, async (transaction) => {
434
+ return await this.transaction({ transaction: options.transaction }, async (transaction) => {
431
435
  const existingEntity = await this.findOneByKeyOrFail(entityType, key, {
432
436
  transaction,
433
437
  includeSoftDeletes: options.includeSoftDeletes,
@@ -439,44 +443,6 @@ let SpannerEntityManager = class SpannerEntityManager {
439
443
  return existingEntity;
440
444
  });
441
445
  }
442
- /**
443
- * Runs the given "read-write" function on a transaction. If a transaction is not passed, a new
444
- * {@link SpannerReadWriteTransaction} is created instead.
445
- *
446
- * @param transaction The transaction to use. If `undefined`, a new transaction is created.
447
- * @param fn The function to run on the transaction.
448
- * @returns The result of the function.
449
- */
450
- async runInExistingOrNewTransaction(transaction, fn) {
451
- if (transaction) {
452
- try {
453
- return await fn(transaction);
454
- }
455
- catch (error) {
456
- throw convertSpannerToEntityError(error) ?? error;
457
- }
458
- }
459
- return this.transaction(fn);
460
- }
461
- /**
462
- * Runs the given "read-only" function on a transaction. If a transaction is not passed, a new
463
- * {@link SpannerReadOnlyTransaction} is created instead.
464
- *
465
- * @param transaction The transaction to use. If `undefined`, a new {@link SpannerReadOnlyTransaction} is created.
466
- * @param fn The function to run on the transaction.
467
- * @returns The result of the function.
468
- */
469
- async runInExistingOrNewReadOnlyTransaction(transaction, fn) {
470
- if (transaction) {
471
- try {
472
- return await fn(transaction);
473
- }
474
- catch (error) {
475
- throw convertSpannerToEntityError(error) ?? error;
476
- }
477
- }
478
- return this.snapshot(fn);
479
- }
480
446
  };
481
447
  SpannerEntityManager = __decorate([
482
448
  Injectable(),
@@ -28,9 +28,6 @@ export class SpannerTableCache {
28
28
  if (softDeleteColumns.length > 1) {
29
29
  throw new InvalidEntityDefinitionError(entityType, `Only one column can be marked as soft delete.`);
30
30
  }
31
- if (softDeleteColumns[0]?.nestedType) {
32
- throw new InvalidEntityDefinitionError(entityType, `Soft delete columns cannot be nested.`);
33
- }
34
31
  const softDeleteColumn = softDeleteColumns[0]?.name ?? null;
35
32
  const columns = getSpannerColumns(entityType);
36
33
  const quotedColumns = columns.map((c) => `\`${c}\``).join(', ');
@@ -1,5 +1,10 @@
1
- import type { NestJsModuleOverrider } from '@causa/runtime/nestjs/testing';
1
+ import type { Fixture, NestJsModuleOverrider } from '@causa/runtime/nestjs/testing';
2
2
  import { Database, Instance, Spanner } from '@google-cloud/spanner';
3
+ import type { Type } from '@nestjs/common';
4
+ /**
5
+ * Parameters for creating a new test database.
6
+ */
7
+ type CreateDatabaseParameters = Pick<SpannerFixture, 'name' | 'sourceDatabaseName' | 'spanner' | 'instance'>;
3
8
  /**
4
9
  * Creates a new database.
5
10
  * This will destroy the existing database if it exists.
@@ -7,32 +12,46 @@ import { Database, Instance, Spanner } from '@google-cloud/spanner';
7
12
  * @param options Options when creating the database.
8
13
  * @returns The database object.
9
14
  */
10
- export declare function createDatabase(options?: {
15
+ export declare function createDatabase(options?: Partial<CreateDatabaseParameters>): Promise<Database>;
16
+ /**
17
+ * A {@link Fixture} that creates a temporary Spanner database and injects it into the NestJS application.
18
+ * The specified tables will be cleared after each test.
19
+ */
20
+ export declare class SpannerFixture implements Fixture {
11
21
  /**
12
- * The name of the database to create. If not specified, a random name will be generated.
22
+ * The name of the temporary database.
13
23
  */
14
- name?: string;
24
+ readonly name: string;
15
25
  /**
16
26
  * If `sourceDatabaseName` is provided, its DDL will be copied into the new database, otherwise it will try to copy
17
27
  * the DDL from `process.env.SPANNER_DATABASE`.
18
28
  * If `null`, no schema will be set on the created database.
19
29
  */
20
- sourceDatabaseName?: string | null;
30
+ readonly sourceDatabaseName: string | null;
21
31
  /**
22
- * The instance on which the database should be created.
32
+ * The Spanner client to use for tests.
33
+ */
34
+ readonly spanner: Spanner;
35
+ /**
36
+ * The Spanner instance to use for tests.
23
37
  * By default, a new Spanner client will be created using the `SPANNER_INSTANCE` environment variable.
24
38
  */
25
- instance?: Instance;
39
+ readonly instance: Instance;
26
40
  /**
27
- * The Spanner client to use to create the database.
28
- * If `instance` is provided, this will be ignored.
41
+ * Types of entities (Spanner tables) to clear.
29
42
  */
30
- spanner?: Spanner;
31
- }): Promise<Database>;
32
- /**
33
- * Returns a {@link NestJsModuleOverrider} that overrides the {@link Database} provider with the provided database.
34
- *
35
- * @param database The temporary database to use.
36
- * @returns The {@link NestJsModuleOverrider} to override the {@link Database} provider.
37
- */
38
- export declare function overrideDatabase(database: Database): NestJsModuleOverrider;
43
+ readonly types: Type[];
44
+ /**
45
+ * The {@link SpannerEntityManager} used to clear tables.
46
+ */
47
+ private entityManager;
48
+ /**
49
+ * The temporary test database created by this fixture.
50
+ */
51
+ private database;
52
+ constructor(options?: Partial<CreateDatabaseParameters> & Partial<Pick<SpannerFixture, 'types'>>);
53
+ init(): Promise<NestJsModuleOverrider>;
54
+ clear(): Promise<void>;
55
+ delete(): Promise<void>;
56
+ }
57
+ export {};