diff --git a/examples/typescript/cypress/e2e/Firestore.cy.ts b/examples/typescript/cypress/e2e/Firestore.cy.ts index 2a6a28ee..ae460db0 100644 --- a/examples/typescript/cypress/e2e/Firestore.cy.ts +++ b/examples/typescript/cypress/e2e/Firestore.cy.ts @@ -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', () => { diff --git a/package.json b/package.json index 0f59205c..a45c60a9 100644 --- a/package.json +++ b/package.json @@ -130,7 +130,7 @@ "name": "ESM: plugin", "path": "lib-esm/index.js", "import": "{ plugin }", - "limit": "2.75kb", + "limit": "3kb", "webpack": false } ] diff --git a/src/attachCustomCommands.ts b/src/attachCustomCommands.ts index 3b629b6d..8358ca09 100644 --- a/src/attachCustomCommands.ts +++ b/src/attachCustomCommands.ts @@ -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. diff --git a/src/firebase-utils.ts b/src/firebase-utils.ts index 2b12913f..d7f76ce5 100644 --- a/src/firebase-utils.ts +++ b/src/firebase-utils.ts @@ -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 @@ -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, ): @@ -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') { @@ -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, + ); } } diff --git a/src/tasks.ts b/src/tasks.ts index 3cb750a2..1ae6e054 100644 --- a/src/tasks.ts +++ b/src/tasks.ts @@ -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 { @@ -233,7 +233,7 @@ export async function callFirestore( if (action === 'get') { const snap = await ( slashPathToFirestoreRef( - adminInstance.firestore(), + adminInstance.firestore, actionPath, options, ) as any @@ -255,7 +255,7 @@ export async function callFirestore( const deletePromise = isDocPath(actionPath) ? ( slashPathToFirestoreRef( - adminInstance.firestore(), + adminInstance.firestore, actionPath, options, ) as FirebaseFirestore.DocumentReference @@ -263,7 +263,7 @@ export async function callFirestore( : deleteCollection( adminInstance.firestore(), slashPathToFirestoreRef( - adminInstance.firestore(), + adminInstance.firestore, actionPath, options, ) as @@ -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 diff --git a/test/unit/tasks.spec.ts b/test/unit/tasks.spec.ts index 695b50b0..6200272c 100644 --- a/test/unit/tasks.spec.ts +++ b/test/unit/tasks.spec.ts @@ -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';