Skip to content

Commit aae83e7

Browse files
author
Bobby
authored
feat: pagination support #20
feat: pagination support
2 parents a9b0a87 + 41ce436 commit aae83e7

File tree

4 files changed

+336
-128
lines changed

4 files changed

+336
-128
lines changed

src/reducers/cacheReducer.js

+97-13
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
isMatch,
2020
get,
2121
isEqual,
22+
takeRight,
2223
} from 'lodash';
2324
import { actionTypes } from '../constants';
2425
import { getBaseQueryName } from '../utils/query';
@@ -178,10 +179,11 @@ const orderTransducer = (order) => {
178179
const orders = isFlat ? [order] : order;
179180
const [fields, direction] = zip(
180181
...orders.map(([field, dir]) => [
181-
(data) =>
182-
typeof data[field] === 'string'
183-
? data[field].toLowerCase()
184-
: data[field],
182+
(data) => {
183+
if (typeof data[field] === 'string') return data[field].toLowerCase();
184+
if (isTimestamp(data[field])) return data[field].seconds;
185+
return data[field];
186+
},
185187
dir || 'asc',
186188
]),
187189
);
@@ -195,7 +197,13 @@ const orderTransducer = (order) => {
195197
* limit from the firestore query
196198
* @returns {xFormLimiter} - transducer
197199
*/
198-
const limitTransducer = (limit) => ([arr] = []) => [take(arr, limit)];
200+
const limitTransducer = ({ limit, endAt, endBefore }) => {
201+
if (!limit) return null;
202+
const fromRight = (endAt || endBefore) !== undefined;
203+
return fromRight
204+
? ([arr] = []) => [takeRight(arr, limit)]
205+
: ([arr] = []) => [take(arr, limit)];
206+
};
199207

200208
/**
201209
* @name filterTransducers
@@ -214,6 +222,7 @@ const filterTransducers = (where) => {
214222
: PROCESSES[op] || (() => true);
215223
return partialRight(map, (collection) =>
216224
filter(Object.values(collection || {}), (doc) => {
225+
if (!doc) return false;
217226
let value;
218227
if (field === '__name__') {
219228
value = doc.id;
@@ -232,6 +241,72 @@ const filterTransducers = (where) => {
232241
);
233242
});
234243
};
244+
245+
/**
246+
* @name paginateTransducers
247+
* @param {RRFQuery} query - Firestore query
248+
* @param {Boolean} isOptimisticWrite - includes optimistic data
249+
* @typedef {Function} xFormFilter - in optimistic reads and overrides
250+
* the reducer needs to take all documents and make a best effort to
251+
* filter down the document based on a cursor.
252+
* @returns {xFormFilter} - transducer
253+
*/
254+
const paginateTransducers = (query, isOptimisticWrite = false) => {
255+
const { orderBy: order, startAt, startAfter, endAt, endBefore, via } = query;
256+
const isOptimisticRead = via === undefined;
257+
if (!(isOptimisticRead || isOptimisticWrite)) return null;
258+
259+
const start = startAt || startAfter;
260+
const end = endAt || endBefore;
261+
const isAfter = startAfter !== undefined;
262+
const isBefore = endBefore !== undefined;
263+
if (start === undefined && end === undefined) return null;
264+
265+
const isFlat = typeof order[0] === 'string';
266+
const orders = isFlat ? [order] : order;
267+
const isPaginateMatched = (doc, at, before = false, after = false) =>
268+
orders.find(([field, sort = 'asc'], idx) => {
269+
const value = Array.isArray(at) ? at[idx] : at;
270+
if (value === undefined) return false;
271+
272+
// TODO: add support for document refs
273+
const proc = isTimestamp(doc[field]) ? PROCESSES_TIMESTAMP : PROCESSES;
274+
let compare = process['=='];
275+
if (startAt || endAt) compare = proc[sort === 'desc' ? '<=' : '>='];
276+
if (startAfter || endBefore) compare = proc[sort === 'desc' ? '<' : '>'];
277+
278+
const isMatched = compare(doc[field], value);
279+
if (isMatched) {
280+
return true;
281+
}
282+
}) !== undefined;
283+
284+
return partialRight(map, (docs) => {
285+
const results = [];
286+
let started = start === undefined;
287+
288+
docs.forEach((doc) => {
289+
if (!started && start) {
290+
if (isPaginateMatched(doc, start, undefined, isAfter)) {
291+
started = true;
292+
}
293+
}
294+
295+
if (started && end) {
296+
if (isPaginateMatched(doc, end, isBefore, undefined)) {
297+
started = false;
298+
}
299+
}
300+
301+
if (started) {
302+
results.push(doc);
303+
}
304+
});
305+
306+
return results;
307+
});
308+
};
309+
235310
/**
236311
* @name populateTransducer
237312
* @param {string} collection - path to collection in Firestore
@@ -318,13 +393,12 @@ function buildTransducer(overrides, query) {
318393
collection,
319394
where,
320395
orderBy: order,
321-
limit,
322396
ordered,
323397
fields,
324398
populates,
325399
} = query;
326400

327-
const useOverrides =
401+
const isOptimistic =
328402
ordered === undefined ||
329403
Object.keys((overrides || {})[collection] || {}).length > 0;
330404

@@ -335,17 +409,26 @@ function buildTransducer(overrides, query) {
335409
const xfGetDoc = getDocumentTransducer((ordered || []).map(([__, id]) => id));
336410
const xfFields = !fields ? null : fieldsTransducer(fields);
337411

338-
const xfApplyOverrides = !useOverrides
412+
const xfApplyOverrides = !isOptimistic
339413
? null
340414
: overridesTransducers(overrides || { [collection]: [] }, collection);
341415
const xfFilter =
342-
!useOverrides || filterTransducers(!where ? ['', '*', ''] : where);
343-
const xfOrder = !useOverrides || !order ? null : orderTransducer(order);
344-
const xfLimit = !limit ? null : limitTransducer(limit);
416+
!isOptimistic || filterTransducers(!where ? ['', '*', ''] : where);
417+
const xfOrder = !isOptimistic || !order ? null : orderTransducer(order);
418+
const xfPaginate = paginateTransducers(query, isOptimistic);
419+
const xfLimit = limitTransducer(query);
345420

346-
if (!useOverrides) {
421+
if (!isOptimistic) {
347422
return flow(
348-
compact([xfPopulate, xfGetCollection, xfGetDoc, xfLimit, xfFields]),
423+
compact([
424+
xfPopulate,
425+
xfGetCollection,
426+
xfGetDoc,
427+
xfOrder,
428+
xfPaginate,
429+
xfLimit,
430+
xfFields,
431+
]),
349432
);
350433
}
351434

@@ -358,6 +441,7 @@ function buildTransducer(overrides, query) {
358441
partialRight(map, (db) => finishDraft(db)),
359442
...xfFilter,
360443
xfOrder,
444+
xfPaginate,
361445
xfLimit,
362446
xfFields,
363447
]),

src/utils/mutate.js

+10-38
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,26 @@
1-
import { chunk, cloneDeep, flatten, mapValues } from 'lodash';
1+
import { chunk, cloneDeep, flatten, isFunction, mapValues } from 'lodash';
22
import debug from 'debug';
33
import { firestoreRef } from './query';
44
import mark from './profiling';
55

66
const info = debug('rrf:mutate');
77

8-
/**
9-
* @param {object} firestore
10-
* @param {string} collection
11-
* @param {string} doc
12-
* @returns Boolean
13-
*/
148
const docRef = (firestore, collection, doc) =>
159
firestore.doc(`${collection}/${doc}`);
1610

17-
/**
18-
* @param object
19-
* @returns Promise
20-
*/
21-
async function promiseAllObject(object) {
22-
return Object.fromEntries(
11+
const promiseAllObject = async (object) =>
12+
Object.fromEntries(
2313
await Promise.all(
2414
Object.entries(object).map(([key, promise]) =>
2515
promise.then((value) => [key, value]),
2616
),
2717
),
2818
);
29-
}
3019

31-
/**
32-
* @param {object} operations
33-
* @returns Boolean
34-
*/
35-
function isBatchedWrite(operations) {
36-
return Array.isArray(operations);
37-
}
38-
39-
/**
40-
* @param {Mutation_v1 | Mutation_v2} read
41-
* @returns Boolean
42-
*/
43-
function isDocRead(read) {
44-
return read && typeof read.doc === 'string';
45-
}
46-
47-
/**
48-
* @param {Mutation_v1 | Mutation_v2} operations
49-
* @returns Boolean
50-
*/
51-
function isSingleWrite(operations) {
52-
return operations && typeof operations.collection === 'string';
53-
}
20+
const isBatchedWrite = (operations) => Array.isArray(operations);
21+
const isDocRead = ({ doc } = {}) => typeof doc === 'string';
22+
const isProviderRead = (read) => isFunction(read);
23+
const isSingleWrite = ({ collection } = {}) => typeof collection === 'string';
5424

5525
// ----- FieldValue support -----
5626

@@ -199,13 +169,15 @@ async function writeInTransaction(firebase, operations) {
199169

200170
const done = mark('mutate.writeInTransaction:reads');
201171
const readsPromised = mapValues(operations.reads, async (read) => {
172+
if (isProviderRead(read)) return read();
173+
202174
if (isDocRead(read)) {
203175
const doc = firestoreRef(firebase, read);
204176
const snapshot = await getter(doc);
205177
return serialize(snapshot.exsits === false ? null : snapshot);
206178
}
207179

208-
// NOTE: Queries are not supported in Firestore Transactions (client-side)
180+
// else query (As of 7/2021, Firestore doesn't include queries in client-side transactions)
209181
const coll = firestoreRef(firebase, read);
210182
const snapshot = await coll.get();
211183
if (snapshot.docs.length === 0) return [];

0 commit comments

Comments
 (0)