Skip to content

Commit

Permalink
fix(firestore): correctly handle timestamp in where (#747)
Browse files Browse the repository at this point in the history
  • Loading branch information
prescottprue authored Dec 31, 2022
1 parent a72cdb2 commit 541bbf2
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 17 deletions.
14 changes: 14 additions & 0 deletions examples/typescript/cypress/e2e/Firestore.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ describe('callFirestore', () => {
expect(results).to.have.length(1);
});
})

it('should query with where with timestamp', () => {
const uniqueName = 'Test Where'
const futureDate = new Date()
futureDate.setDate(futureDate.getDate() - 2)
cy.callFirestore('add', 'projects', { name: uniqueName, createdAt: Timestamp.fromDate(futureDate) })
cy.callFirestore('get', 'projects', {
// where: ['createdAt', '<=', Timestamp.now()],
}).then((results) => {
cy.log('get respond', results);
expect(results).to.exist;
expect(results).to.have.length(1);
});
})
})

describe('set', () => {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@
"name": "ESM: plugin",
"path": "lib-esm/index.js",
"import": "{ plugin }",
"limit": "2.75kb",
"limit": "3kb",
"webpack": false
}
]
Expand Down
2 changes: 1 addition & 1 deletion src/attachCustomCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface FixtureData {
[k: string]: any;
}

type WhereOptions = [string, FirebaseFirestore.WhereFilterOp, any];
export type WhereOptions = [string, FirebaseFirestore.WhereFilterOp, any];

/**
* Options for callFirestore custom Cypress command.
Expand Down
53 changes: 44 additions & 9 deletions src/firebase-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { AppOptions, app, firestore, credential } from 'firebase-admin';
import { getServiceAccount } from './node-utils';
import { CallFirestoreOptions } from './attachCustomCommands';
import { CallFirestoreOptions, WhereOptions } from './attachCustomCommands';
import { convertValueToTimestampOrGeoPointIfPossible } from './tasks';

/**
* Check whether a value is a string or not
Expand Down Expand Up @@ -172,16 +173,37 @@ export function isDocPath(slashPath: string): boolean {
return !(slashPath.replace(/^\/|\/$/g, '').split('/').length % 2);
}

/**
*
* @param ref
* @param whereSetting
* @param firestoreStatics
*/
export function applyWhere(
ref: firestore.CollectionReference | firestore.Query,
whereSetting: WhereOptions,
firestoreStatics: app.App['firestore'],
): firestore.Query {
const [param, filterOp, val] = whereSetting as WhereOptions;
return ref.where(
param,
filterOp,
convertValueToTimestampOrGeoPointIfPossible(
val,
firestoreStatics as typeof firestore,
),
);
}

/**
* Convert slash path to Firestore reference
* @param firestoreInstance - Instance on which to
* create ref
* @param firestoreStatics - Firestore instance statics (invoking gets instance)
* @param slashPath - Path to convert into firestore reference
* @param options - Options object
* @returns Ref at slash path
*/
export function slashPathToFirestoreRef(
firestoreInstance: any,
firestoreStatics: app.App['firestore'],
slashPath: string,
options?: CallFirestoreOptions,
):
Expand All @@ -192,9 +214,13 @@ export function slashPathToFirestoreRef(
throw new Error('Path is required to make Firestore Reference');
}

let ref = isDocPath(slashPath)
? firestoreInstance.doc(slashPath)
: firestoreInstance.collection(slashPath);
const firestoreInstance = firestoreStatics();
if (isDocPath(slashPath)) {
return firestoreInstance.doc(slashPath);
}

let ref: firestore.CollectionReference | firestore.Query =
firestoreInstance.collection(slashPath);

// Apply orderBy to query if it exists
if (options?.orderBy && typeof ref.orderBy === 'function') {
Expand All @@ -211,9 +237,18 @@ export function slashPathToFirestoreRef(
typeof ref.where === 'function'
) {
if (Array.isArray(options.where[0])) {
ref = ref.where(...options.where[0]).where(...options.where[1]);
const [where1, where2] = options.where as WhereOptions[];
ref = applyWhere(
applyWhere(ref, where1, options.statics || firestoreStatics),
where2,
options.statics || firestoreStatics,
);
} else {
ref = ref.where(...options.where);
ref = applyWhere(
ref,
options.where as WhereOptions,
options.statics || firestoreStatics,
);
}
}

Expand Down
12 changes: 6 additions & 6 deletions src/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ function getAuth(
* @param firestoreStatics - Statics from firestore instance
* @returns Value converted into timestamp object if possible
*/
function convertValueToTimestampOrGeoPointIfPossible(
export function convertValueToTimestampOrGeoPointIfPossible(
dataVal: any,
firestoreStatics: typeof firestore,
): firestore.FieldValue {
Expand Down Expand Up @@ -233,7 +233,7 @@ export async function callFirestore(
if (action === 'get') {
const snap = await (
slashPathToFirestoreRef(
adminInstance.firestore(),
adminInstance.firestore,
actionPath,
options,
) as any
Expand All @@ -255,15 +255,15 @@ export async function callFirestore(
const deletePromise = isDocPath(actionPath)
? (
slashPathToFirestoreRef(
adminInstance.firestore(),
adminInstance.firestore,
actionPath,
options,
) as FirebaseFirestore.DocumentReference
).delete()
: deleteCollection(
adminInstance.firestore(),
slashPathToFirestoreRef(
adminInstance.firestore(),
adminInstance.firestore,
actionPath,
options,
) as
Expand Down Expand Up @@ -299,10 +299,10 @@ export async function callFirestore(
: (undefined as any),
);
}
// "update" action
// "update" and "add" action
return (
slashPathToFirestoreRef(
adminInstance.firestore(),
adminInstance.firestore,
actionPath,
options,
) as any
Expand Down
50 changes: 50 additions & 0 deletions test/unit/tasks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,56 @@ describe('tasks', () => {
expect(result[0]).to.have.property('name', secondProject.name);
});

it('supports where with timestamp', async () => {
const projectId = 'one-where-timestamp';
const currentDate = new Date();
await projectsFirestoreRef
.doc(projectId)
.set({ dateField: admin.firestore.Timestamp.fromDate(currentDate) });
const result = await tasks.callFirestore(
adminApp,
'get',
PROJECTS_COLLECTION,
{
statics: { Timestamp: admin.firestore.Timestamp } as any,
where: [
'dateField',
'==',
admin.firestore.Timestamp.fromDate(currentDate),
],
},
);
expect(result[0]).to.have.property('id', projectId);
});

it('supports multiple wheres with timestamps', async () => {
const projectId = 'multi-where-timestamp';
const pastDate = new Date();
pastDate.setDate(pastDate.getDate() - 2);
const fieldName = 'anotherField';
await projectsFirestoreRef
.doc(projectId)
.set({ [fieldName]: admin.firestore.Timestamp.fromDate(pastDate) });
const result = await tasks.callFirestore(
adminApp,
'get',
PROJECTS_COLLECTION,
{
statics: { Timestamp: admin.firestore.Timestamp } as any,
where: [
[
fieldName,
'>=',
admin.firestore.Timestamp.fromDate(new Date('1/1/21')),
],
[fieldName, '<=', admin.firestore.Timestamp.fromDate(new Date())],
],
},
);
// TODO: Come up with a more stable way to verify here - data from other tests can cause fails
expect(result[0]).to.have.property('id', projectId);
});

it('supports multi-where', async () => {
await projectFirestoreRef.set(testProject);
const secondProjectId = 'some';
Expand Down

0 comments on commit 541bbf2

Please sign in to comment.