diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 17d3eb0..c7a627b 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - node-version: [12.x, 14.x, 16.x, 18.x] + node-version: [12.x, 14.x, 16.x, 18.x, 20.x, 22.x, 24.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: diff --git a/src/driver/Firestore/InProcessFirestore.ts b/src/driver/Firestore/InProcessFirestore.ts index e546a0b..5532a31 100644 --- a/src/driver/Firestore/InProcessFirestore.ts +++ b/src/driver/Firestore/InProcessFirestore.ts @@ -388,7 +388,21 @@ export class InProcessFirestoreQuery implements IFirestoreQuery { } offset(offset: number): IFirestoreQuery { - throw new Error("InProcessFirestoreQuery.offset not implemented") + const newQuery: IQueryBuilder = _.cloneDeep(this.query) + + newQuery.transforms.push((collection) => { + if (_.isObject(collection)) { + collection = Object.keys(collection) + .slice(offset) + .reduce((obj: { [key: string]: any }, key: string) => { + obj[key] = collection[key] + return obj + }, {}) + } + return collection + }) + + return new InProcessFirestoreQuery(this.firestore, this.path, newQuery) } limit(limit: number): IFirestoreQuery { diff --git a/tests/driver/Firestore/InProcessFirestore.collectionGroup.offset.test.ts b/tests/driver/Firestore/InProcessFirestore.collectionGroup.offset.test.ts new file mode 100644 index 0000000..c5eb4ed --- /dev/null +++ b/tests/driver/Firestore/InProcessFirestore.collectionGroup.offset.test.ts @@ -0,0 +1,76 @@ +import { InProcessFirestore } from "../../../src/driver/Firestore/InProcessFirestore" + +describe("In-process Firestore offset queries on a collectionGroup", () => { + const db = new InProcessFirestore() + + beforeEach(() => { + db.resetStorage() + }) + + test("offset less than total size", async () => { + // Given some data in a collectionGroup; + await db.collection("zooA/animals").add({ name: "cat" }) + await db.collection("zooA/animals").add({ name: "ant" }) + await db.collection("zooB/animals").add({ name: "bee" }) + + // When we offset by one item with deterministic ordering; + const result = await db + .collectionGroup("animals") + .orderBy("name") + .offset(1) + .get() + + // Then we should get the remaining items. + expect(result.size).toBe(2) + expect(result.empty).toBeFalsy() + expect(result.docs).toHaveLength(2) + expect(result.docs.map((doc) => doc.data())).toEqual([ + { name: "bee" }, + { name: "cat" }, + ]) + }) + + test("offset equal to total size", async () => { + // Given some data in a collectionGroup; + await db.collection("zooA/animals").add({ name: "cat" }) + await db.collection("zooA/animals").add({ name: "ant" }) + await db.collection("zooB/animals").add({ name: "bee" }) + + // When we offset by the total number of rows; + const result = await db + .collectionGroup("animals") + .orderBy("name") + .offset(3) + .get() + + // Then the result should be empty. + expect(result.size).toBe(0) + expect(result.empty).toBeTruthy() + expect(result.docs).toHaveLength(0) + }) + + test("offset composes with limit for pagination", async () => { + // Given some data in a collectionGroup; + await db.collection("zooA/animals").add({ name: "dog" }) + await db.collection("zooA/animals").add({ name: "cat" }) + await db.collection("zooB/animals").add({ name: "ant" }) + await db.collection("zooB/animals").add({ name: "bee" }) + + // When we request a page using offset + limit; + const result = await db + .collectionGroup("animals") + .orderBy("name") + .offset(1) + .limit(2) + .get() + + // Then we should get the expected page. + expect(result.size).toBe(2) + expect(result.empty).toBeFalsy() + expect(result.docs).toHaveLength(2) + expect(result.docs.map((doc) => doc.data())).toEqual([ + { name: "bee" }, + { name: "cat" }, + ]) + }) +}) diff --git a/tests/driver/Firestore/InProcessFirestore.offset.test.ts b/tests/driver/Firestore/InProcessFirestore.offset.test.ts new file mode 100644 index 0000000..e5b9eb7 --- /dev/null +++ b/tests/driver/Firestore/InProcessFirestore.offset.test.ts @@ -0,0 +1,76 @@ +import { InProcessFirestore } from "../../../src/driver/Firestore/InProcessFirestore" + +describe("In-process Firestore offset queries", () => { + const db = new InProcessFirestore() + + beforeEach(() => { + db.resetStorage() + }) + + test("offset less than total size", async () => { + // Given some data in a collection; + await db.collection("animals").add({ name: "cat" }) + await db.collection("animals").add({ name: "ant" }) + await db.collection("animals").add({ name: "bee" }) + + // When we offset by one item with deterministic ordering; + const result = await db + .collection("animals") + .orderBy("name") + .offset(1) + .get() + + // Then we should get the remaining items. + expect(result.size).toBe(2) + expect(result.empty).toBeFalsy() + expect(result.docs).toHaveLength(2) + expect(result.docs.map((doc) => doc.data())).toEqual([ + { name: "bee" }, + { name: "cat" }, + ]) + }) + + test("offset equal to total size", async () => { + // Given some data in a collection; + await db.collection("animals").add({ name: "cat" }) + await db.collection("animals").add({ name: "ant" }) + await db.collection("animals").add({ name: "bee" }) + + // When we offset by the total number of rows; + const result = await db + .collection("animals") + .orderBy("name") + .offset(3) + .get() + + // Then the result should be empty. + expect(result.size).toBe(0) + expect(result.empty).toBeTruthy() + expect(result.docs).toHaveLength(0) + }) + + test("offset composes with limit for pagination", async () => { + // Given some data in a collection; + await db.collection("animals").add({ name: "dog" }) + await db.collection("animals").add({ name: "cat" }) + await db.collection("animals").add({ name: "ant" }) + await db.collection("animals").add({ name: "bee" }) + + // When we request a page using offset + limit; + const result = await db + .collection("animals") + .orderBy("name") + .offset(1) + .limit(2) + .get() + + // Then we should get the expected page. + expect(result.size).toBe(2) + expect(result.empty).toBeFalsy() + expect(result.docs).toHaveLength(2) + expect(result.docs.map((doc) => doc.data())).toEqual([ + { name: "bee" }, + { name: "cat" }, + ]) + }) +})