From c3271ba264d0bf2fec00c596f70cc7ee96d0ff28 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Wed, 12 Jun 2024 22:52:03 -1000 Subject: [PATCH] docs: :memo: More work on JSDoc --- biome.json | 10 + cli/commands/user/create.ts | 2 +- packages/database-interface/base.ts | 39 ++++ packages/database-interface/note.ts | 192 ++++++++++++++---- packages/database-interface/timeline.ts | 6 +- .../tests/media-backends.test.ts | 2 +- server/api/users/:uuid/inbox/index.ts | 4 +- utils/timelines.ts | 2 +- 8 files changed, 205 insertions(+), 52 deletions(-) diff --git a/biome.json b/biome.json index df138bcb..510eb9f5 100644 --- a/biome.json +++ b/biome.json @@ -48,6 +48,16 @@ ] } } + }, + "nursery": { + "noDuplicateElseIf": "warn", + "noDuplicateJsonKeys": "warn", + "noEvolvingTypes": "warn", + "noYodaExpression": "warn", + "useConsistentBuiltinInstantiation": "warn", + "useErrorMessage": "warn", + "useImportExtensions": "off", + "useThrowNewError": "warn" } }, "ignore": ["node_modules", "dist", "glitch", "glitch-dev"] diff --git a/cli/commands/user/create.ts b/cli/commands/user/create.ts index bbe841d7..10a73dae 100644 --- a/cli/commands/user/create.ts +++ b/cli/commands/user/create.ts @@ -69,7 +69,7 @@ export default class UserCreate extends BaseCommand { this.exit(1); } - let password = null; + let password: string | null = null; if (flags["set-password"]) { const password1 = await input({ diff --git a/packages/database-interface/base.ts b/packages/database-interface/base.ts index f3e17a6d..81376630 100644 --- a/packages/database-interface/base.ts +++ b/packages/database-interface/base.ts @@ -1,21 +1,60 @@ import type { InferModelFromColumns, InferSelectModel } from "drizzle-orm"; import type { PgTableWithColumns } from "drizzle-orm/pg-core"; +/** + * BaseInterface is an abstract class that provides a common interface for all models. + * It includes methods for saving, deleting, updating, and reloading data. + * + * @template Table - The type of the table with columns. + * @template Columns - The type of the columns inferred from the table. + */ export abstract class BaseInterface< // biome-ignore lint/suspicious/noExplicitAny: This is just an extended interface Table extends PgTableWithColumns, Columns = InferModelFromColumns, > { + /** + * Constructs a new instance of the BaseInterface. + * + * @param data - The data for the model. + */ constructor(public data: Columns) {} + /** + * Saves the current state of the model to the database. + * + * @returns A promise that resolves with the saved model. + */ public abstract save(): Promise; + /** + * Deletes the model from the database. + * + * @param ids - The ids of the models to delete. + * @returns A promise that resolves when the deletion is complete. + */ public abstract delete(ids: string[]): Promise; + /** + * Deletes the model from the database. + * + * @returns A promise that resolves when the deletion is complete. + */ public abstract delete(): Promise; + /** + * Updates the model with new data. + * + * @param newData - The new data for the model. + * @returns A promise that resolves with the updated model. + */ public abstract update( newData: Partial>, ): Promise; + /** + * Reloads the model from the database. + * + * @returns A promise that resolves when the reloading is complete. + */ public abstract reload(): Promise; } diff --git a/packages/database-interface/note.ts b/packages/database-interface/note.ts index 7c3454c6..df9b8c1c 100644 --- a/packages/database-interface/note.ts +++ b/packages/database-interface/note.ts @@ -40,7 +40,7 @@ import { } 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"; +import type { Status as APIStatus } from "~/types/mastodon/status"; import { Attachment } from "./attachment"; import { BaseInterface } from "./base"; import { Emoji } from "./emoji"; @@ -64,6 +64,12 @@ export class Note extends BaseInterface { this.data = reloaded.data; } + /** + * Insert a new note into the database + * @param data - The data to insert + * @param userRequestingNoteId - The ID of the user requesting the note (used to check visibility of the note) + * @returns The inserted note + */ public static async insert( data: InferInsertModel, userRequestingNoteId?: string, @@ -79,6 +85,12 @@ export class Note extends BaseInterface { return note; } + /** + * Fetch a note from the database by its ID + * @param id - The ID of the note to fetch + * @param userRequestingNoteId - The ID of the user requesting the note (used to check visibility of the note) + * @returns The fetched note + */ static async fromId( id: string | null, userRequestingNoteId?: string, @@ -93,7 +105,12 @@ export class Note extends BaseInterface { userRequestingNoteId, ); } - + /** + * Fetch multiple notes from the database by their IDs + * @param ids - The IDs of the notes to fetch + * @param userRequestingNoteId - The ID of the user requesting the note (used to check visibility of the note) + * @returns The fetched notes + */ static async fromIds( ids: string[], userRequestingNoteId?: string, @@ -107,11 +124,18 @@ export class Note extends BaseInterface { ); } + /** + * Fetch a note from the database by a SQL query + * @param sql - The SQL query to fetch the note with + * @param orderBy - The SQL query to order the results by + * @param userId - The ID of the user requesting the note (used to check visibility of the note) + * @returns The fetched note + */ static async fromSql( sql: SQL | undefined, orderBy: SQL | undefined = desc(Notes.id), userId?: string, - ) { + ): Promise { const found = await findManyNotes( { where: sql, @@ -127,13 +151,22 @@ export class Note extends BaseInterface { return new Note(found[0]); } + /** + * Fetch multiple notes from the database by a SQL query + * @param sql - The SQL query to fetch the notes with + * @param orderBy - The SQL query to order the results by + * @param limit - The maximum number of notes to fetch + * @param offset - The number of notes to skip + * @param userId - The ID of the user requesting the note (used to check visibility of the note) + * @returns - The fetched notes + */ static async manyFromSql( sql: SQL | undefined, orderBy: SQL | undefined = desc(Notes.id), limit?: number, offset?: number, userId?: string, - ) { + ): Promise { const found = await findManyNotes( { where: sql, @@ -151,7 +184,15 @@ export class Note extends BaseInterface { return this.data.id; } - async getUsersToFederateTo() { + /** + * Fetch the users that should be federated to for this note + * + * This includes: + * - Users mentioned in the note + * - Users that can see the note + * @returns The users that should be federated to + */ + async getUsersToFederateTo(): Promise { // Mentioned users const mentionedUsers = this.data.mentions.length > 0 @@ -194,15 +235,15 @@ export class Note extends BaseInterface { return deduplicatedUsersById; } - isNull() { - return this.data === null; - } - get author() { return new User(this.data.author); } - static async getCount() { + /** + * Get the number of notes in the database (excluding remote notes) + * @returns The number of notes in the database + */ + static async getCount(): Promise { return ( await db .select({ @@ -215,7 +256,12 @@ export class Note extends BaseInterface { )[0].count; } - async getReplyChildren(userId?: string) { + /** + * Get the children of this note (replies) + * @param userId - The ID of the user requesting the note (used to check visibility of the note) + * @returns The children of this note + */ + private async getReplyChildren(userId?: string): Promise { return await Note.manyFromSql( eq(Notes.replyId, this.data.id), undefined, @@ -229,7 +275,11 @@ export class Note extends BaseInterface { return this.author.isRemote(); } - async updateFromRemote() { + /** + * Update a note from remote federated servers + * @returns The updated note + */ + async updateFromRemote(): Promise { if (!this.isRemote()) { throw new Error("Cannot refetch a local note (it is not remote)"); } @@ -245,10 +295,15 @@ export class Note extends BaseInterface { return this; } + /** + * Create a new note from user input + * @param data - The data to create the note from + * @returns The created note + */ static async fromData(data: { author: User; content: typeof EntityValidator.$ContentFormat; - visibility: apiStatus["visibility"]; + visibility: APIStatus["visibility"]; isSensitive: boolean; spoilerText: string; emojis?: Emoji[]; @@ -330,10 +385,15 @@ export class Note extends BaseInterface { return newNote; } + /** + * Update a note from user input + * @param data - The data to update the note from + * @returns The updated note + */ async updateFromData(data: { author?: User; content?: typeof EntityValidator.$ContentFormat; - visibility?: apiStatus["visibility"]; + visibility?: APIStatus["visibility"]; isSensitive?: boolean; spoilerText?: string; emojis?: Emoji[]; @@ -405,6 +465,12 @@ export class Note extends BaseInterface { return this; } + /** + * Updates the emojis associated with this note in the database + * + * Deletes all existing emojis associated with this note, then replaces them with the provided emojis. + * @param emojis - The emojis to associate with this note + */ public async recalculateDatabaseEmojis(emojis: Emoji[]): Promise { // Fuse and deduplicate const fusedEmojis = emojis.filter( @@ -428,6 +494,12 @@ export class Note extends BaseInterface { } } + /** + * Updates the mentions associated with this note in the database + * + * Deletes all existing mentions associated with this note, then replaces them with the provided mentions. + * @param mentions - The mentions to associate with this note + */ public async recalculateDatabaseMentions(mentions: User[]): Promise { // Connect mentions await db @@ -445,6 +517,12 @@ export class Note extends BaseInterface { } } + /** + * Updates the attachments associated with this note in the database + * + * Deletes all existing attachments associated with this note, then replaces them with the provided attachments. + * @param mediaAttachments - The IDs of the attachments to associate with this note + */ public async recalculateDatabaseAttachments( mediaAttachments: string[], ): Promise { @@ -466,19 +544,21 @@ export class Note extends BaseInterface { } } - static async resolve( - uri?: string, - providedNote?: typeof EntityValidator.$Note, - ): Promise { + /** + * Resolve a note from a URI + * @param uri - The URI of the note to resolve + * @returns The resolved note + */ + static async resolve(uri: string): Promise { // Check if note not already in database - const foundNote = uri && (await Note.fromSql(eq(Notes.uri, uri))); + const foundNote = await Note.fromSql(eq(Notes.uri, uri)); if (foundNote) { return foundNote; } // Check if URI is of a local note - if (uri?.startsWith(config.http.base_url)) { + if (uri.startsWith(config.http.base_url)) { const uuid = uri.match(idValidator); if (!uuid?.[0]) { @@ -490,18 +570,16 @@ export class Note extends BaseInterface { return await Note.fromId(uuid[0]); } - return await Note.saveFromRemote(uri, providedNote); + return await Note.saveFromRemote(uri); } - static async saveFromRemote( - uri?: string, - providedNote?: typeof EntityValidator.$Note, - ): Promise { - if (!(uri || providedNote)) { - throw new Error("No URI or note provided"); - } - - let note = providedNote || null; + /** + * Save a note from a remote server + * @param uri - The URI of the note to save + * @returns The saved note, or null if the note could not be fetched + */ + static async saveFromRemote(uri: string): Promise { + let note: typeof EntityValidator.$Note | null = null; if (uri) { if (!URL.canParse(uri)) { @@ -531,11 +609,17 @@ export class Note extends BaseInterface { return await Note.fromLysand(note, author); } + /** + * Turns a Lysand Note into a database note (saved) + * @param note Lysand Note + * @param author Author of the note + * @returns The saved note + */ static async fromLysand( note: typeof EntityValidator.$Note, author: User, ): Promise { - const emojis = []; + const emojis: Emoji[] = []; for (const emoji of note.extensions?.["org.lysand:custom_emojis"] ?.emojis ?? []) { @@ -555,7 +639,7 @@ export class Note extends BaseInterface { } } - const attachments = []; + const attachments: Attachment[] = []; for (const attachment of note.attachments ?? []) { const resolvedAttachment = await Attachment.fromLysand( @@ -581,7 +665,7 @@ export class Note extends BaseInterface { content: "", }, }, - visibility: note.visibility as apiStatus["visibility"], + visibility: note.visibility as APIStatus["visibility"], isSensitive: note.is_sensitive ?? false, spoilerText: note.subject ?? "", emojis, @@ -644,7 +728,7 @@ export class Note extends BaseInterface { * @param user The user to check. * @returns Whether this status is viewable by the user. */ - async isViewableByUser(user: User | null) { + async isViewableByUser(user: User | null): Promise { if (this.author.id === user?.id) { return true; } @@ -656,22 +740,28 @@ export class Note extends BaseInterface { } if (this.data.visibility === "private") { return user - ? await db.query.Relationships.findFirst({ + ? !!(await db.query.Relationships.findFirst({ where: (relationship, { and, eq }) => and( eq(relationship.ownerId, user?.id), eq(relationship.subjectId, Notes.authorId), eq(relationship.following, true), ), - }) + })) : false; } return ( - user && this.data.mentions.find((mention) => mention.id === user.id) + !!user && + !!this.data.mentions.find((mention) => mention.id === user.id) ); } - async toApi(userFetching?: User | null): Promise { + /** + * Convert a note to the Mastodon API format + * @param userFetching - The user fetching the note (used to check if the note is favourite and such) + * @returns The note in the Mastodon API format + */ + async toApi(userFetching?: User | null): Promise { const data = this.data; // Convert mentions of local users from @username@host to @username @@ -749,7 +839,7 @@ export class Note extends BaseInterface { spoiler_text: data.spoilerText, tags: [], uri: data.uri || this.getUri(), - visibility: data.visibility as apiStatus["visibility"], + visibility: data.visibility as APIStatus["visibility"], url: data.uri || this.getMastoUri(), bookmarked: false, // @ts-expect-error Glitch-SOC extension @@ -762,24 +852,32 @@ export class Note extends BaseInterface { }; } - getUri() { + getUri(): string { return localObjectUri(this.data.id); } - static getUri(id?: string | null) { + static getUri(id?: string | null): string | null { if (!id) { return null; } return localObjectUri(id); } - getMastoUri() { + /** + * Get the frontend URI of this note + * @returns The frontend URI of this note + */ + getMastoUri(): string { return new URL( `/@${this.author.data.username}/${this.id}`, config.http.base_url, ).toString(); } + /** + * Convert a note to the Lysand format + * @returns The note in the Lysand format + */ toLysand(): typeof EntityValidator.$Note { const status = this.data; return { @@ -822,8 +920,11 @@ export class Note extends BaseInterface { /** * Return all the ancestors of this post, + * i.e. all the posts that this post is a reply to + * @param fetcher - The user fetching the ancestors + * @returns The ancestors of this post */ - async getAncestors(fetcher: User | null) { + async getAncestors(fetcher: User | null): Promise { const ancestors: Note[] = []; let currentStatus: Note = this; @@ -854,8 +955,11 @@ export class Note extends BaseInterface { /** * Return all the descendants of this post (recursive) * Temporary implementation, will be replaced with a recursive SQL query when I get to it + * @param fetcher - The user fetching the descendants + * @param depth - The depth of the recursion (internal) + * @returns The descendants of this post */ - async getDescendants(fetcher: User | null, depth = 0) { + async getDescendants(fetcher: User | null, depth = 0): Promise { const descendants: Note[] = []; for (const child of await this.getReplyChildren(fetcher?.id)) { descendants.push(child); diff --git a/packages/database-interface/timeline.ts b/packages/database-interface/timeline.ts index 7d7ed9ab..49613044 100644 --- a/packages/database-interface/timeline.ts +++ b/packages/database-interface/timeline.ts @@ -66,7 +66,7 @@ export class Timeline { url: string, limit: number, ): Promise { - const linkHeader = []; + const linkHeader: string[] = []; const urlWithoutQuery = new URL( new URL(url).pathname, config.http.base_url, @@ -103,7 +103,7 @@ export class Timeline { urlWithoutQuery: string, limit: number, ): Promise { - const linkHeader = []; + const linkHeader: string[] = []; const objectBefore = await Note.fromSql(gt(Notes.id, notes[0].data.id)); if (objectBefore) { @@ -131,7 +131,7 @@ export class Timeline { urlWithoutQuery: string, limit: number, ): Promise { - const linkHeader = []; + const linkHeader: string[] = []; const objectBefore = await User.fromSql(gt(Users.id, users[0].id)); if (objectBefore) { diff --git a/packages/media-manager/tests/media-backends.test.ts b/packages/media-manager/tests/media-backends.test.ts index 3eb3bbe9..041414f9 100644 --- a/packages/media-manager/tests/media-backends.test.ts +++ b/packages/media-manager/tests/media-backends.test.ts @@ -7,7 +7,7 @@ import { MediaBackendType, MediaHasher, S3MediaBackend, -} from ".."; +} from "../index"; import { MediaConverter } from "../media-converter"; type DeepPartial = { diff --git a/server/api/users/:uuid/inbox/index.ts b/server/api/users/:uuid/inbox/index.ts index d7696dc5..fe14a1c0 100644 --- a/server/api/users/:uuid/inbox/index.ts +++ b/server/api/users/:uuid/inbox/index.ts @@ -204,9 +204,9 @@ export default (app: Hono) => return errorResponse("Author not found", 404); } - const newStatus = await Note.resolve( - undefined, + const newStatus = await Note.fromLysand( note, + account, ).catch((e) => { dualLogger.logError( LogLevel.Error, diff --git a/utils/timelines.ts b/utils/timelines.ts index 8c6830cb..af8b0400 100644 --- a/utils/timelines.ts +++ b/utils/timelines.ts @@ -25,7 +25,7 @@ export async function fetchTimeline( const objects = (await model(args, userId)) as T[]; // Constuct HTTP Link header (next and prev) only if there are more statuses - const linkHeader = []; + const linkHeader: string[] = []; const urlWithoutQuery = new URL( new URL(req.url).pathname, config.http.base_url,