mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 05:49:16 +01:00
refactor(database): 🎨 Refactor note handling into its own class instead of separate functions
This commit is contained in:
parent
2998cb4deb
commit
9081036c6d
36 changed files with 1194 additions and 1215 deletions
4
packages/database-interface/main.ts
Normal file
4
packages/database-interface/main.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { Note } from "./note";
|
||||
import { Timeline } from "./timeline";
|
||||
|
||||
export { Note, Timeline };
|
||||
618
packages/database-interface/note.ts
Normal file
618
packages/database-interface/note.ts
Normal file
|
|
@ -0,0 +1,618 @@
|
|||
import {
|
||||
type InferInsertModel,
|
||||
type SQL,
|
||||
and,
|
||||
desc,
|
||||
eq,
|
||||
inArray,
|
||||
} from "drizzle-orm";
|
||||
import { htmlToText } from "html-to-text";
|
||||
import type * as Lysand from "lysand-types";
|
||||
import { createRegExp, exactly, global } from "magic-regexp";
|
||||
import {
|
||||
type Application,
|
||||
applicationToAPI,
|
||||
} from "~database/entities/Application";
|
||||
import {
|
||||
attachmentToAPI,
|
||||
attachmentToLysand,
|
||||
} from "~database/entities/Attachment";
|
||||
import {
|
||||
type EmojiWithInstance,
|
||||
emojiToAPI,
|
||||
emojiToLysand,
|
||||
parseEmojis,
|
||||
} from "~database/entities/Emoji";
|
||||
import { localObjectURI } from "~database/entities/Federation";
|
||||
import {
|
||||
type Status,
|
||||
type StatusWithRelations,
|
||||
contentToHtml,
|
||||
findFirstNote,
|
||||
findManyNotes,
|
||||
getStatusUri,
|
||||
} from "~database/entities/Status";
|
||||
import {
|
||||
type User,
|
||||
type UserWithRelations,
|
||||
type UserWithRelationsAndRelationships,
|
||||
findManyUsers,
|
||||
getUserUri,
|
||||
userToAPI,
|
||||
userToMention,
|
||||
} from "~database/entities/User";
|
||||
import { db } from "~drizzle/db";
|
||||
import {
|
||||
attachment,
|
||||
emojiToStatus,
|
||||
notification,
|
||||
status,
|
||||
statusToMentions,
|
||||
user,
|
||||
userRelations,
|
||||
} from "~drizzle/schema";
|
||||
import { config } from "~packages/config-manager";
|
||||
import type { Attachment as APIAttachment } from "~types/mastodon/attachment";
|
||||
import type { Status as APIStatus } from "~types/mastodon/status";
|
||||
|
||||
/**
|
||||
* Gives helpers to fetch notes from database in a nice format
|
||||
*/
|
||||
export class Note {
|
||||
private constructor(private status: StatusWithRelations) {}
|
||||
|
||||
static async fromId(id: string | null): Promise<Note | null> {
|
||||
if (!id) return null;
|
||||
|
||||
return await Note.fromSql(eq(status.id, id));
|
||||
}
|
||||
|
||||
static async fromIds(ids: string[]): Promise<Note[]> {
|
||||
return await Note.manyFromSql(inArray(status.id, ids));
|
||||
}
|
||||
|
||||
static async fromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(status.id),
|
||||
) {
|
||||
const found = await findFirstNote({
|
||||
where: sql,
|
||||
orderBy,
|
||||
});
|
||||
|
||||
if (!found) return null;
|
||||
return new Note(found);
|
||||
}
|
||||
|
||||
static async manyFromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(status.id),
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
) {
|
||||
const found = await findManyNotes({
|
||||
where: sql,
|
||||
orderBy,
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
|
||||
return found.map((s) => new Note(s));
|
||||
}
|
||||
|
||||
async getUsersToFederateTo() {
|
||||
// Mentioned users
|
||||
const mentionedUsers =
|
||||
this.getStatus().mentions.length > 0
|
||||
? await findManyUsers({
|
||||
where: (user, { and, isNotNull, inArray }) =>
|
||||
and(
|
||||
isNotNull(user.instanceId),
|
||||
inArray(
|
||||
user.id,
|
||||
this.getStatus().mentions.map(
|
||||
(mention) => mention.id,
|
||||
),
|
||||
),
|
||||
),
|
||||
})
|
||||
: [];
|
||||
|
||||
const usersThatCanSeePost = await findManyUsers({
|
||||
where: (user, { isNotNull }) => isNotNull(user.instanceId),
|
||||
with: {
|
||||
...userRelations,
|
||||
relationships: {
|
||||
where: (relationship, { eq, and }) =>
|
||||
and(
|
||||
eq(relationship.subjectId, user.id),
|
||||
eq(relationship.following, true),
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const fusedUsers = [...mentionedUsers, ...usersThatCanSeePost];
|
||||
|
||||
const deduplicatedUsersById = fusedUsers.filter(
|
||||
(user, index, self) =>
|
||||
index === self.findIndex((t) => t.id === user.id),
|
||||
);
|
||||
|
||||
return deduplicatedUsersById;
|
||||
}
|
||||
|
||||
static fromStatus(status: StatusWithRelations) {
|
||||
return new Note(status);
|
||||
}
|
||||
|
||||
static fromStatuses(statuses: StatusWithRelations[]) {
|
||||
return statuses.map((s) => new Note(s));
|
||||
}
|
||||
|
||||
isNull() {
|
||||
return this.status === null;
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
getAuthor() {
|
||||
return this.status.author;
|
||||
}
|
||||
|
||||
async getReplyChildren() {
|
||||
return await Note.manyFromSql(
|
||||
eq(status.inReplyToPostId, this.status.id),
|
||||
);
|
||||
}
|
||||
|
||||
async unpin(unpinner: User) {
|
||||
return await db
|
||||
.delete(statusToMentions)
|
||||
.where(
|
||||
and(
|
||||
eq(statusToMentions.statusId, this.status.id),
|
||||
eq(statusToMentions.userId, unpinner.id),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static async insert(values: InferInsertModel<typeof status>) {
|
||||
return (await db.insert(status).values(values).returning())[0];
|
||||
}
|
||||
|
||||
static async fromData(
|
||||
author: User,
|
||||
content: Lysand.ContentFormat,
|
||||
visibility: APIStatus["visibility"],
|
||||
is_sensitive: boolean,
|
||||
spoiler_text: string,
|
||||
emojis: EmojiWithInstance[],
|
||||
uri?: string,
|
||||
mentions?: UserWithRelations[],
|
||||
/** List of IDs of database Attachment objects */
|
||||
media_attachments?: string[],
|
||||
replyId?: string,
|
||||
quoteId?: string,
|
||||
application?: Application,
|
||||
): Promise<Note | null> {
|
||||
const htmlContent = await contentToHtml(content, mentions);
|
||||
|
||||
// Parse emojis and fuse with existing emojis
|
||||
let foundEmojis = emojis;
|
||||
|
||||
if (author.instanceId === null) {
|
||||
const parsedEmojis = await parseEmojis(htmlContent);
|
||||
// Fuse and deduplicate
|
||||
foundEmojis = [...emojis, ...parsedEmojis].filter(
|
||||
(emoji, index, self) =>
|
||||
index === self.findIndex((t) => t.id === emoji.id),
|
||||
);
|
||||
}
|
||||
|
||||
const newNote = await Note.insert({
|
||||
authorId: author.id,
|
||||
content: htmlContent,
|
||||
contentSource:
|
||||
content["text/plain"]?.content ||
|
||||
content["text/markdown"]?.content ||
|
||||
Object.entries(content)[0][1].content ||
|
||||
"",
|
||||
contentType: "text/html",
|
||||
visibility,
|
||||
sensitive: is_sensitive,
|
||||
spoilerText: spoiler_text,
|
||||
uri: uri || null,
|
||||
inReplyToPostId: replyId ?? null,
|
||||
quotingPostId: quoteId ?? null,
|
||||
applicationId: application?.id ?? null,
|
||||
});
|
||||
|
||||
// Connect emojis
|
||||
for (const emoji of foundEmojis) {
|
||||
await db
|
||||
.insert(emojiToStatus)
|
||||
.values({
|
||||
emojiId: emoji.id,
|
||||
statusId: newNote.id,
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
// Connect mentions
|
||||
for (const mention of mentions ?? []) {
|
||||
await db
|
||||
.insert(statusToMentions)
|
||||
.values({
|
||||
statusId: newNote.id,
|
||||
userId: mention.id,
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
// Set attachment parents
|
||||
if (media_attachments && media_attachments.length > 0) {
|
||||
await db
|
||||
.update(attachment)
|
||||
.set({
|
||||
statusId: newNote.id,
|
||||
})
|
||||
.where(inArray(attachment.id, media_attachments));
|
||||
}
|
||||
|
||||
// Send notifications for mentioned local users
|
||||
for (const mention of mentions ?? []) {
|
||||
if (mention.instanceId === null) {
|
||||
await db.insert(notification).values({
|
||||
accountId: author.id,
|
||||
notifiedId: mention.id,
|
||||
type: "mention",
|
||||
statusId: newNote.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return await Note.fromId(newNote.id);
|
||||
}
|
||||
|
||||
async updateFromData(
|
||||
content?: Lysand.ContentFormat,
|
||||
visibility?: APIStatus["visibility"],
|
||||
is_sensitive?: boolean,
|
||||
spoiler_text?: string,
|
||||
emojis: EmojiWithInstance[] = [],
|
||||
mentions: UserWithRelations[] = [],
|
||||
/** List of IDs of database Attachment objects */
|
||||
media_attachments: string[] = [],
|
||||
) {
|
||||
const htmlContent = content
|
||||
? await contentToHtml(content, mentions)
|
||||
: undefined;
|
||||
|
||||
// Parse emojis and fuse with existing emojis
|
||||
let foundEmojis = emojis;
|
||||
|
||||
if (this.getAuthor().instanceId === null && htmlContent) {
|
||||
const parsedEmojis = await parseEmojis(htmlContent);
|
||||
// Fuse and deduplicate
|
||||
foundEmojis = [...emojis, ...parsedEmojis].filter(
|
||||
(emoji, index, self) =>
|
||||
index === self.findIndex((t) => t.id === emoji.id),
|
||||
);
|
||||
}
|
||||
|
||||
const newNote = await this.update({
|
||||
content: htmlContent,
|
||||
contentSource: content
|
||||
? content["text/plain"]?.content ||
|
||||
content["text/markdown"]?.content ||
|
||||
Object.entries(content)[0][1].content ||
|
||||
""
|
||||
: undefined,
|
||||
contentType: "text/html",
|
||||
visibility,
|
||||
sensitive: is_sensitive,
|
||||
spoilerText: spoiler_text,
|
||||
});
|
||||
|
||||
// Connect emojis
|
||||
await db
|
||||
.delete(emojiToStatus)
|
||||
.where(eq(emojiToStatus.statusId, this.status.id));
|
||||
|
||||
for (const emoji of foundEmojis) {
|
||||
await db
|
||||
.insert(emojiToStatus)
|
||||
.values({
|
||||
emojiId: emoji.id,
|
||||
statusId: this.status.id,
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
// Connect mentions
|
||||
await db
|
||||
.delete(statusToMentions)
|
||||
.where(eq(statusToMentions.statusId, this.status.id));
|
||||
|
||||
for (const mention of mentions ?? []) {
|
||||
await db
|
||||
.insert(statusToMentions)
|
||||
.values({
|
||||
statusId: this.status.id,
|
||||
userId: mention.id,
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
// Set attachment parents
|
||||
if (media_attachments && media_attachments.length > 0) {
|
||||
await db
|
||||
.update(attachment)
|
||||
.set({
|
||||
statusId: this.status.id,
|
||||
})
|
||||
.where(inArray(attachment.id, media_attachments));
|
||||
}
|
||||
|
||||
return await Note.fromId(newNote.id);
|
||||
}
|
||||
|
||||
async delete() {
|
||||
return (
|
||||
await db
|
||||
.delete(status)
|
||||
.where(eq(status.id, this.status.id))
|
||||
.returning()
|
||||
)[0];
|
||||
}
|
||||
|
||||
async update(newStatus: Partial<Status>) {
|
||||
return (
|
||||
await db
|
||||
.update(status)
|
||||
.set(newStatus)
|
||||
.where(eq(status.id, this.status.id))
|
||||
.returning()
|
||||
)[0];
|
||||
}
|
||||
|
||||
static async deleteMany(ids: string[]) {
|
||||
return await db
|
||||
.delete(status)
|
||||
.where(inArray(status.id, ids))
|
||||
.returning();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this status is viewable by a user.
|
||||
* @param user The user to check.
|
||||
* @returns Whether this status is viewable by the user.
|
||||
*/
|
||||
async isViewableByUser(user: UserWithRelations | null) {
|
||||
if (this.getAuthor().id === user?.id) return true;
|
||||
if (this.getStatus().visibility === "public") return true;
|
||||
if (this.getStatus().visibility === "unlisted") return true;
|
||||
if (this.getStatus().visibility === "private") {
|
||||
return user
|
||||
? await db.query.relationship.findFirst({
|
||||
where: (relationship, { and, eq }) =>
|
||||
and(
|
||||
eq(relationship.ownerId, user?.id),
|
||||
eq(relationship.subjectId, status.authorId),
|
||||
eq(relationship.following, true),
|
||||
),
|
||||
})
|
||||
: false;
|
||||
}
|
||||
return (
|
||||
user &&
|
||||
this.getStatus().mentions.find((mention) => mention.id === user.id)
|
||||
);
|
||||
}
|
||||
|
||||
async toAPI(userFetching?: UserWithRelations | null): Promise<APIStatus> {
|
||||
const data = this.getStatus();
|
||||
const wasPinnedByUser = userFetching
|
||||
? !!(await db.query.userPinnedNotes.findFirst({
|
||||
where: (relation, { and, eq }) =>
|
||||
and(
|
||||
eq(relation.statusId, data.id),
|
||||
eq(relation.userId, userFetching?.id),
|
||||
),
|
||||
}))
|
||||
: false;
|
||||
|
||||
const wasRebloggedByUser = userFetching
|
||||
? !!(await Note.fromSql(
|
||||
and(
|
||||
eq(status.authorId, userFetching?.id),
|
||||
eq(status.reblogId, data.id),
|
||||
),
|
||||
))
|
||||
: false;
|
||||
|
||||
const wasMutedByUser = userFetching
|
||||
? !!(await db.query.relationship.findFirst({
|
||||
where: (relationship, { and, eq }) =>
|
||||
and(
|
||||
eq(relationship.ownerId, userFetching.id),
|
||||
eq(relationship.subjectId, data.authorId),
|
||||
eq(relationship.muting, true),
|
||||
),
|
||||
}))
|
||||
: false;
|
||||
|
||||
// Convert mentions of local users from @username@host to @username
|
||||
const mentionedLocalUsers = data.mentions.filter(
|
||||
(mention) => mention.instanceId === null,
|
||||
);
|
||||
|
||||
let replacedContent = data.content;
|
||||
|
||||
for (const mention of mentionedLocalUsers) {
|
||||
replacedContent = replacedContent.replace(
|
||||
createRegExp(
|
||||
exactly(
|
||||
`@${mention.username}@${
|
||||
new URL(config.http.base_url).host
|
||||
}`,
|
||||
),
|
||||
[global],
|
||||
),
|
||||
`@${mention.username}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
id: data.id,
|
||||
in_reply_to_id: data.inReplyToPostId || null,
|
||||
in_reply_to_account_id: data.inReplyTo?.authorId || null,
|
||||
account: userToAPI(data.author),
|
||||
created_at: new Date(data.createdAt).toISOString(),
|
||||
application: data.application
|
||||
? applicationToAPI(data.application)
|
||||
: null,
|
||||
card: null,
|
||||
content: replacedContent,
|
||||
emojis: data.emojis.map((emoji) => emojiToAPI(emoji)),
|
||||
favourited: !!(data.likes ?? []).find(
|
||||
(like) => like.likerId === userFetching?.id,
|
||||
),
|
||||
favourites_count: (data.likes ?? []).length,
|
||||
media_attachments: (data.attachments ?? []).map(
|
||||
(a) => attachmentToAPI(a) as APIAttachment,
|
||||
),
|
||||
mentions: data.mentions.map((mention) => userToMention(mention)),
|
||||
language: null,
|
||||
muted: wasMutedByUser,
|
||||
pinned: wasPinnedByUser,
|
||||
// TODO: Add polls
|
||||
poll: null,
|
||||
reblog: data.reblog
|
||||
? await Note.fromStatus(
|
||||
data.reblog as StatusWithRelations,
|
||||
).toAPI(userFetching)
|
||||
: null,
|
||||
reblogged: wasRebloggedByUser,
|
||||
reblogs_count: data.reblogCount,
|
||||
replies_count: data.replyCount,
|
||||
sensitive: data.sensitive,
|
||||
spoiler_text: data.spoilerText,
|
||||
tags: [],
|
||||
uri:
|
||||
data.uri ||
|
||||
new URL(
|
||||
`/@${data.author.username}/${data.id}`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
visibility: data.visibility as APIStatus["visibility"],
|
||||
url: data.uri || this.getMastoURI(),
|
||||
bookmarked: false,
|
||||
quote: !!data.quotingPostId,
|
||||
// @ts-expect-error Pleroma extension
|
||||
quote_id: data.quotingPostId || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
getURI() {
|
||||
return localObjectURI(this.getStatus().id);
|
||||
}
|
||||
|
||||
getMastoURI() {
|
||||
return `/@${this.getAuthor().username}/${this.getStatus().id}`;
|
||||
}
|
||||
|
||||
toLysand(): Lysand.Note {
|
||||
const status = this.getStatus();
|
||||
return {
|
||||
type: "Note",
|
||||
created_at: new Date(status.createdAt).toISOString(),
|
||||
id: status.id,
|
||||
author: getUserUri(status.author),
|
||||
uri: this.getURI(),
|
||||
content: {
|
||||
"text/html": {
|
||||
content: status.content,
|
||||
},
|
||||
"text/plain": {
|
||||
content: htmlToText(status.content),
|
||||
},
|
||||
},
|
||||
attachments: (status.attachments ?? []).map((attachment) =>
|
||||
attachmentToLysand(attachment),
|
||||
),
|
||||
is_sensitive: status.sensitive,
|
||||
mentions: status.mentions.map((mention) => mention.uri || ""),
|
||||
quotes: getStatusUri(status.quoting) ?? undefined,
|
||||
replies_to: getStatusUri(status.inReplyTo) ?? undefined,
|
||||
subject: status.spoilerText,
|
||||
visibility: status.visibility as Lysand.Visibility,
|
||||
extensions: {
|
||||
"org.lysand:custom_emojis": {
|
||||
emojis: status.emojis.map((emoji) => emojiToLysand(emoji)),
|
||||
},
|
||||
// TODO: Add polls and reactions
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all the ancestors of this post,
|
||||
*/
|
||||
async getAncestors(fetcher: UserWithRelationsAndRelationships | null) {
|
||||
const ancestors: Note[] = [];
|
||||
|
||||
let currentStatus: Note = this;
|
||||
|
||||
while (currentStatus.getStatus().inReplyToPostId) {
|
||||
const parent = await Note.fromId(
|
||||
currentStatus.getStatus().inReplyToPostId,
|
||||
);
|
||||
|
||||
if (!parent) {
|
||||
break;
|
||||
}
|
||||
|
||||
ancestors.push(parent);
|
||||
currentStatus = parent;
|
||||
}
|
||||
|
||||
// Filter for posts that are viewable by the user
|
||||
const viewableAncestors = ancestors.filter((ancestor) =>
|
||||
ancestor.isViewableByUser(fetcher),
|
||||
);
|
||||
return viewableAncestors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all the descendants of this post (recursive)
|
||||
* Temporary implementation, will be replaced with a recursive SQL query when I get to it
|
||||
*/
|
||||
async getDescendants(
|
||||
fetcher: UserWithRelationsAndRelationships | null,
|
||||
depth = 0,
|
||||
) {
|
||||
const descendants: Note[] = [];
|
||||
for (const child of await this.getReplyChildren()) {
|
||||
descendants.push(child);
|
||||
|
||||
if (depth < 20) {
|
||||
const childDescendants = await child.getDescendants(
|
||||
fetcher,
|
||||
depth + 1,
|
||||
);
|
||||
descendants.push(...childDescendants);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter for posts that are viewable by the user
|
||||
|
||||
const viewableDescendants = descendants.filter((descendant) =>
|
||||
descendant.isViewableByUser(fetcher),
|
||||
);
|
||||
return viewableDescendants;
|
||||
}
|
||||
}
|
||||
6
packages/database-interface/package.json
Normal file
6
packages/database-interface/package.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "database-interface",
|
||||
"version": "0.0.0",
|
||||
"main": "index.ts",
|
||||
"dependencies": {}
|
||||
}
|
||||
85
packages/database-interface/timeline.ts
Normal file
85
packages/database-interface/timeline.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import { type SQL, gt } from "drizzle-orm";
|
||||
import { status } from "~drizzle/schema";
|
||||
import { config } from "~packages/config-manager";
|
||||
import { Note } from "./note";
|
||||
|
||||
enum TimelineType {
|
||||
NOTE = "Note",
|
||||
}
|
||||
|
||||
export class Timeline {
|
||||
constructor(private type: TimelineType) {}
|
||||
|
||||
static async getNoteTimeline(
|
||||
sql: SQL<unknown> | undefined,
|
||||
limit: number,
|
||||
url: string,
|
||||
) {
|
||||
return new Timeline(TimelineType.NOTE).fetchTimeline(sql, limit, url);
|
||||
}
|
||||
|
||||
private async fetchTimeline<T>(
|
||||
sql: SQL<unknown> | undefined,
|
||||
limit: number,
|
||||
url: string,
|
||||
) {
|
||||
const objects: Note[] = [];
|
||||
|
||||
switch (this.type) {
|
||||
case TimelineType.NOTE:
|
||||
objects.push(
|
||||
...(await Note.manyFromSql(sql, undefined, limit)),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
const linkHeader = [];
|
||||
const urlWithoutQuery = new URL(
|
||||
new URL(url).pathname,
|
||||
config.http.base_url,
|
||||
).toString();
|
||||
|
||||
if (objects.length > 0) {
|
||||
switch (this.type) {
|
||||
case TimelineType.NOTE: {
|
||||
const objectBefore = await Note.fromSql(
|
||||
gt(status.id, objects[0].getStatus().id),
|
||||
);
|
||||
|
||||
if (objectBefore) {
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?limit=${limit ?? 20}&min_id=${
|
||||
objects[0].getStatus().id
|
||||
}>; rel="prev"`,
|
||||
);
|
||||
}
|
||||
|
||||
if (objects.length >= (limit ?? 20)) {
|
||||
const objectAfter = await Note.fromSql(
|
||||
gt(
|
||||
status.id,
|
||||
objects[objects.length - 1].getStatus().id,
|
||||
),
|
||||
);
|
||||
|
||||
if (objectAfter) {
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?limit=${
|
||||
limit ?? 20
|
||||
}&max_id=${
|
||||
objects[objects.length - 1].getStatus().id
|
||||
}>; rel="next"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
link: linkHeader.join(", "),
|
||||
objects,
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue