Skip to content

Commit 2c66c4e

Browse files
Add setDoc() (#3139)
1 parent 3af49b4 commit 2c66c4e

File tree

8 files changed

+266
-109
lines changed

8 files changed

+266
-109
lines changed

packages/firestore/lite/index.node.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ export {
3636
doc,
3737
parent,
3838
getDoc,
39-
deleteDoc
39+
deleteDoc,
40+
setDoc
4041
} from './src/api/reference';
4142

4243
// TOOD(firestorelite): Add tests when setDoc() is available

packages/firestore/lite/src/api/field_path.ts

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,53 +17,30 @@
1717

1818
import * as firestore from '../../index';
1919

20+
import { BaseFieldPath } from '../../../src/api/field_path';
2021
import { cast } from './util';
21-
import {
22-
DOCUMENT_KEY_NAME,
23-
FieldPath as InternalFieldPath
24-
} from '../../../src/model/path';
25-
import { validateNamedArrayAtLeastNumberOfElements } from '../../../src/util/input_validation';
26-
import { Code, FirestoreError } from '../../../src/util/error';
22+
import { DOCUMENT_KEY_NAME } from '../../../src/model/path';
2723

2824
/**
2925
* A FieldPath refers to a field in a document. The path may consist of a single
3026
* field name (referring to a top-level field in the document), or a list of
3127
* field names (referring to a nested field in the document).
3228
*/
33-
export class FieldPath implements firestore.FieldPath {
29+
export class FieldPath extends BaseFieldPath implements firestore.FieldPath {
3430
// Note: This class is stripped down a copy of the FieldPath class in the
3531
// legacy SDK. The changes are:
3632
// - The `documentId()` static method has been removed
3733
// - Input validation is limited to errors that cannot be caught by the
3834
// TypeScript transpiler.
3935

40-
/** Internal representation of a Firestore field path. */
41-
_internalPath: InternalFieldPath;
42-
4336
/**
4437
* Creates a FieldPath from the provided field names. If more than one field
4538
* name is provided, the path will point to a nested field in a document.
4639
*
4740
* @param fieldNames A list of field names.
4841
*/
4942
constructor(...fieldNames: string[]) {
50-
validateNamedArrayAtLeastNumberOfElements(
51-
'FieldPath',
52-
fieldNames,
53-
'fieldNames',
54-
1
55-
);
56-
57-
const emptyElement = fieldNames.indexOf('');
58-
if (emptyElement !== -1) {
59-
throw new FirestoreError(
60-
Code.INVALID_ARGUMENT,
61-
`Invalid field name at argument $(i + 1). ` +
62-
'Field names must not be empty.'
63-
);
64-
}
65-
66-
this._internalPath = new InternalFieldPath(fieldNames);
43+
super(fieldNames);
6744
}
6845

6946
isEqual(other: firestore.FieldPath): boolean {

packages/firestore/lite/src/api/reference.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ import * as firestore from '../../index';
2020
import { Document } from '../../../src/model/document';
2121
import { DocumentKey } from '../../../src/model/document_key';
2222
import { Firestore } from './database';
23-
import { DocumentKeyReference } from '../../../src/api/user_data_reader';
23+
import {
24+
DocumentKeyReference,
25+
UserDataReader
26+
} from '../../../src/api/user_data_reader';
2427
import { Query as InternalQuery } from '../../../src/core/query';
2528
import { FirebaseFirestore, FirestoreDataConverter } from '../../index';
2629
import { ResourcePath } from '../../../src/model/path';
@@ -32,6 +35,9 @@ import {
3235
} from '../../../src/remote/datastore';
3336
import { hardAssert } from '../../../src/util/assert';
3437
import { DeleteMutation, Precondition } from '../../../src/model/mutation';
38+
import { PlatformSupport } from '../../../src/platform/platform';
39+
import { applyFirestoreDataConverter } from '../../../src/api/database';
40+
import { DatabaseId } from '../../../src/core/database_info';
3541
import { cast } from './util';
3642
import {
3743
validateArgType,
@@ -271,6 +277,36 @@ export function getDoc<T>(
271277
});
272278
}
273279

280+
export function setDoc<T>(
281+
reference: firestore.DocumentReference<T>,
282+
data: T,
283+
options?: firestore.SetOptions
284+
): Promise<void> {
285+
const ref = cast(reference, DocumentReference);
286+
287+
const [convertedValue] = applyFirestoreDataConverter(
288+
ref._converter,
289+
data,
290+
'setDoc'
291+
);
292+
293+
// Kick off configuring the client, which freezes the settings.
294+
const configureClient = ref.firestore._ensureClientConfigured();
295+
const dataReader = newUserDataReader(
296+
ref.firestore._databaseId,
297+
ref.firestore._settings!
298+
);
299+
300+
const parsed = dataReader.parseSetData('setDoc', convertedValue, options);
301+
302+
return configureClient.then(datastore =>
303+
invokeCommitRpc(
304+
datastore,
305+
parsed.toMutations(ref._key, Precondition.none())
306+
)
307+
);
308+
}
309+
274310
export function deleteDoc(
275311
reference: firestore.DocumentReference
276312
): Promise<void> {
@@ -283,3 +319,15 @@ export function deleteDoc(
283319
])
284320
);
285321
}
322+
323+
function newUserDataReader(
324+
databaseId: DatabaseId,
325+
settings: firestore.Settings
326+
): UserDataReader {
327+
const serializer = PlatformSupport.getPlatform().newSerializer(databaseId);
328+
return new UserDataReader(
329+
databaseId,
330+
!!settings.ignoreUndefinedProperties,
331+
serializer
332+
);
333+
}

packages/firestore/lite/test/helpers.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { initializeApp } from '@firebase/app-exp';
2020
import * as firestore from '../index';
2121

2222
import { initializeFirestore } from '../src/api/database';
23-
import { doc, collection } from '../src/api/reference';
23+
import { doc, collection, setDoc } from '../src/api/reference';
2424
import {
2525
DEFAULT_PROJECT_ID,
2626
DEFAULT_SETTINGS
@@ -55,3 +55,14 @@ export function withTestDoc(
5555
return fn(doc(collection(db, 'test-collection')));
5656
});
5757
}
58+
59+
export function withTestDocAndInitialData(
60+
data: firestore.DocumentData,
61+
fn: (doc: firestore.DocumentReference) => void | Promise<void>
62+
): Promise<void> {
63+
return withTestDb(async db => {
64+
const ref = doc(collection(db, 'test-collection'));
65+
await setDoc(ref, data);
66+
return fn(ref);
67+
});
68+
}

packages/firestore/lite/test/integration.test.ts

Lines changed: 134 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,27 @@ import {
2323
getFirestore,
2424
initializeFirestore
2525
} from '../src/api/database';
26-
import { withTestDb, withTestDoc } from './helpers';
26+
import {
27+
withTestDb,
28+
withTestDbSettings,
29+
withTestDoc,
30+
withTestDocAndInitialData
31+
} from './helpers';
2732
import {
2833
parent,
2934
collection,
3035
CollectionReference,
3136
doc,
3237
DocumentReference,
3338
getDoc,
34-
deleteDoc
39+
deleteDoc,
40+
setDoc
3541
} from '../src/api/reference';
42+
import { FieldPath } from '../src/api/field_path';
43+
import {
44+
DEFAULT_PROJECT_ID,
45+
DEFAULT_SETTINGS
46+
} from '../../test/integration/util/settings';
3647
import { expectEqual, expectNotEqual } from '../../test/util/helpers';
3748
import { FieldValue } from '../../src/api/field_value';
3849

@@ -169,7 +180,127 @@ describe('getDoc()', () => {
169180
});
170181
});
171182

172-
// TODO(firestorelite): Expand test coverage once we can write docs
183+
it('can get an existing document', () => {
184+
return withTestDocAndInitialData({ val: 1 }, async docRef => {
185+
const docSnap = await getDoc(docRef);
186+
expect(docSnap.exists()).to.be.true;
187+
});
188+
});
189+
});
190+
191+
describe('deleteDoc()', () => {
192+
it('can delete a non-existing document', () => {
193+
return withTestDoc(docRef => deleteDoc(docRef));
194+
});
195+
196+
it('can delete an existing document', () => {
197+
return withTestDoc(async docRef => {
198+
await setDoc(docRef, {});
199+
await deleteDoc(docRef);
200+
const docSnap = await getDoc(docRef);
201+
expect(docSnap.exists()).to.be.false;
202+
});
203+
});
204+
});
205+
206+
describe('setDoc()', () => {
207+
it('can set a new document', () => {
208+
return withTestDoc(async docRef => {
209+
await setDoc(docRef, { val: 1 });
210+
const docSnap = await getDoc(docRef);
211+
expect(docSnap.data()).to.deep.equal({ val: 1 });
212+
});
213+
});
214+
215+
it('can merge a document', () => {
216+
return withTestDocAndInitialData({ foo: 1 }, async docRef => {
217+
await setDoc(docRef, { bar: 2 }, { merge: true });
218+
const docSnap = await getDoc(docRef);
219+
expect(docSnap.data()).to.deep.equal({ foo: 1, bar: 2 });
220+
});
221+
});
222+
223+
it('can merge a document with mergeFields', () => {
224+
return withTestDocAndInitialData({ foo: 1 }, async docRef => {
225+
await setDoc(
226+
docRef,
227+
{ foo: 'ignored', bar: 2, baz: { foobar: 3 } },
228+
{ mergeFields: ['bar', new FieldPath('baz', 'foobar')] }
229+
);
230+
const docSnap = await getDoc(docRef);
231+
expect(docSnap.data()).to.deep.equal({
232+
foo: 1,
233+
bar: 2,
234+
baz: { foobar: 3 }
235+
});
236+
});
237+
});
238+
239+
it('throws when user input fails validation', () => {
240+
return withTestDoc(async docRef => {
241+
expect(() => setDoc(docRef, { val: undefined })).to.throw(
242+
'Function setDoc() called with invalid data. Unsupported field value: undefined (found in field val)'
243+
);
244+
});
245+
});
246+
247+
it("can ignore 'undefined'", () => {
248+
return withTestDbSettings(
249+
DEFAULT_PROJECT_ID,
250+
{ ...DEFAULT_SETTINGS, ignoreUndefinedProperties: true },
251+
async db => {
252+
const docRef = doc(collection(db, 'test-collection'));
253+
await setDoc(docRef, { val: undefined });
254+
const docSnap = await getDoc(docRef);
255+
expect(docSnap.data()).to.deep.equal({});
256+
}
257+
);
258+
});
259+
});
260+
261+
describe('DocumentSnapshot', () => {
262+
it('can represent missing data', () => {
263+
return withTestDoc(async docRef => {
264+
const docSnap = await getDoc(docRef);
265+
expect(docSnap.exists()).to.be.false;
266+
expect(docSnap.data()).to.be.undefined;
267+
});
268+
});
269+
270+
it('can return data', () => {
271+
return withTestDocAndInitialData({ foo: 1 }, async docRef => {
272+
const docSnap = await getDoc(docRef);
273+
expect(docSnap.exists()).to.be.true;
274+
expect(docSnap.data()).to.deep.equal({ foo: 1 });
275+
});
276+
});
277+
278+
it('can return single field', () => {
279+
return withTestDocAndInitialData({ foo: 1, bar: 2 }, async docRef => {
280+
const docSnap = await getDoc(docRef);
281+
expect(docSnap.get('foo')).to.equal(1);
282+
expect(docSnap.get(new FieldPath('bar'))).to.equal(2);
283+
});
284+
});
285+
286+
it('can return nested field', () => {
287+
return withTestDocAndInitialData({ foo: { bar: 1 } }, async docRef => {
288+
const docSnap = await getDoc(docRef);
289+
expect(docSnap.get('foo.bar')).to.equal(1);
290+
expect(docSnap.get(new FieldPath('foo', 'bar'))).to.equal(1);
291+
});
292+
});
293+
294+
it('is properly typed', () => {
295+
return withTestDocAndInitialData({ foo: 1 }, async docRef => {
296+
const docSnap = await getDoc(docRef);
297+
let documentData = docSnap.data()!; // "data" is typed as nullable
298+
if (docSnap.exists()) {
299+
documentData = docSnap.data(); // "data" is typed as non-null
300+
}
301+
expect(documentData).to.deep.equal({ foo: 1 });
302+
});
303+
});
173304
});
174305

175306
describe('deleteDoc()', () => {

0 commit comments

Comments
 (0)