Skip to content
This repository was archived by the owner on Oct 11, 2023. It is now read-only.

Commit 09de71a

Browse files
committed
Add transaction success hook
Don't log stats for virtual collections
1 parent ae4e5b7 commit 09de71a

File tree

5 files changed

+93
-42
lines changed

5 files changed

+93
-42
lines changed

package-lock.json

Lines changed: 24 additions & 23 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "strapi-connector-firestore",
3-
"version": "3.0.0-alpha.40",
3+
"version": "3.0.0-alpha.41",
44
"description": "Strapi database connector for Firestore database on Google Cloud Platform.",
55
"keywords": [
66
"firestore",
@@ -27,7 +27,7 @@
2727
"test": "npm run build:test && npm test --prefix test"
2828
},
2929
"dependencies": {
30-
"@google-cloud/firestore": "^4.15.0",
30+
"@google-cloud/firestore": "^4.15.1",
3131
"@types/pino": "^4.16.1",
3232
"fs-extra": "^9.1.0",
3333
"lodash": "^4.17.21",
@@ -38,8 +38,8 @@
3838
"@tsconfig/node12": "^1.0.9",
3939
"@types/fs-extra": "^9.0.12",
4040
"@types/lodash": "^4.14.172",
41-
"@types/node": "^12.20.19",
41+
"@types/node": "^12.20.24",
4242
"rimraf": "^3.0.2",
43-
"typescript": "^4.4.2"
43+
"typescript": "^4.4.3"
4444
}
4545
}

src/queries.ts

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as _ from 'lodash';
2-
import { PickReferenceKeys, PopulatedByKeys, populateDoc, populateSnapshots } from './populate';
2+
import { PickReferenceKeys, populateDoc, populateSnapshots } from './populate';
33
import { StatusError } from './utils/status-error';
4-
import type { StrapiQuery, StrapiContext } from './types';
4+
import type { StrapiQuery, StrapiContext, TransactionSuccessHook } from './types';
55
import type { Queryable } from './db/collection';
66
import type { Transaction } from './db/transaction';
77
import type { Reference, Snapshot } from './db/reference';
@@ -52,20 +52,25 @@ export function queries<T extends object>({ model, strapi }: StrapiContext<T>):
5252
? model.db.doc(model.getPK(values))
5353
: model.db.doc();
5454

55-
return await model.runTransaction(async trans => {
55+
const { result, onSuccess } = await model.runTransaction(async trans => {
5656
// Create while coercing data and updating relations
5757
const data = await trans.create(ref, values);
58-
await model.options.onChange(undefined, data, trans);
58+
const onSuccess = await model.options.onChange(undefined, data, trans);
5959

6060
// Populate relations
61-
return await populateDoc(model, ref, data, populate, trans);
61+
const result = await populateDoc(model, ref, data, populate, trans);
62+
return { result, onSuccess };
6263
});
64+
65+
// Run the success hook
66+
await runOnSuccess(onSuccess, result as any);
67+
return result;
6368
};
6469

6570
const update: FirestoreConnectorQueries<T>['update'] = async (params, values, populate = (model.defaultPopulate as any)) => {
6671
log('update', { params, populate });
6772

68-
return await model.runTransaction(async trans => {
73+
const { result, onSuccess } = await model.runTransaction(async trans => {
6974
const [snap] = await buildAndFetchQuery({
7075
model,
7176
params: { ...params, _limit: 1 },
@@ -81,17 +86,22 @@ export function queries<T extends object>({ model, strapi }: StrapiContext<T>):
8186
...snap.data(),
8287
...await trans.update(snap.ref, values),
8388
};
84-
await model.options.onChange(prevData, data, trans);
89+
const onSuccess = await model.options.onChange(prevData, data, trans);
8590

8691
// Populate relations
87-
return await populateDoc(model, snap.ref, data, populate, trans);
92+
const result = await populateDoc(model, snap.ref, data, populate, trans);
93+
return { result, onSuccess };
8894
});
95+
96+
// Run the success hook
97+
await runOnSuccess(onSuccess, result as any);
98+
return result;
8999
};
90100

91101
const deleteMany: FirestoreConnectorQueries<T>['delete'] = async (params, populate = (model.defaultPopulate as any)) => {
92102
log('delete', { params, populate });
93103

94-
return await model.runTransaction(async trans => {
104+
const results = await model.runTransaction(async trans => {
95105
const query = buildQuery(model.db, { model, params });
96106
const snaps = await fetchQuery(query, trans);
97107

@@ -105,9 +115,25 @@ export function queries<T extends object>({ model, strapi }: StrapiContext<T>):
105115
);
106116
}
107117
});
118+
119+
// Run the success hook
120+
if (Array.isArray(results)) {
121+
await Promise.all(results.map(r => r && runOnSuccess(r.onSuccess, r.result as any)));
122+
} else {
123+
if (results) {
124+
await runOnSuccess(results.onSuccess, results.result as any);
125+
}
126+
}
127+
128+
// Return the results
129+
if (Array.isArray(results)) {
130+
return results.map(r => r && r.result);
131+
} else {
132+
return results && results.result;
133+
}
108134
};
109135

110-
async function deleteOne<K extends PickReferenceKeys<T>>(snap: Snapshot<T>, populate: K[], trans: Transaction): Promise<PopulatedByKeys<T, K> | null> {
136+
async function deleteOne<K extends PickReferenceKeys<T>>(snap: Snapshot<T>, populate: K[], trans: Transaction) {
111137
const prevData = snap.data();
112138
if (!prevData) {
113139
// Delete API returns `null` rather than throwing an error for non-existent documents
@@ -116,10 +142,11 @@ export function queries<T extends object>({ model, strapi }: StrapiContext<T>):
116142

117143
// Delete while updating relations
118144
await trans.delete(snap.ref);
119-
await model.options.onChange(prevData, undefined, trans);
145+
const onSuccess = await model.options.onChange(prevData, undefined, trans);
120146

121147
// Populate relations
122-
return await populateDoc(model, snap.ref, prevData, populate, trans);
148+
const result = await populateDoc(model, snap.ref, prevData, populate, trans);
149+
return { result, onSuccess };
123150
};
124151

125152
const search: FirestoreConnectorQueries<T>['search'] = async (params, populate = (model.defaultPopulate as any)) => {
@@ -213,3 +240,13 @@ async function fetchQuery<T extends object>(queryOrRefs: Queryable<T> | Referenc
213240
return result.docs;
214241
}
215242
}
243+
244+
async function runOnSuccess<T extends object>(onSuccess: void | TransactionSuccessHook<T>, result: T | undefined) {
245+
if (typeof onSuccess === 'function') {
246+
try {
247+
await onSuccess(result);
248+
} catch (err) {
249+
strapi.log.warn(`Transaction onSuccess hook threw an error: ${(err as any).message}`, err);
250+
}
251+
}
252+
}

src/types.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,15 +200,27 @@ export interface ModelOptions<T extends object, R extends DocumentData = Documen
200200
* This hook is only called via the query interface (i.e. `strapi.query(...).create(...)` etc). Any operations performed directly using the model's
201201
* `runTransaction(...)` interface will bypass this hook.
202202
*
203+
* Note: This hook may be called multiple times for a single transaction, if the transaction is retried. The result
204+
* of transaction may not necessarily be committed to the database, depending on the success of the transaction.
205+
* Return a function from this hook, for any code that should be executed only once upon success of the transaction.
206+
*
203207
* @param previousData The previous value of the entity, or `undefined` if the entity is being created.
204208
* @param newData The new value of the entity, or `undefined` if the entity is being deleted.
205209
* @param transaction The transaction being run. Can be used to fetch additional entities, or make additional
206210
* atomic writes.
207-
* @returns If a promise is returned, if will be awaited before completing the transaction.
211+
* @returns The hook may optionally return a function, or a Promise that optionally resolves to a function.
212+
* If a promise is returned, it will be awaited before completing the transaction.
213+
* If it resolves to a function, this function will be called only once, after the transaction has been
214+
* successfully committed.
215+
* Any errors thrown by this function will be caught and ignored.
216+
*
208217
*/
209-
onChange?: (previousData: T | undefined, newData: T | undefined, transaction: Transaction) => void | Promise<void>
218+
onChange?: TransactionOnChangeHook<T>
210219
}
211220

221+
export type TransactionOnChangeHook<T> = (previousData: T | undefined, newData: T | undefined, transaction: Transaction) => (void | TransactionSuccessHook<T>) | PromiseLike<void | TransactionSuccessHook<T>>
222+
export type TransactionSuccessHook<T> = (result: T | undefined) => (void | PromiseLike<void>)
223+
212224
export interface DataSource<T extends object> {
213225
/**
214226
* Indicates whether entries in this data source will have persistent and stable IDs

src/utils/transaction-runner.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ export function makeTransactionRunner<T extends object>(firestore: Firestore, op
2323
// The only scenario where a virtual collection may want to perform a Firestore write is if it has
2424
// a dominant relation to a non-virtual collection. However, because of the (potentially) transient nature of
2525
// references to virtual collections, dominant relations to a virtual collection are not supported.
26-
const trans = new ReadOnlyTransaction(firestore, logTransactionStats);
26+
// Don't log stats for virtual collections
27+
const trans = new ReadOnlyTransaction(firestore, logTransactionStats && !isVirtual);
2728
const result = await fn(trans);
2829
await trans.commit();
2930
return result;

0 commit comments

Comments
 (0)