Skip to content

Add getQuery(), getQueryFromCache() and getQueryFromServer() #3294

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 18 commits into from
Jun 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 .changeset/ten-plants-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
70 changes: 64 additions & 6 deletions packages/firestore/exp/src/api/reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ import { Firestore } from './database';
import { DocumentKeyReference } from '../../../src/api/user_data_reader';
import { debugAssert } from '../../../src/util/assert';
import { cast } from '../../../lite/src/api/util';
import { DocumentSnapshot } from './snapshot';
import { DocumentSnapshot, QuerySnapshot } from './snapshot';
import {
getDocsViaSnapshotListener,
getDocViaSnapshotListener,
SnapshotMetadata
} from '../../../src/api/database';
import { ViewSnapshot } from '../../../src/core/view_snapshot';
import { DocumentReference } from '../../../lite/src/api/reference';
import { DocumentReference, Query } from '../../../lite/src/api/reference';
import { Document } from '../../../src/model/document';

export function getDoc<T>(
Expand Down Expand Up @@ -59,11 +60,11 @@ export function getDocFromCache<T>(
firestore,
ref._key,
doc,
ref._converter,
new SnapshotMetadata(
doc instanceof Document ? doc.hasLocalMutations : false,
/* fromCache= */ true
)
),
ref._converter
);
});
}
Expand All @@ -83,6 +84,63 @@ export function getDocFromServer<T>(
});
}

export function getQuery<T>(
query: firestore.Query<T>
): Promise<QuerySnapshot<T>> {
const internalQuery = cast<Query<T>>(query, Query);
const firestore = cast<Firestore>(query.firestore, Firestore);
return firestore._getFirestoreClient().then(async firestoreClient => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you make methods like this fully async? I'm not seeing why they need mixed notation. This applies to the other functions below.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The outermost code needs to be async to ensure synchronous function execution, which impacts error propagation and execution order. The cast calls here, for example, would not throw but cause rejected Promises.

const snapshot = await getDocsViaSnapshotListener(
firestoreClient,
internalQuery._query
);
return new QuerySnapshot(
firestore,
internalQuery,
snapshot,
new SnapshotMetadata(snapshot.hasPendingWrites, snapshot.fromCache)
);
});
}

export function getQueryFromCache<T>(
query: firestore.Query<T>
): Promise<QuerySnapshot<T>> {
const internalQuery = cast<Query<T>>(query, Query);
const firestore = cast<Firestore>(query.firestore, Firestore);
return firestore._getFirestoreClient().then(async firestoreClient => {
const snapshot = await firestoreClient.getDocumentsFromLocalCache(
internalQuery._query
);
return new QuerySnapshot(
firestore,
internalQuery,
snapshot,
new SnapshotMetadata(snapshot.hasPendingWrites, /* fromCache= */ true)
);
});
}

export function getQueryFromServer<T>(
query: firestore.Query<T>
): Promise<QuerySnapshot<T>> {
const internalQuery = cast<Query<T>>(query, Query);
const firestore = cast<Firestore>(query.firestore, Firestore);
return firestore._getFirestoreClient().then(async firestoreClient => {
const snapshot = await getDocsViaSnapshotListener(
firestoreClient,
internalQuery._query,
{ source: 'server' }
);
return new QuerySnapshot(
firestore,
internalQuery,
snapshot,
new SnapshotMetadata(snapshot.hasPendingWrites, snapshot.fromCache)
);
});
}

/**
* Converts a ViewSnapshot that contains the single document specified by `ref`
* to a DocumentSnapshot.
Expand All @@ -102,7 +160,7 @@ function convertToDocSnapshot<T>(
firestore,
ref._key,
doc,
ref._converter,
new SnapshotMetadata(snapshot.hasPendingWrites, snapshot.fromCache)
new SnapshotMetadata(snapshot.hasPendingWrites, snapshot.fromCache),
ref._converter
);
}
102 changes: 96 additions & 6 deletions packages/firestore/exp/src/api/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,13 @@ import {
} from '../../../lite/src/api/snapshot';
import { Firestore } from './database';
import { cast } from '../../../lite/src/api/util';
import { DocumentReference } from '../../../lite/src/api/reference';
import { SnapshotMetadata } from '../../../src/api/database';
import { DocumentReference, Query } from '../../../lite/src/api/reference';
import {
changesFromSnapshot,
SnapshotMetadata
} from '../../../src/api/database';
import { Code, FirestoreError } from '../../../src/util/error';
import { ViewSnapshot } from '../../../src/core/view_snapshot';

const DEFAULT_SERVER_TIMESTAMP_BEHAVIOR: ServerTimestampBehavior = 'none';

Expand All @@ -43,8 +48,8 @@ export class DocumentSnapshot<T = firestore.DocumentData>
readonly _firestore: Firestore,
key: DocumentKey,
document: Document | null,
converter: firestore.FirestoreDataConverter<T> | null,
readonly metadata: firestore.SnapshotMetadata
readonly metadata: firestore.SnapshotMetadata,
converter: firestore.FirestoreDataConverter<T> | null
) {
super(_firestore, key, document, converter);
this._firestoreImpl = cast(_firestore, Firestore);
Expand All @@ -64,8 +69,8 @@ export class DocumentSnapshot<T = firestore.DocumentData>
this._firestore,
this._key,
this._document,
/* converter= */ null,
this.metadata
this.metadata,
/* converter= */ null
);
return this._converter.fromFirestore(snapshot);
} else {
Expand Down Expand Up @@ -109,3 +114,88 @@ export class QueryDocumentSnapshot<T = firestore.DocumentData>
return super.data(options) as T;
}
}

export class QuerySnapshot<T = firestore.DocumentData>
implements firestore.QuerySnapshot<T> {
private _cachedChanges?: Array<firestore.DocumentChange<T>>;
private _cachedChangesIncludeMetadataChanges?: boolean;

constructor(
private readonly _firestore: Firestore,
readonly query: Query<T>,
private readonly _snapshot: ViewSnapshot,
readonly metadata: SnapshotMetadata
) {}

get docs(): Array<firestore.QueryDocumentSnapshot<T>> {
const result: Array<firestore.QueryDocumentSnapshot<T>> = [];
this.forEach(doc => result.push(doc));
return result;
}

get size(): number {
return this._snapshot.docs.size;
}

get empty(): boolean {
return this.size === 0;
}

forEach(
callback: (result: firestore.QueryDocumentSnapshot<T>) => void,
thisArg?: unknown
): void {
this._snapshot.docs.forEach(doc => {
callback.call(
thisArg,
this._convertToDocumentSnapshot(
doc,
this.metadata.fromCache,
this._snapshot.mutatedKeys.has(doc.key)
)
);
});
}

docChanges(
options: firestore.SnapshotListenOptions = {}
): Array<firestore.DocumentChange<T>> {
const includeMetadataChanges = !!options.includeMetadataChanges;

if (includeMetadataChanges && this._snapshot.excludesMetadataChanges) {
throw new FirestoreError(
Code.INVALID_ARGUMENT,
'To include metadata changes with your document changes, you must ' +
'also pass { includeMetadataChanges:true } to onSnapshot().'
);
}

if (
!this._cachedChanges ||
this._cachedChangesIncludeMetadataChanges !== includeMetadataChanges
) {
this._cachedChanges = changesFromSnapshot<QueryDocumentSnapshot<T>>(
this._snapshot,
includeMetadataChanges,
this._convertToDocumentSnapshot.bind(this)
);
this._cachedChangesIncludeMetadataChanges = includeMetadataChanges;
}

return this._cachedChanges;
}

private _convertToDocumentSnapshot(
doc: Document,
fromCache: boolean,
hasPendingWrites: boolean
): QueryDocumentSnapshot<T> {
return new QueryDocumentSnapshot<T>(
this._firestore,
doc.key,
doc,
new SnapshotMetadata(hasPendingWrites, fromCache),
this.query._converter
);
}
}
9 changes: 9 additions & 0 deletions packages/firestore/exp/test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
DEFAULT_SETTINGS
} from '../../test/integration/util/settings';
import { collection } from '../../lite/src/api/reference';
import { AutoId } from '../../src/util/misc';

let appCount = 0;

Expand All @@ -49,6 +50,14 @@ export function withTestDb(
return withTestDbSettings(DEFAULT_PROJECT_ID, DEFAULT_SETTINGS, fn);
}

export function withTestCollection(
fn: (collRef: firestore.CollectionReference) => void | Promise<void>
): Promise<void> {
return withTestDb(db => {
return fn(collection(db, AutoId.newId()));
});
}

export function withTestDoc(
fn: (doc: firestore.DocumentReference) => void | Promise<void>
): Promise<void> {
Expand Down
50 changes: 48 additions & 2 deletions packages/firestore/exp/test/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
* limitations under the License.
*/

import * as firestore from '../';

import { initializeApp } from '@firebase/app-exp';
import { expect, use } from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
Expand All @@ -24,12 +26,16 @@ import {
getFirestore,
initializeFirestore
} from '../src/api/database';
import { withTestDoc } from './helpers';
import { withTestCollection, withTestDoc } from './helpers';
import {
getDoc,
getDocFromCache,
getDocFromServer
getDocFromServer,
getQuery,
getQueryFromCache,
getQueryFromServer
} from '../src/api/reference';
import { QuerySnapshot } from '../src/api/snapshot';

use(chaiAsPromised);

Expand Down Expand Up @@ -87,3 +93,43 @@ describe('getDocFromServer()', () => {
});
});
});

describe('getQuery()', () => {
it('can query a non-existing collection', () => {
return withTestCollection(async collRef => {
const querySnap = await getQuery(collRef);
validateEmptySnapshot(querySnap, /* fromCache= */ false);
});
});
});

describe('getQueryFromCache()', () => {
it('can query a non-existing collection', () => {
return withTestCollection(async collRef => {
const querySnap = await getQueryFromCache(collRef);
validateEmptySnapshot(querySnap, /* fromCache= */ true);
});
});
});

describe('getQueryFromServer()', () => {
it('can query a non-existing collection', () => {
return withTestCollection(async collRef => {
const querySnap = await getQueryFromServer(collRef);
validateEmptySnapshot(querySnap, /* fromCache= */ false);
});
});
});

function validateEmptySnapshot(
querySnap: QuerySnapshot<firestore.DocumentData>,
fromCache: boolean
): void {
expect(querySnap.metadata.fromCache).to.equal(fromCache);
expect(querySnap.metadata.hasPendingWrites).to.be.false;
expect(querySnap.empty).to.be.true;
expect(querySnap.size).to.equal(0);
expect(querySnap.docs).to.be.empty;
expect(querySnap.docChanges()).to.be.empty;
expect(querySnap.docChanges({ includeMetadataChanges: true })).to.be.empty;
}
2 changes: 1 addition & 1 deletion packages/firestore/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"dev": "rollup -c -w",
"lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'",
"lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'",
"prettier": "prettier --write '*.ts' '*.js' 'exp/**/*.ts' 'src/**/*.js' 'test/**/*.js' 'src/**/*.ts' 'test/**/*.ts'",
"prettier": "prettier --write '*.ts' '*.js' 'lite/**/*.ts' 'exp/**/*.ts' 'src/**/*.js' 'test/**/*.js' 'src/**/*.ts' 'test/**/*.ts'",
"pregendeps:exp": "yarn build:exp",
"gendeps:exp": "../../scripts/exp/extract-deps.sh --types ./exp/index.d.ts --bundle ./dist/exp/index.js --output ./exp/test/deps/dependencies.json",
"pretest:exp": "yarn build:exp",
Expand Down
Loading