Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat: add support for Typescript Custom Mapping (#828)
  • Loading branch information
Brian Chen committed Jan 14, 2020
1 parent 36d75f6 commit 94ddc89
Show file tree
Hide file tree
Showing 16 changed files with 967 additions and 299 deletions.
9 changes: 5 additions & 4 deletions dev/src/document-change.ts
Expand Up @@ -15,6 +15,7 @@
*/

import {QueryDocumentSnapshot} from './document';
import {DocumentData} from './types';

export type DocumentChangeType = 'added' | 'removed' | 'modified';

Expand All @@ -24,9 +25,9 @@ export type DocumentChangeType = 'added' | 'removed' | 'modified';
*
* @class
*/
export class DocumentChange {
export class DocumentChange<T = DocumentData> {
private readonly _type: DocumentChangeType;
private readonly _document: QueryDocumentSnapshot;
private readonly _document: QueryDocumentSnapshot<T>;
private readonly _oldIndex: number;
private readonly _newIndex: number;

Expand All @@ -42,7 +43,7 @@ export class DocumentChange {
*/
constructor(
type: DocumentChangeType,
document: QueryDocumentSnapshot,
document: QueryDocumentSnapshot<T>,
oldIndex: number,
newIndex: number
) {
Expand Down Expand Up @@ -169,7 +170,7 @@ export class DocumentChange {
* @param {*} other The value to compare against.
* @return true if this `DocumentChange` is equal to the provided value.
*/
isEqual(other: DocumentChange): boolean {
isEqual(other: DocumentChange<T>): boolean {
if (this === other) {
return true;
}
Expand Down
77 changes: 37 additions & 40 deletions dev/src/document.ts
Expand Up @@ -36,10 +36,7 @@ import api = google.firestore.v1;
*
* @private
*/
export class DocumentSnapshotBuilder {
/** The reference to the document. */
ref?: DocumentReference;

export class DocumentSnapshotBuilder<T = DocumentData> {
/** The fields of the Firestore `Document` Protobuf backing this document. */
fieldsProto?: ApiMapValue;

Expand All @@ -52,14 +49,18 @@ export class DocumentSnapshotBuilder {
/** The time when this document was last updated. */
updateTime?: Timestamp;

// We include the DocumentReference in the constructor in order to allow the
// DocumentSnapshotBuilder to be typed with <T> when it is constructed.
constructor(readonly ref: DocumentReference<T>) {}

/**
* Builds the DocumentSnapshot.
*
* @private
* @returns Returns either a QueryDocumentSnapshot (if `fieldsProto` was
* provided) or a DocumentSnapshot.
*/
build(): QueryDocumentSnapshot | DocumentSnapshot {
build(): QueryDocumentSnapshot<T> | DocumentSnapshot<T> {
assert(
(this.fieldsProto !== undefined) === (this.createTime !== undefined),
'Create time should be set iff document exists.'
Expand Down Expand Up @@ -94,9 +95,8 @@ export class DocumentSnapshotBuilder {
*
* @class
*/
export class DocumentSnapshot {
private _ref: DocumentReference;
private _fieldsProto: ApiMapValue | undefined;
export class DocumentSnapshot<T = DocumentData> {
private _ref: DocumentReference<T>;
private _serializer: Serializer;
private _readTime: Timestamp | undefined;
private _createTime: Timestamp | undefined;
Expand All @@ -106,7 +106,7 @@ export class DocumentSnapshot {
* @hideconstructor
*
* @param ref The reference to the document.
* @param fieldsProto The fields of the Firestore `Document` Protobuf backing
* @param _fieldsProto The fields of the Firestore `Document` Protobuf backing
* this document (or undefined if the document does not exist).
* @param readTime The time when this snapshot was read (or undefined if
* the document exists only locally).
Expand All @@ -116,14 +116,13 @@ export class DocumentSnapshot {
* if the document does not exist).
*/
constructor(
ref: DocumentReference,
fieldsProto?: ApiMapValue,
ref: DocumentReference<T>,
readonly _fieldsProto?: ApiMapValue,
readTime?: Timestamp,
createTime?: Timestamp,
updateTime?: Timestamp
) {
this._ref = ref;
this._fieldsProto = fieldsProto;
this._serializer = ref.firestore._serializer!;
this._readTime = readTime;
this._createTime = createTime;
Expand All @@ -138,10 +137,10 @@ export class DocumentSnapshot {
* @param obj The object to store in the DocumentSnapshot.
* @return The created DocumentSnapshot.
*/
static fromObject(
ref: DocumentReference,
static fromObject<U>(
ref: DocumentReference<U>,
obj: DocumentData
): DocumentSnapshot {
): DocumentSnapshot<U> {
const serializer = ref.firestore._serializer!;
return new DocumentSnapshot(ref, serializer.encodeFields(obj));
}
Expand All @@ -156,10 +155,10 @@ export class DocumentSnapshot {
* @param data The field/value map to expand.
* @return The created DocumentSnapshot.
*/
static fromUpdateMap(
ref: DocumentReference,
static fromUpdateMap<U>(
ref: DocumentReference<U>,
data: UpdateMap
): DocumentSnapshot {
): DocumentSnapshot<U> {
const serializer = ref.firestore._serializer!;

/**
Expand Down Expand Up @@ -270,7 +269,7 @@ export class DocumentSnapshot {
* }
* });
*/
get ref(): DocumentReference {
get ref(): DocumentReference<T> {
return this._ref;
}

Expand Down Expand Up @@ -364,8 +363,8 @@ export class DocumentSnapshot {
* Retrieves all fields in the document as an object. Returns 'undefined' if
* the document doesn't exist.
*
* @returns {DocumentData|undefined} An object containing all fields in the
* document or 'undefined' if the document doesn't exist.
* @returns {T|undefined} An object containing all fields in the document or
* 'undefined' if the document doesn't exist.
*
* @example
* let documentRef = firestore.doc('col/doc');
Expand All @@ -375,11 +374,7 @@ export class DocumentSnapshot {
* console.log(`Retrieved data: ${JSON.stringify(data)}`);
* });
*/
// We deliberately use `any` in the external API to not impose type-checking
// on end users.
// tslint:disable-next-line no-any
data(): {[field: string]: any} | undefined {
// tslint:disable-line no-any
data(): T | undefined {
const fields = this._fieldsProto;

if (fields === undefined) {
Expand All @@ -390,7 +385,7 @@ export class DocumentSnapshot {
for (const prop of Object.keys(fields)) {
obj[prop] = this._serializer.decodeValue(fields[prop]);
}
return obj;
return this.ref._converter.fromFirestore(obj);
}

/**
Expand Down Expand Up @@ -490,7 +485,7 @@ export class DocumentSnapshot {
* @return {boolean} true if this `DocumentSnapshot` is equal to the provided
* value.
*/
isEqual(other: DocumentSnapshot): boolean {
isEqual(other: DocumentSnapshot<T>): boolean {
// Since the read time is different on every document read, we explicitly
// ignore all document metadata in this comparison.
return (
Expand All @@ -517,7 +512,9 @@ export class DocumentSnapshot {
* @class
* @extends DocumentSnapshot
*/
export class QueryDocumentSnapshot extends DocumentSnapshot {
export class QueryDocumentSnapshot<T = DocumentData> extends DocumentSnapshot<
T
> {
/**
* @hideconstructor
*
Expand All @@ -529,7 +526,7 @@ export class QueryDocumentSnapshot extends DocumentSnapshot {
* @param updateTime The time when the document was last updated.
*/
constructor(
ref: DocumentReference,
ref: DocumentReference<T>,
fieldsProto: ApiMapValue,
readTime: Timestamp,
createTime: Timestamp,
Expand Down Expand Up @@ -582,7 +579,7 @@ export class QueryDocumentSnapshot extends DocumentSnapshot {
*
* @override
*
* @returns {DocumentData} An object containing all fields in the document.
* @returns {T} An object containing all fields in the document.
*
* @example
* let query = firestore.collection('col');
Expand All @@ -592,7 +589,7 @@ export class QueryDocumentSnapshot extends DocumentSnapshot {
* console.log(`Retrieved data: ${JSON.stringify(data)}`);
* });
*/
data(): DocumentData {
data(): T {
const data = super.data();
if (!data) {
throw new Error(
Expand Down Expand Up @@ -871,7 +868,7 @@ export class DocumentMask {
* @private
* @class
*/
export class DocumentTransform {
export class DocumentTransform<T = DocumentData> {
/**
* @private
* @hideconstructor
Expand All @@ -880,7 +877,7 @@ export class DocumentTransform {
* @param transforms A Map of FieldPaths to FieldTransforms.
*/
constructor(
private readonly ref: DocumentReference,
private readonly ref: DocumentReference<T>,
private readonly transforms: Map<FieldPath, FieldTransform>
) {}

Expand All @@ -892,10 +889,10 @@ export class DocumentTransform {
* @param obj The object to extract the transformations from.
* @returns The Document Transform.
*/
static fromObject(
ref: DocumentReference,
static fromObject<T>(
ref: DocumentReference<T>,
obj: DocumentData
): DocumentTransform {
): DocumentTransform<T> {
const updateMap = new Map<FieldPath, unknown>();

for (const prop of Object.keys(obj)) {
Expand All @@ -913,10 +910,10 @@ export class DocumentTransform {
* @param data The update data to extract the transformations from.
* @returns The Document Transform.
*/
static fromUpdateMap(
ref: DocumentReference,
static fromUpdateMap<T>(
ref: DocumentReference<T>,
data: UpdateMap
): DocumentTransform {
): DocumentTransform<T> {
const transforms = new Map<FieldPath, FieldTransform>();

function encode_(val: unknown, path: FieldPath, allowTransforms: boolean) {
Expand Down
41 changes: 26 additions & 15 deletions dev/src/index.ts
Expand Up @@ -43,6 +43,7 @@ import {Timestamp} from './timestamp';
import {parseGetAllArguments, Transaction} from './transaction';
import {
ApiMapValue,
DocumentData,
FirestoreStreamingMethod,
FirestoreUnaryMethod,
GapicClient,
Expand Down Expand Up @@ -83,6 +84,7 @@ export {FieldPath} from './path';
export {GeoPoint} from './geo-point';
export {setLogFunction} from './logger';
export {
FirestoreDataConverter,
UpdateData,
DocumentData,
Settings,
Expand Down Expand Up @@ -695,20 +697,22 @@ export class Firestore {
);
}

const document = new DocumentSnapshotBuilder();

let ref: DocumentReference;
let document: DocumentSnapshotBuilder;
if (typeof documentOrName === 'string') {
document.ref = new DocumentReference(
ref = new DocumentReference(
this,
QualifiedResourcePath.fromSlashSeparatedString(documentOrName)
);
document = new DocumentSnapshotBuilder(ref);
} else {
document.ref = new DocumentReference(
ref = new DocumentReference(
this,
QualifiedResourcePath.fromSlashSeparatedString(
documentOrName.name as string
)
);
document = new DocumentSnapshotBuilder(ref);
document.fieldsProto = documentOrName.fields
? convertFields(documentOrName.fields as ApiMapValue)
: {};
Expand Down Expand Up @@ -886,7 +890,7 @@ export class Firestore {
* }
* });
*/
listCollections() {
listCollections(): Promise<CollectionReference[]> {
const rootDocument = new DocumentReference(this, ResourcePath.EMPTY);
return rootDocument.listCollections();
}
Expand All @@ -912,9 +916,9 @@ export class Firestore {
* console.log(`Second document: ${JSON.stringify(docs[1])}`);
* });
*/
getAll(
...documentRefsOrReadOptions: Array<DocumentReference | ReadOptions>
): Promise<DocumentSnapshot[]> {
getAll<T>(
...documentRefsOrReadOptions: Array<DocumentReference<T> | ReadOptions>
): Promise<Array<DocumentSnapshot<T>>> {
validateMinNumberOfArguments('Firestore.getAll', arguments, 1);

const {documents, fieldMask} = parseGetAllArguments(
Expand All @@ -937,12 +941,12 @@ export class Firestore {
* @param transactionId The transaction ID to use for this read.
* @returns A Promise that contains an array with the resulting documents.
*/
getAll_(
docRefs: DocumentReference[],
getAll_<T>(
docRefs: Array<DocumentReference<T>>,
fieldMask: FieldPath[] | null,
requestTag: string,
transactionId?: Uint8Array
): Promise<DocumentSnapshot[]> {
): Promise<Array<DocumentSnapshot<T>>> {
const requestedDocuments = new Set<string>();
const retrievedDocuments = new Map<string, DocumentSnapshot>();

Expand All @@ -962,11 +966,10 @@ export class Firestore {
}

const self = this;

return self
.requestStream('batchGetDocuments', request, requestTag)
.then(stream => {
return new Promise<DocumentSnapshot[]>((resolve, reject) => {
return new Promise<Array<DocumentSnapshot<T>>>((resolve, reject) => {
stream
.on('error', err => {
logger(
Expand Down Expand Up @@ -1024,11 +1027,19 @@ export class Firestore {

// BatchGetDocuments doesn't preserve document order. We use
// the request order to sort the resulting documents.
const orderedDocuments: DocumentSnapshot[] = [];
const orderedDocuments: Array<DocumentSnapshot<T>> = [];

for (const docRef of docRefs) {
const document = retrievedDocuments.get(docRef.path);
if (document !== undefined) {
orderedDocuments.push(document);
// Recreate the DocumentSnapshot with the DocumentReference
// containing the original converter.
const finalDoc = new DocumentSnapshotBuilder(docRef);
finalDoc.fieldsProto = document._fieldsProto;
finalDoc.readTime = document.readTime;
finalDoc.createTime = document.createTime;
finalDoc.updateTime = document.updateTime;
orderedDocuments.push(finalDoc.build());
} else {
reject(
new Error(`Did not receive document for "${docRef.path}".`)
Expand Down

0 comments on commit 94ddc89

Please sign in to comment.