|
| 1 | +import type { Params } from '@feathersjs/feathers' |
| 2 | +import { addToQuery } from '../utils/add-to-query/add-to-query.util.js' |
| 3 | +import { isEqual } from './is-equal.js' |
| 4 | + |
| 5 | +type PaginateOption = { default?: number; max?: number } |
| 6 | + |
| 7 | +/** What `iterateFind` and `chunkFind` take - documented there. */ |
| 8 | +export type FindPagesOptions<P extends Params = Params> = { |
| 9 | + params?: P & { paginate?: PaginateOption } |
| 10 | + keyset?: boolean |
| 11 | +} |
| 12 | + |
| 13 | +/** |
| 14 | + * The pages of a `find`, one after the other - shared by `iterateFind` and |
| 15 | + * `chunkFind`. Pages by `$skip`, or by the id with the `keyset` option. |
| 16 | + */ |
| 17 | +export async function* findPages( |
| 18 | + service: any, |
| 19 | + options: FindPagesOptions | undefined, |
| 20 | + defaultLimit?: number, |
| 21 | +): AsyncGenerator<any[], void, unknown> { |
| 22 | + if (options?.keyset) { |
| 23 | + yield* keysetPages(service, options) |
| 24 | + return |
| 25 | + } |
| 26 | + |
| 27 | + const params = { |
| 28 | + ...options?.params, |
| 29 | + query: { |
| 30 | + ...(options?.params?.query ?? {}), |
| 31 | + $limit: options?.params?.query?.$limit ?? defaultLimit, |
| 32 | + $skip: options?.params?.query?.$skip ?? 0, |
| 33 | + }, |
| 34 | + paginate: { |
| 35 | + default: options?.params?.paginate?.default ?? 10, |
| 36 | + max: options?.params?.paginate?.max ?? 100, |
| 37 | + }, |
| 38 | + } |
| 39 | + |
| 40 | + let $skip = params.query.$skip |
| 41 | + let result |
| 42 | + |
| 43 | + do { |
| 44 | + // a fresh params object per page: the service may hold on to the last one |
| 45 | + result = await service.find({ |
| 46 | + ...params, |
| 47 | + query: { ...params.query, $skip }, |
| 48 | + }) |
| 49 | + |
| 50 | + // Guard against an infinite loop: an empty page never advances $skip, so |
| 51 | + // `total > $skip` could stay true forever (e.g. $limit:0, or a stale total |
| 52 | + // when items are concurrently removed / filtered out by hooks). |
| 53 | + if (!result.data.length) { |
| 54 | + break |
| 55 | + } |
| 56 | + |
| 57 | + yield result.data |
| 58 | + |
| 59 | + $skip += result.data.length |
| 60 | + } while (result.total > $skip) |
| 61 | +} |
| 62 | + |
| 63 | +async function* keysetPages( |
| 64 | + service: any, |
| 65 | + options: FindPagesOptions, |
| 66 | +): AsyncGenerator<any[], void, unknown> { |
| 67 | + const idField: string = service.id ?? 'id' |
| 68 | + const { $sort, $limit, $skip, $select, ...query } = |
| 69 | + options.params?.query ?? {} |
| 70 | + |
| 71 | + if ($sort && Object.keys($sort).length) { |
| 72 | + throw new Error( |
| 73 | + `keyset pagination sorts by '${idField}', so it can't take a $sort`, |
| 74 | + ) |
| 75 | + } |
| 76 | + |
| 77 | + const { paginate, ...params } = options.params ?? {} |
| 78 | + const limit = Math.min( |
| 79 | + $limit ?? paginate?.default ?? 10, |
| 80 | + paginate?.max ?? 100, |
| 81 | + ) |
| 82 | + const select = |
| 83 | + Array.isArray($select) && !$select.includes(idField) |
| 84 | + ? [...$select, idField] |
| 85 | + : $select |
| 86 | + |
| 87 | + let skip: number | undefined = $skip |
| 88 | + let after: { id: unknown } | undefined |
| 89 | + |
| 90 | + while (true) { |
| 91 | + let pageQuery: Record<string, any> = { |
| 92 | + ...query, |
| 93 | + $sort: { [idField]: 1 }, |
| 94 | + $limit: limit, |
| 95 | + ...(skip !== undefined && { $skip: skip }), |
| 96 | + ...(select !== undefined && { $select: select }), |
| 97 | + } |
| 98 | + if (after) { |
| 99 | + // through `addToQuery`, so a condition on the id in `query` is kept |
| 100 | + pageQuery = addToQuery(pageQuery, { [idField]: { $gt: after.id } }) |
| 101 | + } |
| 102 | + |
| 103 | + const page: any[] = await service.find({ |
| 104 | + ...params, |
| 105 | + query: pageQuery, |
| 106 | + paginate: false, |
| 107 | + }) |
| 108 | + |
| 109 | + if (!page.length) { |
| 110 | + return |
| 111 | + } |
| 112 | + |
| 113 | + // read before the page is handed out: the loop may change its items |
| 114 | + const lastId = page[page.length - 1]?.[idField] |
| 115 | + if (lastId == null || (after && isEqual(lastId, after.id))) { |
| 116 | + throw new Error( |
| 117 | + `keyset pagination made no progress: the items need their '${idField}', sorted by \`$sort\``, |
| 118 | + ) |
| 119 | + } |
| 120 | + |
| 121 | + yield page |
| 122 | + |
| 123 | + after = { id: lastId } |
| 124 | + skip = undefined |
| 125 | + } |
| 126 | +} |
0 commit comments