From 4216a7931aad0bbe2f990e228592d84648641c22 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 29 Aug 2023 12:55:00 -0500 Subject: [PATCH] Add `paginated` helper function, DRY pagination code --- src/controllers/api/accounts.ts | 6 ++---- src/controllers/api/notifications.ts | 6 ++---- src/controllers/api/timelines.ts | 20 +++++++------------- src/utils/web.ts | 26 +++++++++++++++++--------- 4 files changed, 28 insertions(+), 30 deletions(-) diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index 92445622..d84382af 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -6,7 +6,7 @@ import { booleanParamSchema } from '@/schema.ts'; import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { toAccount, toRelationship, toStatus } from '@/transformers/nostr-to-mastoapi.ts'; import { eventDateComparator, isFollowing, lookupAccount } from '@/utils.ts'; -import { buildLinkHeader, paginationSchema, parseBody } from '@/utils/web.ts'; +import { paginated, paginationSchema, parseBody } from '@/utils/web.ts'; import { createEvent } from '@/utils/web.ts'; const createAccountController: AppController = (c) => { @@ -110,9 +110,7 @@ const accountStatusesController: AppController = async (c) => { } const statuses = await Promise.all(events.map(toStatus)); - - const link = buildLinkHeader(c.req.url, events); - return c.json(statuses, 200, link ? { link } : undefined); + return paginated(c, events, statuses); }; const fileSchema = z.custom((value) => value instanceof File); diff --git a/src/controllers/api/notifications.ts b/src/controllers/api/notifications.ts index 30c38597..dce75e4d 100644 --- a/src/controllers/api/notifications.ts +++ b/src/controllers/api/notifications.ts @@ -1,6 +1,6 @@ import { type AppController } from '@/app.ts'; import * as mixer from '@/mixer.ts'; -import { buildLinkHeader, paginationSchema } from '@/utils/web.ts'; +import { paginated, paginationSchema } from '@/utils/web.ts'; import { toNotification } from '@/transformers/nostr-to-mastoapi.ts'; import { Time } from '@/utils.ts'; @@ -14,9 +14,7 @@ const notificationsController: AppController = async (c) => { ); const statuses = await Promise.all(events.map(toNotification)); - - const link = buildLinkHeader(c.req.url, events); - return c.json(statuses, 200, link ? { link } : undefined); + return paginated(c, events, statuses); }; export { notificationsController }; diff --git a/src/controllers/api/timelines.ts b/src/controllers/api/timelines.ts index 1b7c52b8..6f43034a 100644 --- a/src/controllers/api/timelines.ts +++ b/src/controllers/api/timelines.ts @@ -3,7 +3,7 @@ import * as mixer from '@/mixer.ts'; import { getFeed, getPublicFeed } from '@/queries.ts'; import { booleanParamSchema } from '@/schema.ts'; import { toStatus } from '@/transformers/nostr-to-mastoapi.ts'; -import { buildLinkHeader, paginationSchema } from '@/utils/web.ts'; +import { paginated, paginationSchema } from '@/utils/web.ts'; import { Time } from '@/utils.ts'; import type { AppController } from '@/app.ts'; @@ -17,10 +17,8 @@ const homeTimelineController: AppController = async (c) => { return c.json([]); } - const statuses = (await Promise.all(events.map(toStatus))).filter(Boolean); - - const link = buildLinkHeader(c.req.url, events); - return c.json(statuses, 200, link ? { link } : undefined); + const statuses = await Promise.all(events.map(toStatus)); + return paginated(c, events, statuses); }; const publicQuerySchema = z.object({ @@ -36,10 +34,8 @@ const publicTimelineController: AppController = async (c) => { return c.json([]); } - const statuses = (await Promise.all(events.map(toStatus))).filter(Boolean); - - const link = buildLinkHeader(c.req.url, events); - return c.json(statuses, 200, link ? { link } : undefined); + const statuses = await Promise.all(events.map(toStatus)); + return paginated(c, events, statuses); }; const hashtagTimelineController: AppController = async (c) => { @@ -55,10 +51,8 @@ const hashtagTimelineController: AppController = async (c) => { return c.json([]); } - const statuses = (await Promise.all(events.map(toStatus))).filter(Boolean); - - const link = buildLinkHeader(c.req.url, events); - return c.json(statuses, 200, link ? { link } : undefined); + const statuses = await Promise.all(events.map(toStatus)); + return paginated(c, events, statuses); }; export { hashtagTimelineController, homeTimelineController, publicTimelineController }; diff --git a/src/utils/web.ts b/src/utils/web.ts index 7044d04a..6d895231 100644 --- a/src/utils/web.ts +++ b/src/utils/web.ts @@ -89,6 +89,22 @@ function buildLinkHeader(url: string, events: Event[]): string | undefined { return `<${next}>; rel="next", <${prev}>; rel="prev"`; } +type Entity = { id: string }; +type HeaderRecord = Record; + +/** Return results with pagination headers. */ +function paginated(c: AppContext, events: Event[], entities: (Entity | undefined)[], headers: HeaderRecord = {}) { + const link = buildLinkHeader(c.req.url, events); + + if (link) { + headers.link = link; + } + + // Filter out undefined entities. + const results = entities.filter((entity): entity is Entity => Boolean(entity)); + return c.json(results, 200, headers); +} + /** JSON-LD context. */ type LDContext = (string | Record>)[]; @@ -107,12 +123,4 @@ function activityJson(c: Context, object: T) { return response; } -export { - activityJson, - buildLinkHeader, - createAdminEvent, - createEvent, - type PaginationParams, - paginationSchema, - parseBody, -}; +export { activityJson, createAdminEvent, createEvent, paginated, type PaginationParams, paginationSchema, parseBody };