mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
Add paginationMiddleware, getting warmer
This commit is contained in:
parent
316e3e287f
commit
e2ce9d0dab
8 changed files with 126 additions and 65 deletions
|
|
@ -4,7 +4,7 @@
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./mod.ts",
|
".": "./mod.ts",
|
||||||
"./middleware": "./middleware/mod.ts",
|
"./middleware": "./middleware/mod.ts",
|
||||||
"./pagination": "./pagination.ts",
|
"./pagination": "./pagination/mod.ts",
|
||||||
"./routes": "./routes/mod.ts",
|
"./routes": "./routes/mod.ts",
|
||||||
"./schema": "./schema.ts",
|
"./schema": "./schema.ts",
|
||||||
"./views": "./views/mod.ts"
|
"./views": "./views/mod.ts"
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
|
export { paginationMiddleware } from './paginationMiddleware.ts';
|
||||||
export { requireVar } from './requireVar.ts';
|
export { requireVar } from './requireVar.ts';
|
||||||
export { userMiddleware } from './userMiddleware.ts';
|
export { userMiddleware } from './userMiddleware.ts';
|
||||||
|
|
|
||||||
80
packages/api/middleware/paginationMiddleware.ts
Normal file
80
packages/api/middleware/paginationMiddleware.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { paginated, paginatedList, paginationSchema } from '@ditto/api/pagination';
|
||||||
|
|
||||||
|
import type { NostrEvent } from '@nostrify/nostrify';
|
||||||
|
import type { DittoMiddleware } from '../DittoMiddleware.ts';
|
||||||
|
|
||||||
|
interface Pagination {
|
||||||
|
since?: number;
|
||||||
|
until?: number;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListPagination {
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type HeaderRecord = Record<string, string | string[]>;
|
||||||
|
type PaginateFn = (events: NostrEvent[], body: object | unknown[], headers?: HeaderRecord) => Response;
|
||||||
|
type ListPaginateFn = (params: ListPagination, body: object | unknown[], headers?: HeaderRecord) => Response;
|
||||||
|
|
||||||
|
/** Fixes compatibility with Mastodon apps by that don't use `Link` headers. */
|
||||||
|
// @ts-ignore Types are right.
|
||||||
|
export function paginationMiddleware(): DittoMiddleware<{ pagination: Pagination; paginate: PaginateFn }>;
|
||||||
|
export function paginationMiddleware(
|
||||||
|
type: 'list',
|
||||||
|
): DittoMiddleware<{ pagination: ListPagination; paginate: ListPaginateFn }>;
|
||||||
|
export function paginationMiddleware(
|
||||||
|
type?: string,
|
||||||
|
): DittoMiddleware<{ pagination?: Pagination | ListPagination; paginate: PaginateFn | ListPaginateFn }> {
|
||||||
|
return async (c, next) => {
|
||||||
|
const { relay } = c.var;
|
||||||
|
|
||||||
|
const pagination = paginationSchema.parse(c.req.query());
|
||||||
|
|
||||||
|
const {
|
||||||
|
max_id: maxId,
|
||||||
|
min_id: minId,
|
||||||
|
since,
|
||||||
|
until,
|
||||||
|
} = pagination;
|
||||||
|
|
||||||
|
if ((maxId && !until) || (minId && !since)) {
|
||||||
|
const ids: string[] = [];
|
||||||
|
|
||||||
|
if (maxId) ids.push(maxId);
|
||||||
|
if (minId) ids.push(minId);
|
||||||
|
|
||||||
|
if (ids.length) {
|
||||||
|
const events = await relay.query(
|
||||||
|
[{ ids, limit: ids.length }],
|
||||||
|
{ signal: c.req.raw.signal },
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const event of events) {
|
||||||
|
if (!until && maxId === event.id) pagination.until = event.created_at;
|
||||||
|
if (!since && minId === event.id) pagination.since = event.created_at;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'list') {
|
||||||
|
c.set('pagination', {
|
||||||
|
limit: pagination.limit,
|
||||||
|
offset: pagination.offset,
|
||||||
|
});
|
||||||
|
const fn: ListPaginateFn = (params, body, headers) => paginatedList(c, params, body, headers);
|
||||||
|
c.set('paginate', fn);
|
||||||
|
} else {
|
||||||
|
c.set('pagination', {
|
||||||
|
since: pagination.since,
|
||||||
|
until: pagination.until,
|
||||||
|
limit: pagination.limit,
|
||||||
|
});
|
||||||
|
const fn: PaginateFn = (events, body, headers) => paginated(c, events, body, headers);
|
||||||
|
c.set('paginate', fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { buildLinkHeader, buildListLinkHeader } from './link-header.ts';
|
||||||
|
export { paginated, paginatedList } from './paginate.ts';
|
||||||
|
export { paginationSchema } from './schema.ts';
|
||||||
23
packages/api/pagination/schema.test.ts
Normal file
23
packages/api/pagination/schema.test.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { assertEquals } from '@std/assert';
|
||||||
|
|
||||||
|
import { paginationSchema } from './schema.ts';
|
||||||
|
|
||||||
|
Deno.test('paginationSchema', () => {
|
||||||
|
const pagination = paginationSchema.parse({
|
||||||
|
limit: '10',
|
||||||
|
offset: '20',
|
||||||
|
max_id: '1',
|
||||||
|
min_id: '2',
|
||||||
|
since: '3',
|
||||||
|
until: '4',
|
||||||
|
});
|
||||||
|
|
||||||
|
assertEquals(pagination, {
|
||||||
|
limit: 10,
|
||||||
|
offset: 20,
|
||||||
|
max_id: '1',
|
||||||
|
min_id: '2',
|
||||||
|
since: 3,
|
||||||
|
until: 4,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,18 +1,21 @@
|
||||||
import { DittoRoute } from '@ditto/api';
|
import { DittoRoute } from '@ditto/api';
|
||||||
import { userMiddleware } from '@ditto/api/middleware';
|
import { paginationMiddleware, userMiddleware } from '@ditto/api/middleware';
|
||||||
import { booleanParamSchema, languageSchema } from '@ditto/api/schema';
|
import { booleanParamSchema, languageSchema } from '@ditto/api/schema';
|
||||||
|
import { getTagSet } from '@ditto/utils/tags';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import type { Context } from '@hono/hono';
|
||||||
import type { NostrFilter } from '@nostrify/nostrify';
|
import type { NostrFilter } from '@nostrify/nostrify';
|
||||||
|
import type { DittoEnv } from '../DittoEnv.ts';
|
||||||
|
|
||||||
const route = new DittoRoute().use(userMiddleware({ privileged: false, required: true }));
|
const route = new DittoRoute().use(paginationMiddleware());
|
||||||
|
|
||||||
const homeQuerySchema = z.object({
|
const homeQuerySchema = z.object({
|
||||||
exclude_replies: booleanParamSchema.optional(),
|
exclude_replies: booleanParamSchema.optional(),
|
||||||
only_media: booleanParamSchema.optional(),
|
only_media: booleanParamSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
route.get('/home', async (c) => {
|
route.get('/home', userMiddleware({ privileged: false, required: true }), async (c) => {
|
||||||
const { user, pagination } = c.var;
|
const { user, pagination } = c.var;
|
||||||
|
|
||||||
const pubkey = await user?.signer.getPublicKey()!;
|
const pubkey = await user?.signer.getPublicKey()!;
|
||||||
|
|
@ -50,7 +53,7 @@ const publicQuerySchema = z.object({
|
||||||
language: languageSchema.optional(),
|
language: languageSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const publicTimelineController: AppController = (c) => {
|
route.get('/public', (c) => {
|
||||||
const { conf, pagination } = c.var;
|
const { conf, pagination } = c.var;
|
||||||
const result = publicQuerySchema.safeParse(c.req.query());
|
const result = publicQuerySchema.safeParse(c.req.query());
|
||||||
|
|
||||||
|
|
@ -79,30 +82,29 @@ const publicTimelineController: AppController = (c) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderStatuses(c, [filter]);
|
return renderStatuses(c, [filter]);
|
||||||
};
|
});
|
||||||
|
|
||||||
const hashtagTimelineController: AppController = (c) => {
|
route.get('/tag/:hashtag', (c) => {
|
||||||
const { pagination } = c.var;
|
const { pagination } = c.var;
|
||||||
const hashtag = c.req.param('hashtag')!.toLowerCase();
|
const hashtag = c.req.param('hashtag').toLowerCase();
|
||||||
|
|
||||||
return renderStatuses(c, [{ kinds: [1, 20], '#t': [hashtag], ...pagination }]);
|
return renderStatuses(c, [{ kinds: [1, 20], '#t': [hashtag], ...pagination }]);
|
||||||
};
|
});
|
||||||
|
|
||||||
const suggestedTimelineController: AppController = async (c) => {
|
route.get('/suggested', async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf, relay, pagination } = c.var;
|
||||||
const store = c.get('store');
|
|
||||||
const params = c.get('pagination');
|
|
||||||
|
|
||||||
const [follows] = await store.query(
|
const [follows] = await relay.query(
|
||||||
[{ kinds: [3], authors: [conf.pubkey], limit: 1 }],
|
[{ kinds: [3], authors: [conf.pubkey], limit: 1 }],
|
||||||
);
|
);
|
||||||
|
|
||||||
const authors = [...getTagSet(follows?.tags ?? [], 'p')];
|
const authors = [...getTagSet(follows?.tags ?? [], 'p')];
|
||||||
|
|
||||||
return renderStatuses(c, [{ authors, kinds: [1, 20], ...params }]);
|
return renderStatuses(c, [{ authors, kinds: [1, 20], ...pagination }]);
|
||||||
};
|
});
|
||||||
|
|
||||||
/** Render statuses for timelines. */
|
/** Render statuses for timelines. */
|
||||||
async function renderStatuses(c: AppContext, filters: NostrFilter[]) {
|
async function renderStatuses(c: Context<DittoEnv>, filters: NostrFilter[]) {
|
||||||
const { conf, store, user, signal } = c.var;
|
const { conf, store, user, signal } = c.var;
|
||||||
const opts = { signal, timeout: conf.db.timeouts.timelines };
|
const opts = { signal, timeout: conf.db.timeouts.timelines };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
import { AppMiddleware } from '@/app.ts';
|
|
||||||
import { paginationSchema } from '@/schemas/pagination.ts';
|
|
||||||
|
|
||||||
/** Fixes compatibility with Mastodon apps by that don't use `Link` headers. */
|
|
||||||
export const paginationMiddleware: AppMiddleware = async (c, next) => {
|
|
||||||
const { store } = c.var;
|
|
||||||
|
|
||||||
const pagination = paginationSchema.parse(c.req.query());
|
|
||||||
|
|
||||||
const {
|
|
||||||
max_id: maxId,
|
|
||||||
min_id: minId,
|
|
||||||
since,
|
|
||||||
until,
|
|
||||||
} = pagination;
|
|
||||||
|
|
||||||
if ((maxId && !until) || (minId && !since)) {
|
|
||||||
const ids: string[] = [];
|
|
||||||
|
|
||||||
if (maxId) ids.push(maxId);
|
|
||||||
if (minId) ids.push(minId);
|
|
||||||
|
|
||||||
if (ids.length) {
|
|
||||||
const events = await store.query(
|
|
||||||
[{ ids, limit: ids.length }],
|
|
||||||
{ signal: c.req.raw.signal },
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const event of events) {
|
|
||||||
if (!until && maxId === event.id) pagination.until = event.created_at;
|
|
||||||
if (!since && minId === event.id) pagination.since = event.created_at;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.set('pagination', {
|
|
||||||
since: pagination.since,
|
|
||||||
until: pagination.until,
|
|
||||||
limit: pagination.limit,
|
|
||||||
});
|
|
||||||
|
|
||||||
c.set('listPagination', {
|
|
||||||
limit: pagination.limit,
|
|
||||||
offset: pagination.offset,
|
|
||||||
});
|
|
||||||
|
|
||||||
await next();
|
|
||||||
};
|
|
||||||
Loading…
Add table
Reference in a new issue