Skip to content

Add Transaction #3153

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 95 commits into from
Jun 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
95 commits
Select commit Hold shift + click to select a range
da82b5d
Add Firestore, initializeFirestore, getFirestore to Firestore lite
schmidt-sebastian May 26, 2020
30bfc3f
Review
schmidt-sebastian May 27, 2020
f49096f
add one more tests
schmidt-sebastian May 27, 2020
da3403f
Fix lint
schmidt-sebastian May 27, 2020
bd3596e
Add lite to build
schmidt-sebastian May 27, 2020
3aac6f4
Add private
schmidt-sebastian May 27, 2020
6f05e2f
Fix Path
schmidt-sebastian May 27, 2020
634145a
Remove unused import
schmidt-sebastian May 27, 2020
8187cb7
Update package.json
schmidt-sebastian May 27, 2020
5760b02
Simplify
schmidt-sebastian May 27, 2020
4ba0f4e
Merge branch 'mrschmidt/initializefirestore' of github.com:firebase/f…
schmidt-sebastian May 27, 2020
e40faf4
Merge branch 'master' into mrschmidt/initializefirestore
schmidt-sebastian May 27, 2020
f243a6c
Add DocumentReference
schmidt-sebastian May 27, 2020
d48405b
Feedback
schmidt-sebastian May 28, 2020
804fe94
Typo
schmidt-sebastian May 28, 2020
d244bcd
Lint fix
schmidt-sebastian May 28, 2020
bc342a3
Update integration.test.ts
schmidt-sebastian May 28, 2020
5297d47
Update rollup.config.lite.js
schmidt-sebastian May 28, 2020
867d9c0
One more test
schmidt-sebastian May 28, 2020
084fdcf
Update reference.ts
schmidt-sebastian May 28, 2020
ca44064
Add CollectionReference
schmidt-sebastian May 28, 2020
920c0e2
Merge branch 'master' into mrschmidt/initializefirestore
schmidt-sebastian May 28, 2020
d5745f0
Merge
schmidt-sebastian May 28, 2020
b636d27
Merge
schmidt-sebastian May 28, 2020
31ba3ba
Add DocumentSnapshot
schmidt-sebastian May 28, 2020
505792f
Add exports
schmidt-sebastian May 28, 2020
3750a00
Add getDoc()
schmidt-sebastian May 28, 2020
5d3e2aa
Fix build
schmidt-sebastian May 28, 2020
58a84d1
More integration test fixes
schmidt-sebastian May 29, 2020
6f91171
Fix Integration tests
schmidt-sebastian May 29, 2020
c63977f
Add DocumentReference
schmidt-sebastian May 29, 2020
6352026
Add deleteDoc()
schmidt-sebastian May 29, 2020
be02305
Cleanup
schmidt-sebastian May 29, 2020
518631d
Merge
schmidt-sebastian May 29, 2020
6318fa4
Update datastore.ts
schmidt-sebastian May 29, 2020
a4ff5fd
Add setDoc()
schmidt-sebastian May 29, 2020
72b0b8b
Add setDoc()
schmidt-sebastian May 29, 2020
f3a4316
Add addDoc()
schmidt-sebastian May 30, 2020
efa58c4
Add converter
schmidt-sebastian May 30, 2020
7649b67
Throw when user data parsing fails
schmidt-sebastian May 30, 2020
3aa5d2f
Merge
schmidt-sebastian May 30, 2020
66d72d0
Throw when user data parsing fails
schmidt-sebastian May 30, 2020
e5b5d8f
Add test
schmidt-sebastian May 30, 2020
ca9ab39
Update test names
schmidt-sebastian May 30, 2020
97cf094
Merge branch 'mrschmidt/setdoc' into mrschmidt/adddoc
schmidt-sebastian May 30, 2020
1a81864
Simplify
schmidt-sebastian May 30, 2020
068d5a5
Simplify
schmidt-sebastian May 30, 2020
94e22df
Merge branch 'mrschmidt/newgetdocument' into mrschmidt/deletedoc
schmidt-sebastian May 30, 2020
555a16c
Merge branch 'mrschmidt/deletedoc' of github.com:firebase/firebase-js…
schmidt-sebastian May 30, 2020
61b7c4c
Merge branch 'mrschmidt/deletedoc' into mrschmidt/setdoc
schmidt-sebastian May 30, 2020
d8abac1
Simplify
schmidt-sebastian May 30, 2020
ad1a038
Merge branch 'mrschmidt/setdoc' into mrschmidt/adddoc
schmidt-sebastian May 30, 2020
b2fd485
Simplify
schmidt-sebastian May 30, 2020
df8186b
addDoc
schmidt-sebastian May 30, 2020
8a7fc93
Add documentID() (#3137)
schmidt-sebastian Jun 1, 2020
8bb9829
Add updateDoc()
schmidt-sebastian Jun 2, 2020
3242bc0
Add WriteBatch
schmidt-sebastian Jun 2, 2020
f3bfcb2
Use validateReference
schmidt-sebastian Jun 2, 2020
637b4bd
Update types
schmidt-sebastian Jun 2, 2020
1da4a6f
Add Transaction
schmidt-sebastian Jun 2, 2020
7736e3d
Lint
schmidt-sebastian Jun 2, 2020
5dc9471
Merge
schmidt-sebastian Jun 2, 2020
0230550
Typo
schmidt-sebastian Jun 2, 2020
c3e3375
Merge
schmidt-sebastian Jun 3, 2020
835ad93
Merge
schmidt-sebastian Jun 3, 2020
bd5a998
Merge
schmidt-sebastian Jun 3, 2020
f6553cb
Merge branch 'mrschmidt/snapshot' of github.com:firebase/firebase-js-…
schmidt-sebastian Jun 3, 2020
25f01e8
Merge
schmidt-sebastian Jun 3, 2020
0174149
Add terminate()
schmidt-sebastian Jun 3, 2020
7c4e78a
Lint
schmidt-sebastian Jun 3, 2020
5e62710
lint
schmidt-sebastian Jun 3, 2020
9aa0263
Add test
schmidt-sebastian Jun 3, 2020
2685912
Simplify
schmidt-sebastian Jun 4, 2020
6a34bbb
Merge
schmidt-sebastian Jun 4, 2020
3361a63
Feedback
schmidt-sebastian Jun 4, 2020
27bed90
Merge
schmidt-sebastian Jun 4, 2020
6f8521a
Merge branch 'master' into mrschmidt/newgetdocument
schmidt-sebastian Jun 4, 2020
4142278
Merge
schmidt-sebastian Jun 4, 2020
a005ed5
Merge
schmidt-sebastian Jun 4, 2020
d7200bd
Merge
schmidt-sebastian Jun 5, 2020
6af39e9
Merge
schmidt-sebastian Jun 5, 2020
b327858
Merge
schmidt-sebastian Jun 5, 2020
403fe89
Merge
schmidt-sebastian Jun 5, 2020
93d89cd
Lint
schmidt-sebastian Jun 5, 2020
7b6e899
Merge
schmidt-sebastian Jun 5, 2020
03f8223
Merge
schmidt-sebastian Jun 5, 2020
2216e4a
Merge
schmidt-sebastian Jun 5, 2020
29b9fa9
Merge branch 'master' into mrschmidt/terminate
schmidt-sebastian Jun 5, 2020
07005ec
fix build
schmidt-sebastian Jun 5, 2020
cba92df
Merge
schmidt-sebastian Jun 5, 2020
863a6d8
Fix
schmidt-sebastian Jun 5, 2020
8d14d84
Merge
schmidt-sebastian Jun 5, 2020
581a42c
Merge
schmidt-sebastian Jun 5, 2020
f77c7a9
Merge
schmidt-sebastian Jun 5, 2020
3d6bb9c
Feedback
schmidt-sebastian Jun 5, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/firestore/lite/index.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ export { DocumentSnapshot, QueryDocumentSnapshot } from './src/api/snapshot';

export { WriteBatch, writeBatch } from './src/api/write_batch';

export { Transaction, runTransaction } from './src/api/transaction';

export { setLogLevel } from '../src/util/log';

export function registerFirestore(): void {
Expand Down
181 changes: 181 additions & 0 deletions packages/firestore/lite/src/api/transaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/**
* @license
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as firestore from '../../';

import { UserDataReader } from '../../../src/api/user_data_reader';
import { Transaction as InternalTransaction } from '../../../src/core/transaction';
import {
Document,
MaybeDocument,
NoDocument
} from '../../../src/model/document';
import { fail } from '../../../src/util/assert';
import { applyFirestoreDataConverter } from '../../../src/api/database';
import { DocumentSnapshot } from './snapshot';
import { Firestore } from './database';
import { TransactionRunner } from '../../../src/core/transaction_runner';
import { AsyncQueue } from '../../../src/util/async_queue';
import { Deferred } from '../../../src/util/promise';
import { FieldPath as ExternalFieldPath } from '../../../src/api/field_path';
import { validateReference } from './write_batch';
import { newUserDataReader } from './reference';
import { FieldPath } from './field_path';
import { cast } from './util';

export class Transaction implements firestore.Transaction {
// This is the lite version of the Transaction API used in the legacy SDK. The
// class is a close copy but takes different input types.

private readonly _dataReader: UserDataReader;

constructor(
private readonly _firestore: Firestore,
private readonly _transaction: InternalTransaction
) {
// Kick off configuring the client, which freezes the settings.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
_firestore._ensureClientConfigured();
this._dataReader = newUserDataReader(
_firestore._databaseId,
_firestore._settings!
);
}

get<T>(
documentRef: firestore.DocumentReference<T>
): Promise<firestore.DocumentSnapshot<T>> {
const ref = validateReference(documentRef, this._firestore);
return this._transaction
.lookup([ref._key])
.then((docs: MaybeDocument[]) => {
if (!docs || docs.length !== 1) {
return fail('Mismatch in docs returned from document lookup.');
}
const doc = docs[0];
if (doc instanceof NoDocument) {
return new DocumentSnapshot<T>(
this._firestore,
ref._key,
null,
ref._converter
);
} else if (doc instanceof Document) {
return new DocumentSnapshot<T>(
this._firestore,
doc.key,
doc,
ref._converter
);
} else {
throw fail(
`BatchGetDocumentsRequest returned unexpected document type: ${doc.constructor.name}`
);
}
});
}

set<T>(documentRef: firestore.DocumentReference<T>, value: T): Transaction;
set<T>(
documentRef: firestore.DocumentReference<T>,
value: Partial<T>,
options: firestore.SetOptions
): Transaction;
set<T>(
documentRef: firestore.DocumentReference<T>,
value: T,
options?: firestore.SetOptions
): Transaction {
const ref = validateReference(documentRef, this._firestore);
const [convertedValue] = applyFirestoreDataConverter(
ref._converter,
value,
'Transaction.set'
);
const parsed = this._dataReader.parseSetData(
'Transaction.set',
convertedValue,
options
);
this._transaction.set(ref._key, parsed);
return this;
}

update(
documentRef: firestore.DocumentReference<unknown>,
value: firestore.UpdateData
): Transaction;
update(
documentRef: firestore.DocumentReference<unknown>,
field: string | ExternalFieldPath,
value: unknown,
...moreFieldsAndValues: unknown[]
): Transaction;
update(
documentRef: firestore.DocumentReference<unknown>,
fieldOrUpdateData: string | ExternalFieldPath | firestore.UpdateData,
value?: unknown,
...moreFieldsAndValues: unknown[]
): Transaction {
const ref = validateReference(documentRef, this._firestore);

let parsed;
if (
typeof fieldOrUpdateData === 'string' ||
fieldOrUpdateData instanceof FieldPath
) {
parsed = this._dataReader.parseUpdateVarargs(
'Transaction.update',
fieldOrUpdateData,
value,
moreFieldsAndValues
);
} else {
parsed = this._dataReader.parseUpdateData(
'Transaction.update',
fieldOrUpdateData
);
}

this._transaction.update(ref._key, parsed);
return this;
}

delete(documentRef: firestore.DocumentReference<unknown>): Transaction {
const ref = validateReference(documentRef, this._firestore);
this._transaction.delete(ref._key);
return this;
}
}

export function runTransaction<T>(
firestore: firestore.FirebaseFirestore,
updateFunction: (transaction: firestore.Transaction) => Promise<T>
): Promise<T> {
const firestoreClient = cast(firestore, Firestore);
return firestoreClient._ensureClientConfigured().then(async datastore => {
const deferred = new Deferred<T>();
new TransactionRunner<T>(
new AsyncQueue(),
datastore,
internalTransaction =>
updateFunction(new Transaction(firestoreClient, internalTransaction)),
deferred
).run();
return deferred.promise;
});
}
4 changes: 2 additions & 2 deletions packages/firestore/lite/src/api/write_batch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ export class WriteBatch implements firestore.WriteBatch {
// This is the lite version of the WriteBatch API used in the legacy SDK. The
// class is a close copy but takes different input types.

private readonly _dataReader: UserDataReader;
private _mutations = [] as Mutation[];
private _committed = false;
private _dataReader: UserDataReader;

constructor(private _firestore: Firestore) {
constructor(private readonly _firestore: Firestore) {
// Kick off configuring the client, which freezes the settings.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
_firestore._ensureClientConfigured();
Expand Down
120 changes: 112 additions & 8 deletions packages/firestore/lite/test/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ import {
DEFAULT_SETTINGS
} from '../../test/integration/util/settings';
import { writeBatch } from '../src/api/write_batch';
import { runTransaction } from '../src/api/transaction';
import { expectEqual, expectNotEqual } from '../../test/util/helpers';
import { FieldValue } from '../../src/api/field_value';

use(chaiAsPromised);

describe('Firestore', () => {
Expand Down Expand Up @@ -324,7 +324,95 @@ describe('WriteBatch', () => {
});
});

function genericMutationTests(op: MutationTester): void {
describe('Transaction', () => {
class TransactionTester implements MutationTester {
delete(ref: firestore.DocumentReference<unknown>): Promise<void> {
return runTransaction(ref.firestore, async transaction => {
transaction.delete(ref);
});
}

set<T>(
ref: firestore.DocumentReference<T>,
data: T | Partial<T>,
options?: firestore.SetOptions
): Promise<void> {
const args = Array.from(arguments);
return runTransaction(ref.firestore, async transaction => {
// TODO(mrschmidt): Find a way to remove the `any` cast here
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(transaction.set as any).apply(transaction, args);
});
}

update(
ref: firestore.DocumentReference<unknown>,
dataOrField: firestore.UpdateData | string | firestore.FieldPath,
value?: unknown,
...moreFieldsAndValues: unknown[]
): Promise<void> {
const args = Array.from(arguments);
return runTransaction(ref.firestore, async transaction => {
// TODO(mrschmidt): Find a way to remove the `any` cast here
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(transaction.update as any).apply(transaction, args);
});
}
}

genericMutationTests(
new TransactionTester(),
/* validationUsesPromises= */ true
);

it('can read and then write', () => {
return withTestDocAndInitialData({ counter: 1 }, async doc => {
await runTransaction(doc.firestore, async transaction => {
const snap = await transaction.get(doc);
transaction.update(doc, 'counter', snap.get('counter') + 1);
});
const result = await getDoc(doc);
expect(result.get('counter')).to.equal(2);
});
});

it('can read non-existing doc then write', () => {
return withTestDoc(async doc => {
await runTransaction(doc.firestore, async transaction => {
const snap = await transaction.get(doc);
expect(snap.exists()).to.be.false;
transaction.set(doc, { counter: 1 });
});
const result = await getDoc(doc);
expect(result.get('counter')).to.equal(1);
});
});

it('retries when document is modified', () => {
return withTestDoc(async doc => {
let retryCounter = 0;
await runTransaction(doc.firestore, async transaction => {
++retryCounter;
await transaction.get(doc);

if (retryCounter === 1) {
// Out of band modification that doesn't use the transaction
await setDoc(doc, { counter: 'invalid' });
}

transaction.set(doc, { counter: 1 });
});
expect(retryCounter).to.equal(2);
const result = await getDoc(doc);
expect(result.get('counter')).to.equal(1);
});
});
});

function genericMutationTests(
op: MutationTester,
validationUsesPromises: boolean = false
): void {
const setDoc = op.set;
const updateDoc = op.update;
const deleteDoc = op.delete;
Expand Down Expand Up @@ -379,9 +467,17 @@ function genericMutationTests(op: MutationTester): void {

it('throws when user input fails validation', () => {
return withTestDoc(async docRef => {
expect(() => setDoc(docRef, { val: undefined })).to.throw(
/Function .* called with invalid data. Unsupported field value: undefined \(found in field val\)/
);
if (validationUsesPromises) {
return expect(
setDoc(docRef, { val: undefined })
).to.eventually.be.rejectedWith(
/Function .* called with invalid data. Unsupported field value: undefined \(found in field val\)/
);
} else {
expect(() => setDoc(docRef, { val: undefined })).to.throw(
/Function .* called with invalid data. Unsupported field value: undefined \(found in field val\)/
);
}
});
});

Expand Down Expand Up @@ -418,9 +514,17 @@ function genericMutationTests(op: MutationTester): void {

it('throws when user input fails validation', () => {
return withTestDoc(async docRef => {
expect(() => updateDoc(docRef, { val: undefined })).to.throw(
/Function .* called with invalid data. Unsupported field value: undefined \(found in field val\)/
);
if (validationUsesPromises) {
return expect(
updateDoc(docRef, { val: undefined })
).to.eventually.be.rejectedWith(
/Function .* called with invalid data. Unsupported field value: undefined \(found in field val\)/
);
} else {
expect(() => updateDoc(docRef, { val: undefined })).to.throw(
/Function .* called with invalid data. Unsupported field value: undefined \(found in field val\)/
);
}
});
});
});
Expand Down
2 changes: 1 addition & 1 deletion packages/firestore/src/util/promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface Rejecter {
(reason?: Error): void;
}

export class Deferred<R> {
export class Deferred<R = void> {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of adding void to the generic? It seems to compile and run fine even when I removed it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without it new Deferred() is Deferred<unknown>. I would like it to default it to Deferred<void>.

promise: Promise<R>;
// Assigned synchronously in constructor by Promise constructor callback.
resolve!: Resolver<R>;
Expand Down