diff --git a/bun.lockb b/bun.lockb index d70cd385..f8539fc9 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/database/entities/Application.ts b/database/entities/Application.ts index eefb32f7..35264689 100644 --- a/database/entities/Application.ts +++ b/database/entities/Application.ts @@ -1,10 +1,9 @@ -import type { Application } from "@prisma/client"; -import { client } from "~database/datasource"; +import type { InferSelectModel } from "drizzle-orm"; +import { db } from "~drizzle/db"; +import type { application } from "~drizzle/schema"; import type { APIApplication } from "~types/entities/application"; -/** - * Represents an application that can authenticate with the API. - */ +export type Application = InferSelectModel; /** * Retrieves the application associated with the given access token. @@ -14,16 +13,14 @@ import type { APIApplication } from "~types/entities/application"; export const getFromToken = async ( token: string, ): Promise => { - const dbToken = await client.token.findFirst({ - where: { - access_token: token, - }, - include: { + const result = await db.query.token.findFirst({ + where: (tokens, { eq }) => eq(tokens.accessToken, token), + with: { application: true, }, }); - return dbToken?.application || null; + return result?.application || null; }; /** @@ -34,6 +31,6 @@ export const applicationToAPI = (app: Application): APIApplication => { return { name: app.name, website: app.website, - vapid_key: app.vapid_key, + vapid_key: app.vapidKey, }; }; diff --git a/database/entities/Attachment.ts b/database/entities/Attachment.ts index 319cb304..2fe6daed 100644 --- a/database/entities/Attachment.ts +++ b/database/entities/Attachment.ts @@ -1,21 +1,24 @@ -import type { Attachment } from "@prisma/client"; import type { Config } from "config-manager"; import { MediaBackendType } from "media-manager"; import type { APIAsyncAttachment } from "~types/entities/async_attachment"; import type { APIAttachment } from "~types/entities/attachment"; import type * as Lysand from "lysand-types"; -import { client } from "~database/datasource"; +import { db } from "~drizzle/db"; +import { attachment } from "~drizzle/schema"; +import type { InferSelectModel } from "drizzle-orm"; + +export type Attachment = InferSelectModel; export const attachmentToAPI = ( attachment: Attachment, ): APIAsyncAttachment | APIAttachment => { let type = "unknown"; - if (attachment.mime_type.startsWith("image/")) { + if (attachment.mimeType.startsWith("image/")) { type = "image"; - } else if (attachment.mime_type.startsWith("video/")) { + } else if (attachment.mimeType.startsWith("video/")) { type = "video"; - } else if (attachment.mime_type.startsWith("audio/")) { + } else if (attachment.mimeType.startsWith("audio/")) { type = "audio"; } @@ -23,8 +26,8 @@ export const attachmentToAPI = ( id: attachment.id, type: type as "image" | "video" | "audio" | "unknown", url: attachment.url, - remote_url: attachment.remote_url, - preview_url: attachment.thumbnail_url, + remote_url: attachment.remoteUrl, + preview_url: attachment.thumbnailUrl, text_url: null, meta: { width: attachment.width || undefined, @@ -63,7 +66,7 @@ export const attachmentToLysand = ( attachment: Attachment, ): Lysand.ContentFormat => { return { - [attachment.mime_type]: { + [attachment.mimeType]: { content: attachment.url, blurhash: attachment.blurhash ?? undefined, description: attachment.description ?? undefined, @@ -82,13 +85,15 @@ export const attachmentToLysand = ( }; export const attachmentFromLysand = async ( - attachment: Lysand.ContentFormat, -): Promise => { - const key = Object.keys(attachment)[0]; - const value = attachment[key]; + attachmentToConvert: Lysand.ContentFormat, +): Promise> => { + const key = Object.keys(attachmentToConvert)[0]; + const value = attachmentToConvert[key]; - return await client.attachment.create({ - data: { + const result = await db + .insert(attachment) + .values({ + mimeType: key, url: value.content, description: value.description || undefined, duration: value.duration || undefined, @@ -97,10 +102,11 @@ export const attachmentFromLysand = async ( size: value.size || undefined, width: value.width || undefined, sha256: value.hash?.sha256 || undefined, - mime_type: key, blurhash: value.blurhash || undefined, - }, - }); + }) + .returning(); + + return result[0]; }; export const getUrl = (name: string, config: Config) => { diff --git a/database/entities/Emoji.ts b/database/entities/Emoji.ts index 1c4248d4..10c5bf19 100644 --- a/database/entities/Emoji.ts +++ b/database/entities/Emoji.ts @@ -1,33 +1,36 @@ -import type { Emoji } from "@prisma/client"; -import { client } from "~database/datasource"; import type { APIEmoji } from "~types/entities/emoji"; import type * as Lysand from "lysand-types"; import { addInstanceIfNotExists } from "./Instance"; +import { db } from "~drizzle/db"; +import { emoji, instance } from "~drizzle/schema"; +import { and, eq, type InferSelectModel } from "drizzle-orm"; -/** - * Represents an emoji entity in the database. - */ +export type EmojiWithInstance = InferSelectModel & { + instance: InferSelectModel | null; +}; /** * Used for parsing emojis from local text * @param text The text to parse * @returns An array of emojis */ -export const parseEmojis = async (text: string): Promise => { +export const parseEmojis = async (text: string) => { const regex = /:[a-zA-Z0-9_]+:/g; const matches = text.match(regex); if (!matches) return []; - return await client.emoji.findMany({ - where: { - shortcode: { - in: matches.map((match) => match.replace(/:/g, "")), - }, - instanceId: null, - }, - include: { + const emojis = await db.query.emoji.findMany({ + where: (emoji, { eq, or }) => + or( + ...matches + .map((match) => match.replace(/:/g, "")) + .map((match) => eq(emoji.shortcode, match)), + ), + with: { instance: true, }, }); + + return emojis; }; /** @@ -36,62 +39,72 @@ export const parseEmojis = async (text: string): Promise => { * @param host Host to fetch the emoji from if remote * @returns The emoji */ -export const fetchEmoji = async (emoji: Lysand.Emoji, host?: string) => { - const existingEmoji = await client.emoji.findFirst({ - where: { - shortcode: emoji.name, - instance: host - ? { - base_url: host, - } - : null, - }, - }); +export const fetchEmoji = async ( + emojiToFetch: Lysand.Emoji, + host?: string, +): Promise => { + const existingEmoji = await db + .select() + .from(emoji) + .innerJoin(instance, eq(emoji.instanceId, instance.id)) + .where( + and( + eq(emoji.shortcode, emojiToFetch.name), + host ? eq(instance.baseUrl, host) : undefined, + ), + ) + .limit(1); - if (existingEmoji) return existingEmoji; + if (existingEmoji[0]) + return { + ...existingEmoji[0].Emoji, + instance: existingEmoji[0].Instance, + }; - const instance = host ? await addInstanceIfNotExists(host) : null; + const foundInstance = host ? await addInstanceIfNotExists(host) : null; - return await client.emoji.create({ - data: { - shortcode: emoji.name, - url: Object.entries(emoji.url)[0][1].content, - alt: - emoji.alt || - Object.entries(emoji.url)[0][1].description || - undefined, - content_type: Object.keys(emoji.url)[0], - visible_in_picker: true, - instance: host - ? { - connect: { - id: instance?.id, - }, - } - : undefined, - }, - }); + const result = ( + await db + .insert(emoji) + .values({ + shortcode: emojiToFetch.name, + url: Object.entries(emojiToFetch.url)[0][1].content, + alt: + emojiToFetch.alt || + Object.entries(emojiToFetch.url)[0][1].description || + undefined, + contentType: Object.keys(emojiToFetch.url)[0], + visibleInPicker: true, + instanceId: foundInstance?.id, + }) + .returning() + )[0]; + + return { + ...result, + instance: foundInstance, + }; }; /** * Converts the emoji to an APIEmoji object. * @returns The APIEmoji object. */ -export const emojiToAPI = (emoji: Emoji): APIEmoji => { +export const emojiToAPI = (emoji: EmojiWithInstance): APIEmoji => { return { shortcode: emoji.shortcode, static_url: emoji.url, // TODO: Add static version url: emoji.url, - visible_in_picker: emoji.visible_in_picker, + visible_in_picker: emoji.visibleInPicker, category: undefined, }; }; -export const emojiToLysand = (emoji: Emoji): Lysand.Emoji => { +export const emojiToLysand = (emoji: EmojiWithInstance): Lysand.Emoji => { return { name: emoji.shortcode, url: { - [emoji.content_type]: { + [emoji.contentType]: { content: emoji.url, description: emoji.alt || undefined, }, diff --git a/database/entities/Federation.ts b/database/entities/Federation.ts index 6d88ce7d..981383b7 100644 --- a/database/entities/Federation.ts +++ b/database/entities/Federation.ts @@ -1,14 +1,13 @@ -import type { User } from "@prisma/client"; import type * as Lysand from "lysand-types"; import { config } from "config-manager"; -import { getUserUri } from "./User"; +import { getUserUri, type User } from "./User"; export const objectToInboxRequest = async ( object: Lysand.Entity, author: User, userToSendTo: User, ): Promise => { - if (!userToSendTo.instanceId || !userToSendTo.endpoints.inbox) { + if (!userToSendTo.instanceId || !userToSendTo.endpoints?.inbox) { throw new Error("UserToSendTo has no inbox or is a local user"); } diff --git a/database/entities/Instance.ts b/database/entities/Instance.ts index e38f0196..0e700972 100644 --- a/database/entities/Instance.ts +++ b/database/entities/Instance.ts @@ -1,6 +1,6 @@ -import type { Instance } from "@prisma/client"; -import { client } from "~database/datasource"; +import { db } from "~drizzle/db"; import type * as Lysand from "lysand-types"; +import { instance } from "~drizzle/schema"; /** * Represents an instance in the database. @@ -11,16 +11,12 @@ import type * as Lysand from "lysand-types"; * @param url * @returns Either the database instance if it already exists, or a newly created instance. */ -export const addInstanceIfNotExists = async ( - url: string, -): Promise => { +export const addInstanceIfNotExists = async (url: string) => { const origin = new URL(url).origin; const host = new URL(url).host; - const found = await client.instance.findFirst({ - where: { - base_url: host, - }, + const found = await db.query.instance.findFirst({ + where: (instance, { eq }) => eq(instance.baseUrl, host), }); if (found) return found; @@ -40,12 +36,15 @@ export const addInstanceIfNotExists = async ( throw new Error("Invalid instance metadata (missing name or version)"); } - return await client.instance.create({ - data: { - base_url: host, - name: metadata.name, - version: metadata.version, - logo: metadata.logo, - }, - }); + return ( + await db + .insert(instance) + .values({ + baseUrl: host, + name: metadata.name, + version: metadata.version, + logo: metadata.logo, + }) + .returning() + )[0]; }; diff --git a/database/entities/Like.ts b/database/entities/Like.ts index dcaf092d..87f20a9b 100644 --- a/database/entities/Like.ts +++ b/database/entities/Like.ts @@ -1,9 +1,12 @@ -import type { Like } from "@prisma/client"; import { config } from "config-manager"; -import { client } from "~database/datasource"; import type { StatusWithRelations } from "./Status"; import type { UserWithRelations } from "./User"; import type * as Lysand from "lysand-types"; +import { and, eq, type InferSelectModel } from "drizzle-orm"; +import { notification, like } from "~drizzle/schema"; +import { db } from "~drizzle/db"; + +export type Like = InferSelectModel; /** * Represents a Like entity in the database. @@ -33,22 +36,18 @@ export const createLike = async ( user: UserWithRelations, status: StatusWithRelations, ) => { - await client.like.create({ - data: { - likedId: status.id, - likerId: user.id, - }, + await db.insert(like).values({ + likedId: status.id, + likerId: user.id, }); if (status.author.instanceId === user.instanceId) { // Notify the user that their post has been favourited - await client.notification.create({ - data: { - accountId: user.id, - type: "favourite", - notifiedId: status.authorId, - statusId: status.id, - }, + await db.insert(notification).values({ + accountId: user.id, + type: "favourite", + notifiedId: status.authorId, + statusId: status.id, }); } else { // TODO: Add database jobs for federating this @@ -64,22 +63,21 @@ export const deleteLike = async ( user: UserWithRelations, status: StatusWithRelations, ) => { - await client.like.deleteMany({ - where: { - likedId: status.id, - likerId: user.id, - }, - }); + await db + .delete(like) + .where(and(eq(like.likedId, status.id), eq(like.likerId, user.id))); // Notify the user that their post has been favourited - await client.notification.deleteMany({ - where: { - accountId: user.id, - type: "favourite", - notifiedId: status.authorId, - statusId: status.id, - }, - }); + await db + .delete(notification) + .where( + and( + eq(notification.accountId, user.id), + eq(notification.type, "favourite"), + eq(notification.notifiedId, status.authorId), + eq(notification.statusId, status.id), + ), + ); if (user.instanceId === null && status.author.instanceId !== null) { // User is local, federate the delete diff --git a/database/entities/Notification.ts b/database/entities/Notification.ts index cab2c4f3..d513b7f1 100644 --- a/database/entities/Notification.ts +++ b/database/entities/Notification.ts @@ -1,7 +1,10 @@ -import type { Notification } from "@prisma/client"; import type { APINotification } from "~types/entities/notification"; import { type StatusWithRelations, statusToAPI } from "./Status"; import { type UserWithRelations, userToAPI } from "./User"; +import type { InferSelectModel } from "drizzle-orm"; +import type { notification } from "~drizzle/schema"; + +export type Notification = InferSelectModel; export type NotificationWithRelations = Notification & { status: StatusWithRelations | null; diff --git a/database/entities/Object.ts b/database/entities/Object.ts index 9a54b8c5..fb436e50 100644 --- a/database/entities/Object.ts +++ b/database/entities/Object.ts @@ -1,15 +1,22 @@ -import type { LysandObject } from "@prisma/client"; -import { client } from "~database/datasource"; -import type { LysandObjectType } from "~types/lysand/Object"; +import type { InferSelectModel } from "drizzle-orm"; +import { db } from "~drizzle/db"; +import { lysandObject } from "~drizzle/schema"; +import { findFirstUser } from "./User"; +import type * as Lysand from "lysand-types"; + +export type LysandObject = InferSelectModel; /** * Represents a Lysand object in the database. */ -export const createFromObject = async (object: LysandObjectType) => { - const foundObject = await client.lysandObject.findFirst({ - where: { remote_id: object.id }, - include: { +export const createFromObject = async ( + object: Lysand.Entity, + authorUri: string, +) => { + const foundObject = await db.query.lysandObject.findFirst({ + where: (o, { eq }) => eq(o.remoteId, object.id), + with: { author: true, }, }); @@ -18,45 +25,43 @@ export const createFromObject = async (object: LysandObjectType) => { return foundObject; } - const author = await client.lysandObject.findFirst({ - // biome-ignore lint/suspicious/noExplicitAny: - where: { uri: (object as any).author }, + const author = await findFirstUser({ + where: (user, { eq }) => eq(user.uri, authorUri), }); - return await client.lysandObject.create({ - data: { - authorId: author?.id, - created_at: new Date(object.created_at).toISOString(), - extensions: object.extensions || {}, - remote_id: object.id, - type: object.type, - uri: object.uri, - // Rest of data (remove id, author, created_at, extensions, type, uri) - extra_data: Object.fromEntries( - Object.entries(object).filter( - ([key]) => - ![ - "id", - "author", - "created_at", - "extensions", - "type", - "uri", - ].includes(key), - ), + return await db.insert(lysandObject).values({ + authorId: author?.id, + createdAt: new Date(object.created_at).toISOString(), + extensions: object.extensions, + remoteId: object.id, + type: object.type, + uri: object.uri, + // Rest of data (remove id, author, created_at, extensions, type, uri) + extraData: Object.fromEntries( + Object.entries(object).filter( + ([key]) => + ![ + "id", + "author", + "created_at", + "extensions", + "type", + "uri", + ].includes(key), ), - }, + ), }); }; -export const toLysand = (lyObject: LysandObject): LysandObjectType => { +export const toLysand = (lyObject: LysandObject): Lysand.Entity => { return { - id: lyObject.remote_id || lyObject.id, - created_at: new Date(lyObject.created_at).toISOString(), + id: lyObject.remoteId || lyObject.id, + created_at: new Date(lyObject.createdAt).toISOString(), type: lyObject.type, uri: lyObject.uri, - ...lyObject.extra_data, - extensions: lyObject.extensions, + ...(lyObject.extraData as object), + // @ts-expect-error Assume stored JSON is valid + extensions: lyObject.extensions as object, }; }; diff --git a/database/entities/Queue.ts b/database/entities/Queue.ts index d804e41e..108c4ee8 100644 --- a/database/entities/Queue.ts +++ b/database/entities/Queue.ts @@ -1,4 +1,3 @@ -import type { User } from "@prisma/client"; import { config } from "config-manager"; // import { Worker } from "bullmq"; import { type StatusWithRelations, statusToLysand } from "./Status"; @@ -118,70 +117,6 @@ import { type StatusWithRelations, statusToLysand } from "./Status"; } ); */ -/** - * Convert a string into an ArrayBuffer - * from https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String - */ -export const str2ab = (str: string) => { - const buf = new ArrayBuffer(str.length); - const bufView = new Uint8Array(buf); - for (let i = 0, strLen = str.length; i < strLen; i++) { - bufView[i] = str.charCodeAt(i); - } - return buf; -}; - -export const federateStatusTo = async ( - status: StatusWithRelations, - sender: User, - user: User, -) => { - const privateKey = await crypto.subtle.importKey( - "pkcs8", - str2ab(atob(user.privateKey ?? "")), - "Ed25519", - false, - ["sign"], - ); - - const digest = await crypto.subtle.digest( - "SHA-256", - new TextEncoder().encode("request_body"), - ); - - const userInbox = new URL(user.endpoints.inbox); - - const date = new Date(); - - const signature = await crypto.subtle.sign( - "Ed25519", - privateKey, - new TextEncoder().encode( - `(request-target): post ${userInbox.pathname}\n` + - `host: ${userInbox.host}\n` + - `date: ${date.toUTCString()}\n` + - `digest: SHA-256=${btoa( - String.fromCharCode(...new Uint8Array(digest)), - )}\n`, - ), - ); - - const signatureBase64 = btoa( - String.fromCharCode(...new Uint8Array(signature)), - ); - - return fetch(userInbox, { - method: "POST", - headers: { - "Content-Type": "application/json", - Date: date.toUTCString(), - Origin: config.http.base_url, - Signature: `keyId="${sender.uri}",algorithm="ed25519",headers="(request-target) host date digest",signature="${signatureBase64}"`, - }, - body: JSON.stringify(statusToLysand(status)), - }); -}; - export const addStatusFederationJob = async (statusId: string) => { /* await federationQueue.add("federation", { id: statusId, diff --git a/database/entities/Relationship.ts b/database/entities/Relationship.ts index 07566ff7..6c0e580f 100644 --- a/database/entities/Relationship.ts +++ b/database/entities/Relationship.ts @@ -1,10 +1,11 @@ -import type { Relationship, User } from "@prisma/client"; import { client } from "~database/datasource"; import type { APIRelationship } from "~types/entities/relationship"; +import type { User } from "./User"; +import type { InferSelectModel } from "drizzle-orm"; +import { relationship } from "~drizzle/schema"; +import { db } from "~drizzle/db"; -/** - * Stores Mastodon API relationships - */ +export type Relationship = InferSelectModel; /** * Creates a new relationship between two users. @@ -16,25 +17,29 @@ export const createNewRelationship = async ( owner: User, other: User, ): Promise => { - return await client.relationship.create({ - data: { - ownerId: owner.id, - subjectId: other.id, - languages: [], - following: false, - showingReblogs: false, - notifying: false, - followedBy: false, - blocking: false, - blockedBy: false, - muting: false, - mutingNotifications: false, - requested: false, - domainBlocking: false, - endorsed: false, - note: "", - }, - }); + return ( + await db + .insert(relationship) + .values({ + ownerId: owner.id, + subjectId: other.id, + languages: [], + following: false, + showingReblogs: false, + notifying: false, + followedBy: false, + blocking: false, + blockedBy: false, + muting: false, + mutingNotifications: false, + requested: false, + domainBlocking: false, + endorsed: false, + note: "", + updatedAt: new Date().toISOString(), + }) + .returning() + )[0]; }; export const checkForBidirectionalRelationships = async ( @@ -42,18 +47,14 @@ export const checkForBidirectionalRelationships = async ( user2: User, createIfNotExists = true, ): Promise => { - const relationship1 = await client.relationship.findFirst({ - where: { - ownerId: user1.id, - subjectId: user2.id, - }, + const relationship1 = await db.query.relationship.findFirst({ + where: (rel, { and, eq }) => + and(eq(rel.ownerId, user1.id), eq(rel.subjectId, user2.id)), }); - const relationship2 = await client.relationship.findFirst({ - where: { - ownerId: user2.id, - subjectId: user1.id, - }, + const relationship2 = await db.query.relationship.findFirst({ + where: (rel, { and, eq }) => + and(eq(rel.ownerId, user2.id), eq(rel.subjectId, user1.id)), }); if (!relationship1 && !relationship2 && createIfNotExists) { @@ -82,7 +83,7 @@ export const relationshipToAPI = (rel: Relationship): APIRelationship => { notifying: rel.notifying, requested: rel.requested, showing_reblogs: rel.showingReblogs, - languages: rel.languages, + languages: rel.languages ?? [], note: rel.note, }; }; diff --git a/database/entities/Status.ts b/database/entities/Status.ts index 6ba0ce44..4eb8575c 100644 --- a/database/entities/Status.ts +++ b/database/entities/Status.ts @@ -1,21 +1,9 @@ -import { getBestContentType } from "@content_types"; -import { addStausToMeilisearch } from "@meilisearch"; -import { - type Application, - type Emoji, - Prisma, - type Relationship, - type Status, - type User, - type Attachment, -} from "@prisma/client"; import { sanitizeHtml } from "@sanitization"; import { config } from "config-manager"; import { htmlToText } from "html-to-text"; import linkifyHtml from "linkify-html"; import linkifyStr from "linkify-string"; import { parse } from "marked"; -import { client } from "~database/datasource"; import type { APIAttachment } from "~types/entities/attachment"; import type { APIStatus } from "~types/entities/status"; import type { Note } from "~types/lysand/Object"; @@ -26,41 +14,466 @@ import { attachmentToAPI, attachmentToLysand, } from "./Attachment"; -import { emojiToAPI, emojiToLysand, fetchEmoji, parseEmojis } from "./Emoji"; -import type { UserWithRelations } from "./User"; -import { getUserUri, resolveUser, resolveWebFinger, userToAPI } from "./User"; -import { statusAndUserRelations, userRelations } from "./relations"; +import { + emojiToAPI, + emojiToLysand, + fetchEmoji, + parseEmojis, + type EmojiWithInstance, +} from "./Emoji"; +import { + getUserUri, + resolveUser, + resolveWebFinger, + userToAPI, + userExtras, + userRelations, + userExtrasTemplate, + type User, + type UserWithRelations, + type UserWithRelationsAndRelationships, + transformOutputToUserWithRelations, + findManyUsers, +} from "./User"; import { objectToInboxRequest } from "./Federation"; +import { + and, + eq, + or, + type InferSelectModel, + sql, + isNotNull, + inArray, +} from "drizzle-orm"; +import { + status, + type application, + attachment, + type like, + user, + statusToUser, + emojiToStatus, + instance, +} from "~drizzle/schema"; +import { db } from "~drizzle/db"; -const statusRelations = Prisma.validator()({ - include: statusAndUserRelations, -}); +export type Status = InferSelectModel; -export type StatusWithRelations = Prisma.StatusGetPayload< - typeof statusRelations +export type StatusWithRelations = Status & { + author: UserWithRelations; + mentions: UserWithRelations[]; + attachments: InferSelectModel[]; + reblog: StatusWithoutRecursiveRelations | null; + emojis: EmojiWithInstance[]; + likes: InferSelectModel[]; + inReplyTo: StatusWithoutRecursiveRelations | null; + quoting: StatusWithoutRecursiveRelations | null; + application: InferSelectModel | null; + reblogCount: number; + likeCount: number; + replyCount: number; +}; + +export type StatusWithoutRecursiveRelations = Omit< + StatusWithRelations, + | "inReplyTo" + | "quoting" + | "reblog" + | "mentions" + | "reblogCount" + | "likeCount" + | "replyCount" >; -/** - * Represents a status (i.e. a post) - */ +export const statusExtras = { + reblogCount: + sql`(SELECT COUNT(*) FROM "Status" "status" WHERE "status"."reblogId" = "status".id)`.as( + "reblog_count", + ), + likeCount: + sql`(SELECT COUNT(*) FROM "Like" "like" WHERE "like"."likedId" = "status".id)`.as( + "like_count", + ), + replyCount: + sql`(SELECT COUNT(*) FROM "Status" "status" WHERE "status"."inReplyToPostId" = "status".id)`.as( + "reply_count", + ), +}; /** * Returns whether this status is viewable by a user. * @param user The user to check. * @returns Whether this status is viewable by the user. */ -export const isViewableByUser = (status: Status, user: User | null) => { +export const isViewableByUser = async ( + status: StatusWithRelations, + user: UserWithRelations | null, +) => { if (status.authorId === user?.id) return true; if (status.visibility === "public") return true; if (status.visibility === "unlisted") return true; if (status.visibility === "private") { - // @ts-expect-error Prisma TypeScript types dont include relations - return !!(user?.relationships as Relationship[]).find( - (rel) => rel.id === status.authorId, - ); + 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; } - // @ts-expect-error Prisma TypeScript types dont include relations - return user && (status.mentions as User[]).includes(user); + return user && status.mentions.includes(user); +}; + +export const findManyStatuses = async ( + query: Parameters[0], +): Promise => { + const output = await db.query.status.findMany({ + ...query, + with: { + ...query?.with, + attachments: { + where: (attachment, { eq }) => + eq(attachment.statusId, sql`"status"."id"`), + }, + author: { + with: { + ...userRelations, + }, + extras: userExtrasTemplate("status_author"), + }, + mentions: { + with: { + user: { + with: userRelations, + extras: userExtrasTemplate("status_mentions_user"), + }, + }, + }, + reblog: { + with: { + attachments: true, + emojis: { + with: { + emoji: { + with: { + instance: true, + }, + }, + }, + }, + likes: true, + application: true, + mentions: { + with: { + user: { + with: { + instance: true, + relationships: true, + }, + }, + }, + }, + author: { + with: { + ...userRelations, + }, + extras: userExtrasTemplate("status_reblog_author"), + }, + }, + }, + inReplyTo: { + with: { + attachments: true, + emojis: { + with: { + emoji: { + with: { + instance: true, + }, + }, + }, + }, + likes: true, + application: true, + mentions: { + with: { + user: { + with: { + instance: true, + relationships: true, + }, + }, + }, + }, + author: { + with: { + ...userRelations, + }, + extras: userExtrasTemplate("status_inReplyTo_author"), + }, + }, + }, + quoting: { + with: { + attachments: true, + emojis: { + with: { + emoji: { + with: { + instance: true, + }, + }, + }, + }, + likes: true, + application: true, + mentions: { + with: { + user: { + with: { + instance: true, + relationships: true, + }, + }, + }, + }, + author: { + with: { + ...userRelations, + }, + extras: userExtrasTemplate("status_quoting_author"), + }, + }, + }, + }, + extras: { + ...statusExtras, + ...query?.extras, + }, + }); + + return output.map((post) => ({ + ...post, + author: transformOutputToUserWithRelations(post.author), + mentions: post.mentions.map( + (mention) => + mention.user && + transformOutputToUserWithRelations(mention.user), + ), + reblog: post.reblog && { + ...post.reblog, + author: transformOutputToUserWithRelations(post.reblog.author), + emojis: post.reblog.emojis.map( + (emoji) => + (emoji as unknown as Record) + .emoji as EmojiWithInstance, + ), + }, + inReplyTo: post.inReplyTo && { + ...post.inReplyTo, + author: transformOutputToUserWithRelations(post.inReplyTo.author), + emojis: post.inReplyTo.emojis.map( + (emoji) => + (emoji as unknown as Record) + .emoji as EmojiWithInstance, + ), + }, + quoting: post.quoting && { + ...post.quoting, + author: transformOutputToUserWithRelations(post.quoting.author), + emojis: post.quoting.emojis.map( + (emoji) => + (emoji as unknown as Record) + .emoji as EmojiWithInstance, + ), + }, + emojis: (post.emojis ?? []).map( + (emoji) => + (emoji as unknown as Record) + .emoji as EmojiWithInstance, + ), + reblogCount: Number(post.reblogCount), + likeCount: Number(post.likeCount), + replyCount: Number(post.replyCount), + })); +}; + +export const findFirstStatuses = async ( + query: Parameters[0], +): Promise => { + const output = await db.query.status.findFirst({ + ...query, + with: { + ...query?.with, + attachments: { + where: (attachment, { eq }) => + eq(attachment.statusId, sql`"status"."id"`), + }, + author: { + with: { + ...userRelations, + }, + extras: userExtrasTemplate("status_author"), + }, + mentions: { + with: { + user: { + with: userRelations, + extras: userExtrasTemplate("status_mentions_user"), + }, + }, + where: (mention, { eq }) => eq(mention.a, sql`"status"."id"`), + }, + reblog: { + with: { + attachments: true, + emojis: { + with: { + emoji: { + with: { + instance: true, + }, + }, + }, + }, + likes: true, + application: true, + mentions: { + with: { + user: { + with: { + instance: true, + relationships: true, + }, + }, + }, + }, + author: { + with: { + ...userRelations, + }, + extras: userExtrasTemplate("status_reblog_author"), + }, + }, + }, + inReplyTo: { + with: { + attachments: true, + emojis: { + with: { + emoji: { + with: { + instance: true, + }, + }, + }, + }, + likes: true, + application: true, + mentions: { + with: { + user: { + with: { + instance: true, + relationships: true, + }, + }, + }, + }, + author: { + with: { + ...userRelations, + }, + extras: userExtrasTemplate("status_inReplyTo_author"), + }, + }, + }, + quoting: { + with: { + attachments: true, + emojis: { + with: { + emoji: { + with: { + instance: true, + }, + }, + }, + }, + likes: true, + application: true, + mentions: { + with: { + user: { + with: { + instance: true, + relationships: true, + }, + }, + }, + }, + author: { + with: { + ...userRelations, + }, + extras: userExtrasTemplate("status_quoting_author"), + }, + }, + }, + }, + extras: { + ...statusExtras, + ...query?.extras, + }, + }); + + if (!output) return null; + + return { + ...output, + author: transformOutputToUserWithRelations(output.author), + mentions: output.mentions.map((mention) => + transformOutputToUserWithRelations(mention.user), + ), + reblog: output.reblog && { + ...output.reblog, + author: transformOutputToUserWithRelations(output.reblog.author), + emojis: output.reblog.emojis.map( + (emoji) => + (emoji as unknown as Record) + .emoji as EmojiWithInstance, + ), + }, + inReplyTo: output.inReplyTo && { + ...output.inReplyTo, + author: transformOutputToUserWithRelations(output.inReplyTo.author), + emojis: output.inReplyTo.emojis.map( + (emoji) => + (emoji as unknown as Record) + .emoji as EmojiWithInstance, + ), + }, + quoting: output.quoting && { + ...output.quoting, + author: transformOutputToUserWithRelations(output.quoting.author), + emojis: output.quoting.emojis.map( + (emoji) => + (emoji as unknown as Record) + .emoji as EmojiWithInstance, + ), + }, + emojis: (output.emojis ?? []).map( + (emoji) => + (emoji as unknown as Record) + .emoji as EmojiWithInstance, + ), + reblogCount: Number(output.reblogCount), + likeCount: Number(output.likeCount), + replyCount: Number(output.replyCount), + }; }; export const resolveStatus = async ( @@ -71,12 +484,9 @@ export const resolveStatus = async ( throw new Error("No URI or note provided"); } - // Check if status not already in database - const foundStatus = await client.status.findUnique({ - where: { - uri: uri ?? providedNote?.uri, - }, - include: statusAndUserRelations, + const foundStatus = await findFirstStatuses({ + where: (status, { eq }) => + eq(status.uri, uri ?? providedNote?.uri ?? ""), }); if (foundStatus) return foundStatus; @@ -145,7 +555,7 @@ export const resolveStatus = async ( } } - return await createNewStatus( + const createdStatus = await createNewStatus( author, note.content ?? { "text/plain": { @@ -168,6 +578,12 @@ export const resolveStatus = async ( note.replies_to ? await resolveStatus(note.replies_to) : undefined, note.quotes ? await resolveStatus(note.quotes) : undefined, ); + + if (!createdStatus) { + throw new Error("Failed to create status"); + } + + return createdStatus; }; /** @@ -175,18 +591,16 @@ export const resolveStatus = async ( */ export const getAncestors = async ( status: StatusWithRelations, - fetcher: UserWithRelations | null, + fetcher: UserWithRelationsAndRelationships | null, ) => { const ancestors: StatusWithRelations[] = []; let currentStatus = status; while (currentStatus.inReplyToPostId) { - const parent = await client.status.findFirst({ - where: { - id: currentStatus.inReplyToPostId, - }, - include: statusAndUserRelations, + const parent = await findFirstStatuses({ + where: (status, { eq }) => + eq(status.id, currentStatus.inReplyToPostId ?? ""), }); if (!parent) break; @@ -197,7 +611,6 @@ export const getAncestors = async ( } // Filter for posts that are viewable by the user - const viewableAncestors = ancestors.filter((ancestor) => isViewableByUser(ancestor, fetcher), ); @@ -210,7 +623,7 @@ export const getAncestors = async ( */ export const getDescendants = async ( status: StatusWithRelations, - fetcher: UserWithRelations | null, + fetcher: UserWithRelationsAndRelationships | null, depth = 0, ) => { const descendants: StatusWithRelations[] = []; @@ -219,11 +632,8 @@ export const getDescendants = async ( // Fetch all children of children of children recursively calling getDescendants - const children = await client.status.findMany({ - where: { - inReplyToPostId: currentStatus.id, - }, - include: statusAndUserRelations, + const children = await findManyStatuses({ + where: (status, { eq }) => eq(status.inReplyToPostId, currentStatus.id), }); for (const child of children) { @@ -252,34 +662,52 @@ export const getDescendants = async ( * @param text The text to parse mentions from. * @returns An array of users mentioned in the text. */ -export const parseTextMentions = async (text: string) => { +export const parseTextMentions = async ( + text: string, +): Promise => { const mentionedPeople = text.match(/@[a-zA-Z0-9_]+(@[a-zA-Z0-9_.:]+)?/g) ?? []; - const found = await client.user.findMany({ - where: { - OR: mentionedPeople.map((person) => ({ - username: person.split("@")[1], - instance: - person.split("@").length > 2 - ? { - base_url: person.split("@")[2], - } - : null, - })), - }, - include: userRelations, - }); + if (mentionedPeople.length === 0) return []; + + const found = await db + .select({ + id: user.id, + username: user.username, + baseUrl: instance.baseUrl, + }) + .from(user) + .innerJoin(instance, eq(user.instanceId, instance.id)) + .where( + or( + ...mentionedPeople.map((person) => + and( + eq(user.username, person.split("@")[1]), + person.split("@").length > 2 + ? eq(instance.baseUrl, person.split("@")[2]) + : undefined, + ), + ), + ), + ); const notFound = mentionedPeople.filter( (person) => !found.find( (user) => user.username === person.split("@")[1] && - user.instance?.base_url === person.split("@")[2], + user.baseUrl === person.split("@")[2], ), ); + const finalList = await findManyUsers({ + where: (user, { inArray }) => + inArray( + user.username, + found.map((f) => f.username), + ), + }); + // Attempt to resolve mentions that were not found for (const person of notFound) { if (person.split("@").length < 2) continue; @@ -290,11 +718,11 @@ export const parseTextMentions = async (text: string) => { ); if (user) { - found.push(user); + finalList.push(user); } } - return found; + return finalList; }; export const replaceTextMentions = async ( @@ -306,10 +734,10 @@ export const replaceTextMentions = async ( // Replace @username and @username@domain if (mention.instanceId) { finalText = finalText.replace( - `@${mention.username}@${mention.instance?.base_url}`, + `@${mention.username}@${mention.instance?.baseUrl}`, `@${mention.username}@${mention.instance?.base_url}`, + )}">@${mention.username}@${mention.instance?.baseUrl}`, ); } else { finalText = finalText.replace( @@ -334,14 +762,14 @@ export const createNewStatus = async ( visibility: APIStatus["visibility"], is_sensitive: boolean, spoiler_text: string, - emojis: Emoji[], + emojis: EmojiWithInstance[], uri?: string, mentions?: UserWithRelations[], /** List of IDs of database Attachment objects */ media_attachments?: string[], inReplyTo?: StatusWithRelations, quoting?: StatusWithRelations, -) => { +): Promise => { let htmlContent: string; if (content["text/html"]) { @@ -384,50 +812,66 @@ export const createNewStatus = async ( ); } - const status = await client.status.create({ - data: { - authorId: author.id, - content: htmlContent, - contentSource: - content["text/plain"]?.content || - content["text/markdown"]?.content || - "", - contentType: "text/html", - visibility: visibility, - sensitive: is_sensitive, - spoilerText: spoiler_text, - emojis: { - connect: foundEmojis.map((emoji) => { - return { - id: emoji.id, - }; - }), - }, - attachments: media_attachments - ? { - connect: media_attachments.map((attachment) => { - return { - id: attachment, - }; - }), - } - : undefined, - inReplyToPostId: inReplyTo?.id, - quotingPostId: quoting?.id, - instanceId: author.instanceId || undefined, - uri: uri || null, - mentions: { - connect: mentions?.map((mention) => { - return { - id: mention.id, - }; - }), - }, - }, - include: statusAndUserRelations, - }); + const newStatus = ( + await db + .insert(status) + .values({ + authorId: author.id, + content: htmlContent, + contentSource: + content["text/plain"]?.content || + content["text/markdown"]?.content || + "", + contentType: "text/html", + visibility: visibility, + sensitive: is_sensitive, + spoilerText: spoiler_text, + instanceId: author.instanceId || null, + uri: uri || null, + inReplyToPostId: inReplyTo?.id, + quotingPostId: quoting?.id, + updatedAt: new Date().toISOString(), + }) + .returning() + )[0]; - return status; + // Connect emojis + for (const emoji of foundEmojis) { + await db + .insert(emojiToStatus) + .values({ + a: emoji.id, + b: newStatus.id, + }) + .execute(); + } + + // Connect mentions + for (const mention of mentions ?? []) { + await db + .insert(statusToUser) + .values({ + a: newStatus.id, + b: mention.id, + }) + .execute(); + } + + // Set attachment parents + if (media_attachments && media_attachments.length > 0) { + await db + .update(attachment) + .set({ + statusId: newStatus.id, + }) + .where(inArray(attachment.id, media_attachments)); + } + + return ( + (await findFirstStatuses({ + where: (status, { eq }) => eq(status.id, newStatus.id), + })) || null + ); }; export const federateStatus = async (status: StatusWithRelations) => { @@ -453,77 +897,72 @@ export const federateStatus = async (status: StatusWithRelations) => { } }; -export const getUsersToFederateTo = async (status: StatusWithRelations) => { - return await client.user.findMany({ - where: { - OR: [ - ["public", "unlisted", "private"].includes(status.visibility) - ? { - relationships: { - some: { - subjectId: status.authorId, - following: true, - }, - }, - instanceId: { - not: null, - }, - } - : {}, - // Mentioned users - { - id: { - in: status.mentions.map((m) => m.id), - }, - instanceId: { - not: null, - }, - }, - ], +export const getUsersToFederateTo = async ( + status: StatusWithRelations, +): Promise => { + // Mentioned users + const mentionedUsers = + status.mentions.length > 0 + ? await findManyUsers({ + where: (user, { or, and, isNotNull, eq, inArray }) => + and( + isNotNull(user.instanceId), + inArray( + user.id, + status.mentions.map((mention) => mention.id), + ), + ), + with: { + ...userRelations, + }, + }) + : []; + + 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; }; export const editStatus = async ( - status: StatusWithRelations, + statusToEdit: StatusWithRelations, data: { content: string; visibility?: APIStatus["visibility"]; sensitive: boolean; spoiler_text: string; - emojis?: Emoji[]; + emojis?: EmojiWithInstance[]; content_type?: string; uri?: string; mentions?: User[]; media_attachments?: string[]; }, -) => { - // Get people mentioned in the content (match @username or @username@domain.com mentions - const mentionedPeople = - data.content.match(/@[a-zA-Z0-9_]+(@[a-zA-Z0-9_]+)?/g) ?? []; - - let mentions = data.mentions || []; +): Promise => { + const mentions = await parseTextMentions(data.content); // Parse emojis const emojis = await parseEmojis(data.content); data.emojis = data.emojis ? [...data.emojis, ...emojis] : emojis; - // Get list of mentioned users - if (mentions.length === 0) { - mentions = await client.user.findMany({ - where: { - OR: mentionedPeople.map((person) => ({ - username: person.split("@")[1], - instance: { - base_url: person.split("@")[2], - }, - })), - }, - include: userRelations, - }); - } - let formattedContent = ""; // Get HTML version of content @@ -544,53 +983,62 @@ export const editStatus = async ( .join("\n"); } - const newStatus = await client.status.update({ - where: { - id: status.id, - }, - data: { - content: formattedContent, - contentSource: data.content, - contentType: data.content_type, - visibility: data.visibility, - sensitive: data.sensitive, - spoilerText: data.spoiler_text, - emojis: { - connect: data.emojis.map((emoji) => { - return { - id: emoji.id, - }; - }), - }, - attachments: data.media_attachments - ? { - connect: data.media_attachments.map((attachment) => { - return { - id: attachment, - }; - }), - } - : undefined, - mentions: { - connect: mentions.map((mention) => { - return { - id: mention.id, - }; - }), - }, - }, - include: statusAndUserRelations, - }); + const updated = ( + await db + .update(status) + .set({ + content: formattedContent, + contentSource: data.content, + contentType: data.content_type, + visibility: data.visibility, + sensitive: data.sensitive, + spoilerText: data.spoiler_text, + }) + .where(eq(status.id, statusToEdit.id)) + .returning() + )[0]; - return newStatus; + // Connect emojis + for (const emoji of data.emojis) { + await db + .insert(emojiToStatus) + .values({ + a: emoji.id, + b: updated.id, + }) + .execute(); + } + + // Connect mentions + for (const mention of mentions) { + await db + .insert(statusToUser) + .values({ + a: updated.id, + b: mention.id, + }) + .execute(); + } + + // Set attachment parents + await db + .update(attachment) + .set({ + statusId: updated.id, + }) + .where(inArray(attachment.id, data.media_attachments ?? [])); + + return ( + (await findFirstStatuses({ + where: (status, { eq }) => eq(status.id, updated.id), + })) || null + ); }; export const isFavouritedBy = async (status: Status, user: User) => { - return !!(await client.like.findFirst({ - where: { - likerId: user.id, - likedId: status.id, - }, + return !!(await db.query.like.findFirst({ + where: (like, { and, eq }) => + and(eq(like.likerId, user.id), eq(like.likedId, status.id)), })); }; @@ -599,73 +1047,98 @@ export const isFavouritedBy = async (status: Status, user: User) => { * @returns A promise that resolves with the API status. */ export const statusToAPI = async ( - status: StatusWithRelations, - user?: UserWithRelations, + statusToConvert: StatusWithRelations, + userFetching?: UserWithRelations, ): Promise => { + const wasPinnedByUser = userFetching + ? !!(await db.query.statusToUser.findFirst({ + where: (relation, { and, eq }) => + and( + eq(relation.a, statusToConvert.id), + eq(relation.b, userFetching?.id), + ), + })) + : false; + + const wasRebloggedByUser = userFetching + ? !!(await db.query.status.findFirst({ + where: (status, { eq, and }) => + and( + eq(status.authorId, userFetching?.id), + eq(status.reblogId, statusToConvert.id), + ), + })) + : false; + + const wasMutedByUser = userFetching + ? !!(await db.query.relationship.findFirst({ + where: (relationship, { and, eq }) => + and( + eq(relationship.ownerId, userFetching.id), + eq(relationship.subjectId, statusToConvert.authorId), + eq(relationship.muting, true), + ), + })) + : false; + return { - id: status.id, - in_reply_to_id: status.inReplyToPostId || null, - in_reply_to_account_id: status.inReplyToPost?.authorId || null, - // @ts-expect-error Prisma TypeScript types dont include relations - account: userToAPI(status.author), - created_at: new Date(status.createdAt).toISOString(), - application: status.application - ? applicationToAPI(status.application) + id: statusToConvert.id, + in_reply_to_id: statusToConvert.inReplyToPostId || null, + in_reply_to_account_id: statusToConvert.inReplyTo?.authorId || null, + account: userToAPI(statusToConvert.author), + created_at: new Date(statusToConvert.createdAt).toISOString(), + application: statusToConvert.application + ? applicationToAPI(statusToConvert.application) : null, card: null, - content: status.content, - emojis: status.emojis.map((emoji) => emojiToAPI(emoji)), - favourited: !!(status.likes ?? []).find( - (like) => like.likerId === user?.id, + content: statusToConvert.content, + emojis: statusToConvert.emojis.map((emoji) => emojiToAPI(emoji)), + favourited: !!(statusToConvert.likes ?? []).find( + (like) => like.likerId === userFetching?.id, ), - favourites_count: (status.likes ?? []).length, - media_attachments: (status.attachments ?? []).map( + favourites_count: (statusToConvert.likes ?? []).length, + media_attachments: (statusToConvert.attachments ?? []).map( (a) => attachmentToAPI(a) as APIAttachment, ), - // @ts-expect-error Prisma TypeScript types dont include relations - mentions: status.mentions.map((mention) => userToAPI(mention)), + mentions: statusToConvert.mentions.map((mention) => userToAPI(mention)), language: null, - muted: user - ? user.relationships.find((r) => r.subjectId === status.authorId) - ?.muting || false - : false, - pinned: status.pinnedBy.find((u) => u.id === user?.id) ? true : false, + muted: wasMutedByUser, + pinned: wasPinnedByUser, // TODO: Add polls poll: null, - reblog: status.reblog - ? await statusToAPI(status.reblog as unknown as StatusWithRelations) + reblog: statusToConvert.reblog + ? await statusToAPI( + statusToConvert.reblog as unknown as StatusWithRelations, + userFetching, + ) : null, - reblogged: !!(await client.status.findFirst({ - where: { - authorId: user?.id, - reblogId: status.id, - }, - })), - reblogs_count: status._count.reblogs, - replies_count: status._count.replies, - sensitive: status.sensitive, - spoiler_text: status.spoilerText, + reblogged: wasRebloggedByUser, + reblogs_count: statusToConvert.reblogCount, + replies_count: statusToConvert.replyCount, + sensitive: statusToConvert.sensitive, + spoiler_text: statusToConvert.spoilerText, tags: [], uri: - status.uri || + statusToConvert.uri || new URL( - `/@${status.author.username}/${status.id}`, + `/@${statusToConvert.author.username}/${statusToConvert.id}`, config.http.base_url, ).toString(), visibility: "public", url: - status.uri || + statusToConvert.uri || new URL( - `/@${status.author.username}/${status.id}`, + `/@${statusToConvert.author.username}/${statusToConvert.id}`, config.http.base_url, ).toString(), bookmarked: false, - quote: status.quotingPost + quote: statusToConvert.quoting ? await statusToAPI( - status.quotingPost as unknown as StatusWithRelations, + statusToConvert.quoting as unknown as StatusWithRelations, + userFetching, ) : null, - quote_id: status.quotingPost?.id || undefined, + quote_id: statusToConvert.quotingPostId || undefined, }; }; @@ -693,13 +1166,13 @@ export const statusToLysand = (status: StatusWithRelations): Lysand.Note => { content: htmlToText(status.content), }, }, - attachments: status.attachments.map((attachment) => + attachments: (status.attachments ?? []).map((attachment) => attachmentToLysand(attachment), ), is_sensitive: status.sensitive, mentions: status.mentions.map((mention) => mention.uri || ""), - quotes: getStatusUri(status.quotingPost) ?? undefined, - replies_to: getStatusUri(status.inReplyToPost) ?? undefined, + quotes: getStatusUri(status.quoting) ?? undefined, + replies_to: getStatusUri(status.inReplyTo) ?? undefined, subject: status.spoilerText, visibility: status.visibility as Lysand.Visibility, extensions: { diff --git a/database/entities/User.ts b/database/entities/User.ts index 7cd40f94..b35e4b61 100644 --- a/database/entities/User.ts +++ b/database/entities/User.ts @@ -1,35 +1,128 @@ import { addUserToMeilisearch } from "@meilisearch"; -import type { User } from "@prisma/client"; -import { Prisma } from "@prisma/client"; import { type Config, config } from "config-manager"; import { htmlToText } from "html-to-text"; -import { client } from "~database/datasource"; import type { APIAccount } from "~types/entities/account"; import type { APISource } from "~types/entities/source"; import type * as Lysand from "lysand-types"; -import { fetchEmoji, emojiToAPI, emojiToLysand } from "./Emoji"; +import { + fetchEmoji, + emojiToAPI, + emojiToLysand, + type EmojiWithInstance, +} from "./Emoji"; import { addInstanceIfNotExists } from "./Instance"; -import { userRelations } from "./relations"; import { createNewRelationship } from "./Relationship"; import { getBestContentType, urlToContentFormat } from "@content_types"; import { objectToInboxRequest } from "./Federation"; +import { and, eq, sql, type InferSelectModel } from "drizzle-orm"; +import { + emojiToUser, + instance, + notification, + relationship, + user, +} from "~drizzle/schema"; +import { db } from "~drizzle/db"; + +export type User = InferSelectModel & { + endpoints?: Partial<{ + dislikes: string; + featured: string; + likes: string; + followers: string; + following: string; + inbox: string; + outbox: string; + }>; +}; + +export type UserWithRelations = User & { + instance: InferSelectModel | null; + emojis: EmojiWithInstance[]; + followerCount: number; + followingCount: number; + statusCount: number; +}; + +export type UserWithRelationsAndRelationships = UserWithRelations & { + relationships: InferSelectModel[]; + relationshipSubjects: InferSelectModel[]; +}; + +export const userRelations: { + instance: true; + emojis: { + with: { + emoji: { + with: { + instance: true; + }; + }; + }; + }; +} = { + instance: true, + emojis: { + with: { + emoji: { + with: { + instance: true, + }, + }, + }, + }, +}; + +export const userExtras = { + followerCount: + sql`(SELECT COUNT(*) FROM "Relationship" "relationships" WHERE ("relationships"."ownerId" = "user".id AND "relationships"."following" = true))`.as( + "follower_count", + ), + followingCount: + sql`(SELECT COUNT(*) FROM "Relationship" "relationshipSubjects" WHERE ("relationshipSubjects"."subjectId" = "user".id AND "relationshipSubjects"."following" = true))`.as( + "following_count", + ), + statusCount: + sql`(SELECT COUNT(*) FROM "Status" "statuses" WHERE "statuses"."authorId" = "user".id)`.as( + "status_count", + ), +}; + +export const userExtrasTemplate = (name: string) => ({ + // @ts-ignore + followerCount: sql([ + `(SELECT COUNT(*) FROM "Relationship" "relationships" WHERE ("relationships"."ownerId" = "${name}".id AND "relationships"."following" = true))`, + ]).as("follower_count"), + // @ts-ignore + followingCount: sql([ + `(SELECT COUNT(*) FROM "Relationship" "relationshipSubjects" WHERE ("relationshipSubjects"."subjectId" = "${name}".id AND "relationshipSubjects"."following" = true))`, + ]).as("following_count"), + // @ts-ignore + statusCount: sql([ + `(SELECT COUNT(*) FROM "Status" "statuses" WHERE "statuses"."authorId" = "${name}".id)`, + ]).as("status_count"), +}); + +/* const a = await db.query.user.findFirst({ + with: { + instance: true, + emojis: { + with: { + instance: true, + }, + }, + }, + extras: { + // + followerCount: sql`SELECT COUNT(*) FROM relationship WHERE owner_id = user.id AND following = true`, + }, +}); */ export interface AuthData { user: UserWithRelations | null; token: string; } -/** - * Represents a user in the database. - * Stores local and remote users - */ - -const userRelations2 = Prisma.validator()({ - include: userRelations, -}); - -export type UserWithRelations = Prisma.UserGetPayload; - /** * Get the user's avatar in raw URL format * @param config The config to use @@ -68,19 +161,22 @@ export const followRequestUser = async ( reblogs = false, notify = false, languages: string[] = [], -) => { +): Promise> => { const isRemote = follower.instanceId !== followee.instanceId; - const relationship = await client.relationship.update({ - where: { id: relationshipId }, - data: { - following: isRemote ? false : !followee.isLocked, - requested: isRemote ? true : followee.isLocked, - showingReblogs: reblogs, - notifying: notify, - languages: languages, - }, - }); + const updatedRelationship = ( + await db + .update(relationship) + .set({ + following: isRemote ? false : !followee.isLocked, + requested: isRemote ? true : followee.isLocked, + showingReblogs: reblogs, + notifying: notify, + languages: languages, + }) + .where(eq(relationship.id, relationshipId)) + .returning() + )[0]; if (isRemote) { // Federate @@ -100,35 +196,26 @@ export const followRequestUser = async ( `Failed to federate follow request from ${follower.id} to ${followee.uri}`, ); - return await client.relationship.update({ - where: { id: relationshipId }, - data: { - following: false, - requested: false, - }, - }); + return ( + await db + .update(relationship) + .set({ + following: false, + requested: false, + }) + .where(eq(relationship.id, relationshipId)) + .returning() + )[0]; } } else { - if (followee.isLocked) { - await client.notification.create({ - data: { - accountId: follower.id, - type: "follow_request", - notifiedId: followee.id, - }, - }); - } else { - await client.notification.create({ - data: { - accountId: follower.id, - type: "follow", - notifiedId: followee.id, - }, - }); - } + await db.insert(notification).values({ + accountId: followee.id, + type: followee.isLocked ? "follow_request" : "follow", + notifiedId: follower.id, + }); } - return relationship; + return updatedRelationship; }; export const sendFollowAccept = async (follower: User, followee: User) => { @@ -169,13 +256,88 @@ export const sendFollowReject = async (follower: User, followee: User) => { } }; -export const resolveUser = async (uri: string) => { - // Check if user not already in database - const foundUser = await client.user.findUnique({ - where: { - uri, +export const transformOutputToUserWithRelations = ( + user: Omit & { + followerCount: unknown; + followingCount: unknown; + statusCount: unknown; + emojis: { + a: string; + b: string; + emoji?: EmojiWithInstance; + }[]; + instance: InferSelectModel | null; + endpoints: unknown; + }, +): UserWithRelations => { + return { + ...user, + followerCount: Number(user.followerCount), + followingCount: Number(user.followingCount), + statusCount: Number(user.statusCount), + endpoints: + user.endpoints ?? + ({} as Partial<{ + dislikes: string; + featured: string; + likes: string; + followers: string; + following: string; + inbox: string; + outbox: string; + }>), + emojis: user.emojis.map( + (emoji) => + (emoji as unknown as Record) + .emoji as EmojiWithInstance, + ), + }; +}; + +export const findManyUsers = async ( + query: Parameters[0], +): Promise => { + const output = await db.query.user.findMany({ + ...query, + with: { + ...userRelations, + ...query?.with, }, - include: userRelations, + extras: { + ...userExtras, + ...query?.extras, + }, + }); + + return output.map((user) => transformOutputToUserWithRelations(user)); +}; + +export const findFirstUser = async ( + query: Parameters[0], +): Promise => { + const output = await db.query.user.findFirst({ + ...query, + with: { + ...userRelations, + ...query?.with, + }, + extras: { + ...userExtras, + ...query?.extras, + }, + }); + + if (!output) return null; + + return transformOutputToUserWithRelations(output); +}; + +export const resolveUser = async ( + uri: string, +): Promise => { + // Check if user not already in database + const foundUser = await findFirstUser({ + where: (user, { eq }) => eq(user.uri, uri), }); if (foundUser) return foundUser; @@ -192,12 +354,12 @@ export const resolveUser = async (uri: string) => { ); } - return client.user.findUnique({ - where: { - id: uuid[0], - }, - include: userRelations, + const foundLocalUser = await findFirstUser({ + where: (user, { eq }) => eq(user.id, uuid[0]), + with: userRelations, }); + + return foundLocalUser || null; } if (!URL.canParse(uri)) { @@ -244,50 +406,62 @@ export const resolveUser = async (uri: string) => { emojis.push(await fetchEmoji(emoji)); } - const user = await client.user.create({ - data: { - username: data.username, - uri: data.uri, - createdAt: new Date(data.created_at), - endpoints: { - dislikes: data.dislikes, - featured: data.featured, - likes: data.likes, - followers: data.followers, - following: data.following, - inbox: data.inbox, - outbox: data.outbox, - }, - emojis: { - connect: emojis.map((emoji) => ({ - id: emoji.id, - })), - }, - instanceId: instance.id, - avatar: data.avatar - ? Object.entries(data.avatar)[0][1].content - : "", - header: data.header - ? Object.entries(data.header)[0][1].content - : "", - displayName: data.display_name ?? "", - note: getBestContentType(data.bio).content, - publicKey: data.public_key.public_key, - source: { - language: null, - note: "", - privacy: "public", - sensitive: false, - fields: [], - }, - }, - include: userRelations, + const newUser = ( + await db + .insert(user) + .values({ + username: data.username, + uri: data.uri, + createdAt: new Date(data.created_at).toISOString(), + endpoints: { + dislikes: data.dislikes, + featured: data.featured, + likes: data.likes, + followers: data.followers, + following: data.following, + inbox: data.inbox, + outbox: data.outbox, + }, + instanceId: instance.id, + avatar: data.avatar + ? Object.entries(data.avatar)[0][1].content + : "", + header: data.header + ? Object.entries(data.header)[0][1].content + : "", + displayName: data.display_name ?? "", + note: getBestContentType(data.bio).content, + publicKey: data.public_key.public_key, + source: { + language: null, + note: "", + privacy: "public", + sensitive: false, + fields: [], + }, + }) + .returning() + )[0]; + + // Add emojis to user + await db.insert(emojiToUser).values( + emojis.map((emoji) => ({ + a: emoji.id, + b: newUser.id, + })), + ); + + const finalUser = await findFirstUser({ + where: (user, { eq }) => eq(user.id, newUser.id), + with: userRelations, }); - // Add to Meilisearch - await addUserToMeilisearch(user); + if (!finalUser) return null; - return user; + // Add to Meilisearch + await addUserToMeilisearch(finalUser); + + return finalUser; }; export const getUserUri = (user: User) => { @@ -301,19 +475,25 @@ export const getUserUri = (user: User) => { * Resolves a WebFinger identifier to a user. * @param identifier Either a UUID or a username */ -export const resolveWebFinger = async (identifier: string, host: string) => { +export const resolveWebFinger = async ( + identifier: string, + host: string, +): Promise => { // Check if user not already in database - const foundUser = await client.user.findUnique({ - where: { - username: identifier, - instance: { - base_url: host, - }, - }, - include: userRelations, - }); + const foundUser = await db + .select() + .from(user) + .innerJoin(instance, eq(user.instanceId, instance.id)) + .where(and(eq(user.username, identifier), eq(instance.baseUrl, host))) + .limit(1); - if (foundUser) return foundUser; + if (foundUser[0]) + return ( + (await findFirstUser({ + where: (user, { eq }) => eq(user.id, foundUser[0].User.id), + with: userRelations, + })) || null + ); const hostWithProtocol = host.startsWith("http") ? host : `https://${host}`; @@ -383,49 +563,57 @@ export const createNewLocalUser = async (data: { avatar?: string; header?: string; admin?: boolean; -}) => { +}): Promise => { const keys = await generateUserKeys(); - const user = await client.user.create({ - data: { - username: data.username, - displayName: data.display_name ?? data.username, - password: await Bun.password.hash(data.password), - email: data.email, - note: data.bio ?? "", - avatar: data.avatar ?? config.defaults.avatar, - header: data.header ?? config.defaults.avatar, - isAdmin: data.admin ?? false, - publicKey: keys.public_key, - privateKey: keys.private_key, - source: { - language: null, - note: "", - privacy: "public", - sensitive: false, - fields: [], - }, - }, - include: userRelations, + const newUser = ( + await db + .insert(user) + .values({ + username: data.username, + displayName: data.display_name ?? data.username, + password: await Bun.password.hash(data.password), + email: data.email, + note: data.bio ?? "", + avatar: data.avatar ?? config.defaults.avatar, + header: data.header ?? config.defaults.avatar, + isAdmin: data.admin ?? false, + publicKey: keys.public_key, + privateKey: keys.private_key, + updatedAt: new Date().toISOString(), + source: { + language: null, + note: "", + privacy: "public", + sensitive: false, + fields: [], + }, + }) + .returning() + )[0]; + + const finalUser = await findFirstUser({ + where: (user, { eq }) => eq(user.id, newUser.id), + with: userRelations, }); - // Add to Meilisearch - await addUserToMeilisearch(user); + if (!finalUser) return null; - return user; + // Add to Meilisearch + await addUserToMeilisearch(finalUser); + + return finalUser; }; /** * Parses mentions from a list of URIs */ -export const parseMentionsUris = async (mentions: string[]) => { - return await client.user.findMany({ - where: { - uri: { - in: mentions, - }, - }, - include: userRelations, +export const parseMentionsUris = async ( + mentions: string[], +): Promise => { + return await findManyUsers({ + where: (user, { inArray }) => inArray(user.uri, mentions), + with: userRelations, }); }; @@ -434,23 +622,22 @@ export const parseMentionsUris = async (mentions: string[]) => { * @param access_token The access token to retrieve the user from. * @returns The user associated with the given access token. */ -export const retrieveUserFromToken = async (access_token: string) => { +export const retrieveUserFromToken = async ( + access_token: string, +): Promise => { if (!access_token) return null; - const token = await client.token.findFirst({ - where: { - access_token, - }, - include: { - user: { - include: userRelations, - }, - }, + const token = await db.query.token.findFirst({ + where: (tokens, { eq }) => eq(tokens.accessToken, access_token), }); - if (!token) return null; + if (!token || !token.userId) return null; - return token.user; + const user = await findFirstUser({ + where: (user, { eq }) => eq(user.id, token.userId ?? ""), + }); + + return user; }; /** @@ -461,34 +648,24 @@ export const retrieveUserFromToken = async (access_token: string) => { export const getRelationshipToOtherUser = async ( user: UserWithRelations, other: User, -) => { - const relationship = await client.relationship.findFirst({ - where: { - ownerId: user.id, - subjectId: other.id, - }, +): Promise> => { + const foundRelationship = await db.query.relationship.findFirst({ + where: (relationship, { and, eq }) => + and( + eq(relationship.ownerId, user.id), + eq(relationship.subjectId, other.id), + ), }); - if (!relationship) { + if (!foundRelationship) { // Create new relationship const newRelationship = await createNewRelationship(user, other); - await client.user.update({ - where: { id: user.id }, - data: { - relationships: { - connect: { - id: newRelationship.id, - }, - }, - }, - }); - return newRelationship; } - return relationship; + return foundRelationship; }; /** @@ -526,40 +703,42 @@ export const generateUserKeys = async () => { }; export const userToAPI = ( - user: UserWithRelations, + userToConvert: UserWithRelations, isOwnAccount = false, ): APIAccount => { return { - id: user.id, - username: user.username, - display_name: user.displayName, - note: user.note, + id: userToConvert.id, + username: userToConvert.username, + display_name: userToConvert.displayName, + note: userToConvert.note, url: - user.uri || - new URL(`/@${user.username}`, config.http.base_url).toString(), - avatar: getAvatarUrl(user, config), - header: getHeaderUrl(user, config), - locked: user.isLocked, - created_at: new Date(user.createdAt).toISOString(), - followers_count: user.relationshipSubjects.filter((r) => r.following) - .length, - following_count: user.relationships.filter((r) => r.following).length, - statuses_count: user._count.statuses, - emojis: user.emojis.map((emoji) => emojiToAPI(emoji)), + userToConvert.uri || + new URL( + `/@${userToConvert.username}`, + config.http.base_url, + ).toString(), + avatar: getAvatarUrl(userToConvert, config), + header: getHeaderUrl(userToConvert, config), + locked: userToConvert.isLocked, + created_at: new Date(userToConvert.createdAt).toISOString(), + followers_count: userToConvert.followerCount, + following_count: userToConvert.followingCount, + statuses_count: userToConvert.statusCount, + emojis: userToConvert.emojis.map((emoji) => emojiToAPI(emoji)), // TODO: Add fields fields: [], - bot: user.isBot, + bot: userToConvert.isBot, source: - isOwnAccount && user.source - ? (user.source as APISource) + isOwnAccount && userToConvert.source + ? (userToConvert.source as APISource) : undefined, // TODO: Add static avatar and header avatar_static: "", header_static: "", acct: - user.instance === null - ? user.username - : `${user.username}@${user.instance.base_url}`, + userToConvert.instance === null + ? userToConvert.username + : `${userToConvert.username}@${userToConvert.instance.baseUrl}`, // TODO: Add these fields limited: false, moved: null, @@ -569,8 +748,8 @@ export const userToAPI = ( mute_expires_at: undefined, group: false, pleroma: { - is_admin: user.isAdmin, - is_moderator: user.isAdmin, + is_admin: userToConvert.isAdmin, + is_moderator: userToConvert.isAdmin, }, }; }; diff --git a/drizzle-scanned/0000_third_misty_knight.sql b/drizzle-scanned/0000_third_misty_knight.sql new file mode 100644 index 00000000..98c2e90e --- /dev/null +++ b/drizzle-scanned/0000_third_misty_knight.sql @@ -0,0 +1,462 @@ +-- Current sql file was generated after introspecting the database +-- If you want to run this migration please uncomment this code before executing migrations +/* +CREATE TABLE IF NOT EXISTS "_prisma_migrations" ( + "id" varchar(36) PRIMARY KEY NOT NULL, + "checksum" varchar(64) NOT NULL, + "finished_at" timestamp with time zone, + "migration_name" varchar(255) NOT NULL, + "logs" text, + "rolled_back_at" timestamp with time zone, + "started_at" timestamp with time zone DEFAULT now() NOT NULL, + "applied_steps_count" integer DEFAULT 0 NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Emoji" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "shortcode" text NOT NULL, + "url" text NOT NULL, + "visible_in_picker" boolean NOT NULL, + "instanceId" uuid, + "alt" text, + "content_type" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Like" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "likerId" uuid NOT NULL, + "likedId" uuid NOT NULL, + "createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "LysandObject" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "remote_id" text NOT NULL, + "type" text NOT NULL, + "uri" text NOT NULL, + "created_at" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "authorId" uuid, + "extra_data" jsonb NOT NULL, + "extensions" jsonb NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Relationship" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "ownerId" uuid NOT NULL, + "subjectId" uuid NOT NULL, + "following" boolean NOT NULL, + "showingReblogs" boolean NOT NULL, + "notifying" boolean NOT NULL, + "followedBy" boolean NOT NULL, + "blocking" boolean NOT NULL, + "blockedBy" boolean NOT NULL, + "muting" boolean NOT NULL, + "mutingNotifications" boolean NOT NULL, + "requested" boolean NOT NULL, + "domainBlocking" boolean NOT NULL, + "endorsed" boolean NOT NULL, + "languages" text[], + "note" text NOT NULL, + "createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp(3) NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Application" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "name" text NOT NULL, + "website" text, + "vapid_key" text, + "client_id" text NOT NULL, + "secret" text NOT NULL, + "scopes" text NOT NULL, + "redirect_uris" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Token" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "token_type" text NOT NULL, + "scope" text NOT NULL, + "access_token" text NOT NULL, + "code" text NOT NULL, + "created_at" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "userId" uuid, + "applicationId" uuid +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "_EmojiToUser" ( + "A" uuid NOT NULL, + "B" uuid NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "_EmojiToStatus" ( + "A" uuid NOT NULL, + "B" uuid NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "_StatusToUser" ( + "A" uuid NOT NULL, + "B" uuid NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "_UserPinnedNotes" ( + "A" uuid NOT NULL, + "B" uuid NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Attachment" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "url" text NOT NULL, + "remote_url" text, + "thumbnail_url" text, + "mime_type" text NOT NULL, + "description" text, + "blurhash" text, + "sha256" text, + "fps" integer, + "duration" integer, + "width" integer, + "height" integer, + "size" integer, + "statusId" uuid +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Notification" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "type" text NOT NULL, + "createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "notifiedId" uuid NOT NULL, + "accountId" uuid NOT NULL, + "statusId" uuid +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Status" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "uri" text, + "authorId" uuid NOT NULL, + "createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp(3) NOT NULL, + "reblogId" uuid, + "content" text DEFAULT '' NOT NULL, + "contentType" text DEFAULT 'text/plain' NOT NULL, + "visibility" text NOT NULL, + "inReplyToPostId" uuid, + "quotingPostId" uuid, + "instanceId" uuid, + "sensitive" boolean NOT NULL, + "spoilerText" text DEFAULT '' NOT NULL, + "applicationId" uuid, + "contentSource" text DEFAULT '' NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Instance" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "base_url" text NOT NULL, + "name" text NOT NULL, + "version" text NOT NULL, + "logo" jsonb NOT NULL, + "disableAutomoderation" boolean DEFAULT false NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "OpenIdAccount" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "userId" uuid, + "serverId" text NOT NULL, + "issuerId" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "User" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "uri" text, + "username" text NOT NULL, + "displayName" text NOT NULL, + "password" text, + "email" text, + "note" text DEFAULT '' NOT NULL, + "isAdmin" boolean DEFAULT false NOT NULL, + "endpoints" jsonb, + "source" jsonb NOT NULL, + "avatar" text NOT NULL, + "header" text NOT NULL, + "createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp(3) NOT NULL, + "isBot" boolean DEFAULT false NOT NULL, + "isLocked" boolean DEFAULT false NOT NULL, + "isDiscoverable" boolean DEFAULT false NOT NULL, + "sanctions" text[] DEFAULT 'RRAY[', + "publicKey" text NOT NULL, + "privateKey" text, + "instanceId" uuid, + "disableAutomoderation" boolean DEFAULT false NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "OpenIdLoginFlow" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "codeVerifier" text NOT NULL, + "applicationId" uuid, + "issuerId" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Flag" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "flagType" text DEFAULT 'other' NOT NULL, + "createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "flaggeStatusId" uuid, + "flaggedUserId" uuid +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "ModNote" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "notedStatusId" uuid, + "notedUserId" uuid, + "modId" uuid NOT NULL, + "note" text NOT NULL, + "createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "ModTag" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "taggedStatusId" uuid, + "taggedUserId" uuid, + "modId" uuid NOT NULL, + "tag" text NOT NULL, + "createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "LysandObject_remote_id_key" ON "LysandObject" ("remote_id");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "LysandObject_uri_key" ON "LysandObject" ("uri");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "Application_client_id_key" ON "Application" ("client_id");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "_EmojiToUser_AB_unique" ON "_EmojiToUser" ("A","B");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "_EmojiToUser_B_index" ON "_EmojiToUser" ("B");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "_EmojiToStatus_AB_unique" ON "_EmojiToStatus" ("A","B");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "_EmojiToStatus_B_index" ON "_EmojiToStatus" ("B");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "_StatusToUser_AB_unique" ON "_StatusToUser" ("A","B");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "_StatusToUser_B_index" ON "_StatusToUser" ("B");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "_UserPinnedNotes_AB_unique" ON "_UserPinnedNotes" ("A","B");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "_UserPinnedNotes_B_index" ON "_UserPinnedNotes" ("B");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "Status_uri_key" ON "Status" ("uri");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "User_uri_key" ON "User" ("uri");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "User_username_key" ON "User" ("username");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "User_email_key" ON "User" ("email");--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Emoji" ADD CONSTRAINT "Emoji_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "public"."Instance"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Like" ADD CONSTRAINT "Like_likerId_fkey" FOREIGN KEY ("likerId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Like" ADD CONSTRAINT "Like_likedId_fkey" FOREIGN KEY ("likedId") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "LysandObject" ADD CONSTRAINT "LysandObject_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "public"."LysandObject"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Relationship" ADD CONSTRAINT "Relationship_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Relationship" ADD CONSTRAINT "Relationship_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Token" ADD CONSTRAINT "Token_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Token" ADD CONSTRAINT "Token_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "public"."Application"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "_EmojiToUser" ADD CONSTRAINT "_EmojiToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."Emoji"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "_EmojiToUser" ADD CONSTRAINT "_EmojiToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "_EmojiToStatus" ADD CONSTRAINT "_EmojiToStatus_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."Emoji"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "_EmojiToStatus" ADD CONSTRAINT "_EmojiToStatus_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "_StatusToUser" ADD CONSTRAINT "_StatusToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "_StatusToUser" ADD CONSTRAINT "_StatusToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "_UserPinnedNotes" ADD CONSTRAINT "_UserPinnedNotes_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "_UserPinnedNotes" ADD CONSTRAINT "_UserPinnedNotes_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Attachment" ADD CONSTRAINT "Attachment_statusId_fkey" FOREIGN KEY ("statusId") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Notification" ADD CONSTRAINT "Notification_notifiedId_fkey" FOREIGN KEY ("notifiedId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Notification" ADD CONSTRAINT "Notification_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Notification" ADD CONSTRAINT "Notification_statusId_fkey" FOREIGN KEY ("statusId") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Status" ADD CONSTRAINT "Status_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Status" ADD CONSTRAINT "Status_reblogId_fkey" FOREIGN KEY ("reblogId") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Status" ADD CONSTRAINT "Status_inReplyToPostId_fkey" FOREIGN KEY ("inReplyToPostId") REFERENCES "public"."Status"("id") ON DELETE set null ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Status" ADD CONSTRAINT "Status_quotingPostId_fkey" FOREIGN KEY ("quotingPostId") REFERENCES "public"."Status"("id") ON DELETE set null ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Status" ADD CONSTRAINT "Status_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "public"."Instance"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Status" ADD CONSTRAINT "Status_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "public"."Application"("id") ON DELETE set null ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "OpenIdAccount" ADD CONSTRAINT "OpenIdAccount_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE set null ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "User" ADD CONSTRAINT "User_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "public"."Instance"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "OpenIdLoginFlow" ADD CONSTRAINT "OpenIdLoginFlow_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "public"."Application"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Flag" ADD CONSTRAINT "Flag_flaggeStatusId_fkey" FOREIGN KEY ("flaggeStatusId") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Flag" ADD CONSTRAINT "Flag_flaggedUserId_fkey" FOREIGN KEY ("flaggedUserId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "ModNote" ADD CONSTRAINT "ModNote_notedStatusId_fkey" FOREIGN KEY ("notedStatusId") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "ModNote" ADD CONSTRAINT "ModNote_notedUserId_fkey" FOREIGN KEY ("notedUserId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "ModNote" ADD CONSTRAINT "ModNote_modId_fkey" FOREIGN KEY ("modId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "ModTag" ADD CONSTRAINT "ModTag_taggedStatusId_fkey" FOREIGN KEY ("taggedStatusId") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "ModTag" ADD CONSTRAINT "ModTag_taggedUserId_fkey" FOREIGN KEY ("taggedUserId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "ModTag" ADD CONSTRAINT "ModTag_modId_fkey" FOREIGN KEY ("modId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +*/ \ No newline at end of file diff --git a/drizzle-scanned/meta/0000_snapshot.json b/drizzle-scanned/meta/0000_snapshot.json new file mode 100644 index 00000000..a014f406 --- /dev/null +++ b/drizzle-scanned/meta/0000_snapshot.json @@ -0,0 +1,1867 @@ +{ + "id": "00000000-0000-0000-0000-000000000000", + "prevId": "", + "version": "5", + "dialect": "pg", + "tables": { + "_prisma_migrations": { + "name": "_prisma_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(36)", + "primaryKey": true, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "migration_name": { + "name": "migration_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "logs": { + "name": "logs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rolled_back_at": { + "name": "rolled_back_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "applied_steps_count": { + "name": "applied_steps_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Emoji": { + "name": "Emoji", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "shortcode": { + "name": "shortcode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visible_in_picker": { + "name": "visible_in_picker", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "instanceId": { + "name": "instanceId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "alt": { + "name": "alt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "Emoji_instanceId_fkey": { + "name": "Emoji_instanceId_fkey", + "tableFrom": "Emoji", + "tableTo": "Instance", + "schemaTo": "public", + "columnsFrom": [ + "instanceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Like": { + "name": "Like", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "likerId": { + "name": "likerId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "likedId": { + "name": "likedId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "Like_likerId_fkey": { + "name": "Like_likerId_fkey", + "tableFrom": "Like", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "likerId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Like_likedId_fkey": { + "name": "Like_likedId_fkey", + "tableFrom": "Like", + "tableTo": "Status", + "schemaTo": "public", + "columnsFrom": [ + "likedId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "LysandObject": { + "name": "LysandObject", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "remote_id": { + "name": "remote_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "authorId": { + "name": "authorId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "extra_data": { + "name": "extra_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "extensions": { + "name": "extensions", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "LysandObject_remote_id_key": { + "name": "LysandObject_remote_id_key", + "columns": [ + "remote_id" + ], + "isUnique": true + }, + "LysandObject_uri_key": { + "name": "LysandObject_uri_key", + "columns": [ + "uri" + ], + "isUnique": true + } + }, + "foreignKeys": { + "LysandObject_authorId_fkey": { + "name": "LysandObject_authorId_fkey", + "tableFrom": "LysandObject", + "tableTo": "LysandObject", + "schemaTo": "public", + "columnsFrom": [ + "authorId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Relationship": { + "name": "Relationship", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "ownerId": { + "name": "ownerId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "subjectId": { + "name": "subjectId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "following": { + "name": "following", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "showingReblogs": { + "name": "showingReblogs", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "notifying": { + "name": "notifying", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "followedBy": { + "name": "followedBy", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "blocking": { + "name": "blocking", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "blockedBy": { + "name": "blockedBy", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "muting": { + "name": "muting", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "mutingNotifications": { + "name": "mutingNotifications", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "requested": { + "name": "requested", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "domainBlocking": { + "name": "domainBlocking", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "endorsed": { + "name": "endorsed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "languages": { + "name": "languages", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "Relationship_ownerId_fkey": { + "name": "Relationship_ownerId_fkey", + "tableFrom": "Relationship", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "ownerId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Relationship_subjectId_fkey": { + "name": "Relationship_subjectId_fkey", + "tableFrom": "Relationship", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "subjectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Application": { + "name": "Application", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vapid_key": { + "name": "vapid_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "Application_client_id_key": { + "name": "Application_client_id_key", + "columns": [ + "client_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Token": { + "name": "Token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Token_userId_fkey": { + "name": "Token_userId_fkey", + "tableFrom": "Token", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Token_applicationId_fkey": { + "name": "Token_applicationId_fkey", + "tableFrom": "Token", + "tableTo": "Application", + "schemaTo": "public", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "_EmojiToUser": { + "name": "_EmojiToUser", + "schema": "", + "columns": { + "A": { + "name": "A", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "B": { + "name": "B", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "_EmojiToUser_AB_unique": { + "name": "_EmojiToUser_AB_unique", + "columns": [ + "A", + "B" + ], + "isUnique": true + }, + "_EmojiToUser_B_index": { + "name": "_EmojiToUser_B_index", + "columns": [ + "B" + ], + "isUnique": false + } + }, + "foreignKeys": { + "_EmojiToUser_A_fkey": { + "name": "_EmojiToUser_A_fkey", + "tableFrom": "_EmojiToUser", + "tableTo": "Emoji", + "schemaTo": "public", + "columnsFrom": [ + "A" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "_EmojiToUser_B_fkey": { + "name": "_EmojiToUser_B_fkey", + "tableFrom": "_EmojiToUser", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "B" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "_EmojiToStatus": { + "name": "_EmojiToStatus", + "schema": "", + "columns": { + "A": { + "name": "A", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "B": { + "name": "B", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "_EmojiToStatus_AB_unique": { + "name": "_EmojiToStatus_AB_unique", + "columns": [ + "A", + "B" + ], + "isUnique": true + }, + "_EmojiToStatus_B_index": { + "name": "_EmojiToStatus_B_index", + "columns": [ + "B" + ], + "isUnique": false + } + }, + "foreignKeys": { + "_EmojiToStatus_A_fkey": { + "name": "_EmojiToStatus_A_fkey", + "tableFrom": "_EmojiToStatus", + "tableTo": "Emoji", + "schemaTo": "public", + "columnsFrom": [ + "A" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "_EmojiToStatus_B_fkey": { + "name": "_EmojiToStatus_B_fkey", + "tableFrom": "_EmojiToStatus", + "tableTo": "Status", + "schemaTo": "public", + "columnsFrom": [ + "B" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "_StatusToUser": { + "name": "_StatusToUser", + "schema": "", + "columns": { + "A": { + "name": "A", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "B": { + "name": "B", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "_StatusToUser_AB_unique": { + "name": "_StatusToUser_AB_unique", + "columns": [ + "A", + "B" + ], + "isUnique": true + }, + "_StatusToUser_B_index": { + "name": "_StatusToUser_B_index", + "columns": [ + "B" + ], + "isUnique": false + } + }, + "foreignKeys": { + "_StatusToUser_A_fkey": { + "name": "_StatusToUser_A_fkey", + "tableFrom": "_StatusToUser", + "tableTo": "Status", + "schemaTo": "public", + "columnsFrom": [ + "A" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "_StatusToUser_B_fkey": { + "name": "_StatusToUser_B_fkey", + "tableFrom": "_StatusToUser", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "B" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "_UserPinnedNotes": { + "name": "_UserPinnedNotes", + "schema": "", + "columns": { + "A": { + "name": "A", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "B": { + "name": "B", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "_UserPinnedNotes_AB_unique": { + "name": "_UserPinnedNotes_AB_unique", + "columns": [ + "A", + "B" + ], + "isUnique": true + }, + "_UserPinnedNotes_B_index": { + "name": "_UserPinnedNotes_B_index", + "columns": [ + "B" + ], + "isUnique": false + } + }, + "foreignKeys": { + "_UserPinnedNotes_A_fkey": { + "name": "_UserPinnedNotes_A_fkey", + "tableFrom": "_UserPinnedNotes", + "tableTo": "Status", + "schemaTo": "public", + "columnsFrom": [ + "A" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "_UserPinnedNotes_B_fkey": { + "name": "_UserPinnedNotes_B_fkey", + "tableFrom": "_UserPinnedNotes", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "B" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Attachment": { + "name": "Attachment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remote_url": { + "name": "remote_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "thumbnail_url": { + "name": "thumbnail_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blurhash": { + "name": "blurhash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fps": { + "name": "fps", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "statusId": { + "name": "statusId", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Attachment_statusId_fkey": { + "name": "Attachment_statusId_fkey", + "tableFrom": "Attachment", + "tableTo": "Status", + "schemaTo": "public", + "columnsFrom": [ + "statusId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Notification": { + "name": "Notification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "notifiedId": { + "name": "notifiedId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "accountId": { + "name": "accountId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "statusId": { + "name": "statusId", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Notification_notifiedId_fkey": { + "name": "Notification_notifiedId_fkey", + "tableFrom": "Notification", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "notifiedId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notification_accountId_fkey": { + "name": "Notification_accountId_fkey", + "tableFrom": "Notification", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "accountId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notification_statusId_fkey": { + "name": "Notification_statusId_fkey", + "tableFrom": "Notification", + "tableTo": "Status", + "schemaTo": "public", + "columnsFrom": [ + "statusId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Status": { + "name": "Status", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorId": { + "name": "authorId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true + }, + "reblogId": { + "name": "reblogId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "contentType": { + "name": "contentType", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text/plain'" + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inReplyToPostId": { + "name": "inReplyToPostId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "quotingPostId": { + "name": "quotingPostId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "instanceId": { + "name": "instanceId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sensitive": { + "name": "sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "spoilerText": { + "name": "spoilerText", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "applicationId": { + "name": "applicationId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "contentSource": { + "name": "contentSource", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": { + "Status_uri_key": { + "name": "Status_uri_key", + "columns": [ + "uri" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Status_authorId_fkey": { + "name": "Status_authorId_fkey", + "tableFrom": "Status", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "authorId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Status_reblogId_fkey": { + "name": "Status_reblogId_fkey", + "tableFrom": "Status", + "tableTo": "Status", + "schemaTo": "public", + "columnsFrom": [ + "reblogId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Status_inReplyToPostId_fkey": { + "name": "Status_inReplyToPostId_fkey", + "tableFrom": "Status", + "tableTo": "Status", + "schemaTo": "public", + "columnsFrom": [ + "inReplyToPostId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Status_quotingPostId_fkey": { + "name": "Status_quotingPostId_fkey", + "tableFrom": "Status", + "tableTo": "Status", + "schemaTo": "public", + "columnsFrom": [ + "quotingPostId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Status_instanceId_fkey": { + "name": "Status_instanceId_fkey", + "tableFrom": "Status", + "tableTo": "Instance", + "schemaTo": "public", + "columnsFrom": [ + "instanceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Status_applicationId_fkey": { + "name": "Status_applicationId_fkey", + "tableFrom": "Status", + "tableTo": "Application", + "schemaTo": "public", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Instance": { + "name": "Instance", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "disableAutomoderation": { + "name": "disableAutomoderation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "OpenIdAccount": { + "name": "OpenIdAccount", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issuerId": { + "name": "issuerId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "OpenIdAccount_userId_fkey": { + "name": "OpenIdAccount_userId_fkey", + "tableFrom": "OpenIdAccount", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "User": { + "name": "User", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "displayName": { + "name": "displayName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "isAdmin": { + "name": "isAdmin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "endpoints": { + "name": "endpoints", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header": { + "name": "header", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true + }, + "isBot": { + "name": "isBot", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isLocked": { + "name": "isLocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isDiscoverable": { + "name": "isDiscoverable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sanctions": { + "name": "sanctions", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'RRAY['" + }, + "publicKey": { + "name": "publicKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "instanceId": { + "name": "instanceId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "disableAutomoderation": { + "name": "disableAutomoderation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "User_uri_key": { + "name": "User_uri_key", + "columns": [ + "uri" + ], + "isUnique": true + }, + "User_username_key": { + "name": "User_username_key", + "columns": [ + "username" + ], + "isUnique": true + }, + "User_email_key": { + "name": "User_email_key", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": { + "User_instanceId_fkey": { + "name": "User_instanceId_fkey", + "tableFrom": "User", + "tableTo": "Instance", + "schemaTo": "public", + "columnsFrom": [ + "instanceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "OpenIdLoginFlow": { + "name": "OpenIdLoginFlow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "codeVerifier": { + "name": "codeVerifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issuerId": { + "name": "issuerId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "OpenIdLoginFlow_applicationId_fkey": { + "name": "OpenIdLoginFlow_applicationId_fkey", + "tableFrom": "OpenIdLoginFlow", + "tableTo": "Application", + "schemaTo": "public", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Flag": { + "name": "Flag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "flagType": { + "name": "flagType", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'other'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "flaggeStatusId": { + "name": "flaggeStatusId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "flaggedUserId": { + "name": "flaggedUserId", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Flag_flaggeStatusId_fkey": { + "name": "Flag_flaggeStatusId_fkey", + "tableFrom": "Flag", + "tableTo": "Status", + "schemaTo": "public", + "columnsFrom": [ + "flaggeStatusId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Flag_flaggedUserId_fkey": { + "name": "Flag_flaggedUserId_fkey", + "tableFrom": "Flag", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "flaggedUserId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "ModNote": { + "name": "ModNote", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "notedStatusId": { + "name": "notedStatusId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "notedUserId": { + "name": "notedUserId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "modId": { + "name": "modId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ModNote_notedStatusId_fkey": { + "name": "ModNote_notedStatusId_fkey", + "tableFrom": "ModNote", + "tableTo": "Status", + "schemaTo": "public", + "columnsFrom": [ + "notedStatusId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModNote_notedUserId_fkey": { + "name": "ModNote_notedUserId_fkey", + "tableFrom": "ModNote", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "notedUserId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModNote_modId_fkey": { + "name": "ModNote_modId_fkey", + "tableFrom": "ModNote", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "modId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "ModTag": { + "name": "ModTag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "taggedStatusId": { + "name": "taggedStatusId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "taggedUserId": { + "name": "taggedUserId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "modId": { + "name": "modId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ModTag_taggedStatusId_fkey": { + "name": "ModTag_taggedStatusId_fkey", + "tableFrom": "ModTag", + "tableTo": "Status", + "schemaTo": "public", + "columnsFrom": [ + "taggedStatusId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModTag_taggedUserId_fkey": { + "name": "ModTag_taggedUserId_fkey", + "tableFrom": "ModTag", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "taggedUserId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModTag_modId_fkey": { + "name": "ModTag_modId_fkey", + "tableFrom": "ModTag", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "modId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/drizzle-scanned/meta/_journal.json b/drizzle-scanned/meta/_journal.json new file mode 100644 index 00000000..3ff690dc --- /dev/null +++ b/drizzle-scanned/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "5", + "dialect": "pg", + "entries": [ + { + "idx": 0, + "version": "5", + "when": 1712812153499, + "tag": "0000_third_misty_knight", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/drizzle-scanned/schema.ts b/drizzle-scanned/schema.ts new file mode 100644 index 00000000..2d3395d1 --- /dev/null +++ b/drizzle-scanned/schema.ts @@ -0,0 +1,292 @@ +import { pgTable, varchar, timestamp, text, integer, foreignKey, uuid, boolean, uniqueIndex, jsonb, index } from "drizzle-orm/pg-core" + import { sql } from "drizzle-orm" + + + +export const prismaMigrations = pgTable("_prisma_migrations", { + id: varchar("id", { length: 36 }).primaryKey().notNull(), + checksum: varchar("checksum", { length: 64 }).notNull(), + finishedAt: timestamp("finished_at", { withTimezone: true, mode: 'string' }), + migrationName: varchar("migration_name", { length: 255 }).notNull(), + logs: text("logs"), + rolledBackAt: timestamp("rolled_back_at", { withTimezone: true, mode: 'string' }), + startedAt: timestamp("started_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), + appliedStepsCount: integer("applied_steps_count").default(0).notNull(), +}); + +export const emoji = pgTable("Emoji", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + shortcode: text("shortcode").notNull(), + url: text("url").notNull(), + visibleInPicker: boolean("visible_in_picker").notNull(), + instanceId: uuid("instanceId").references(() => instance.id, { onDelete: "cascade", onUpdate: "cascade" } ), + alt: text("alt"), + contentType: text("content_type").notNull(), +}); + +export const like = pgTable("Like", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + likerId: uuid("likerId").notNull().references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ), + likedId: uuid("likedId").notNull().references(() => status.id, { onDelete: "cascade", onUpdate: "cascade" } ), + createdAt: timestamp("createdAt", { precision: 3, mode: 'string' }).defaultNow().notNull(), +}); + +export const lysandObject = pgTable("LysandObject", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + remoteId: text("remote_id").notNull(), + type: text("type").notNull(), + uri: text("uri").notNull(), + createdAt: timestamp("created_at", { precision: 3, mode: 'string' }).defaultNow().notNull(), + authorId: uuid("authorId"), + extraData: jsonb("extra_data").notNull(), + extensions: jsonb("extensions").notNull(), +}, +(table) => { + return { + remoteIdKey: uniqueIndex("LysandObject_remote_id_key").on(table.remoteId), + uriKey: uniqueIndex("LysandObject_uri_key").on(table.uri), + lysandObjectAuthorIdFkey: foreignKey({ + columns: [table.authorId], + foreignColumns: [table.id], + name: "LysandObject_authorId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + } +}); + +export const relationship = pgTable("Relationship", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + ownerId: uuid("ownerId").notNull().references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ), + subjectId: uuid("subjectId").notNull().references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ), + following: boolean("following").notNull(), + showingReblogs: boolean("showingReblogs").notNull(), + notifying: boolean("notifying").notNull(), + followedBy: boolean("followedBy").notNull(), + blocking: boolean("blocking").notNull(), + blockedBy: boolean("blockedBy").notNull(), + muting: boolean("muting").notNull(), + mutingNotifications: boolean("mutingNotifications").notNull(), + requested: boolean("requested").notNull(), + domainBlocking: boolean("domainBlocking").notNull(), + endorsed: boolean("endorsed").notNull(), + languages: text("languages").array(), + note: text("note").notNull(), + createdAt: timestamp("createdAt", { precision: 3, mode: 'string' }).defaultNow().notNull(), + updatedAt: timestamp("updatedAt", { precision: 3, mode: 'string' }).notNull(), +}); + +export const application = pgTable("Application", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + name: text("name").notNull(), + website: text("website"), + vapidKey: text("vapid_key"), + clientId: text("client_id").notNull(), + secret: text("secret").notNull(), + scopes: text("scopes").notNull(), + redirectUris: text("redirect_uris").notNull(), +}, +(table) => { + return { + clientIdKey: uniqueIndex("Application_client_id_key").on(table.clientId), + } +}); + +export const token = pgTable("Token", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + tokenType: text("token_type").notNull(), + scope: text("scope").notNull(), + accessToken: text("access_token").notNull(), + code: text("code").notNull(), + createdAt: timestamp("created_at", { precision: 3, mode: 'string' }).defaultNow().notNull(), + userId: uuid("userId").references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ), + applicationId: uuid("applicationId").references(() => application.id, { onDelete: "cascade", onUpdate: "cascade" } ), +}); + +export const emojiToUser = pgTable("_EmojiToUser", { + a: uuid("A").notNull().references(() => emoji.id, { onDelete: "cascade", onUpdate: "cascade" } ), + b: uuid("B").notNull().references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ), +}, +(table) => { + return { + abUnique: uniqueIndex("_EmojiToUser_AB_unique").on(table.a, table.b), + bIdx: index().on(table.b), + } +}); + +export const emojiToStatus = pgTable("_EmojiToStatus", { + a: uuid("A").notNull().references(() => emoji.id, { onDelete: "cascade", onUpdate: "cascade" } ), + b: uuid("B").notNull().references(() => status.id, { onDelete: "cascade", onUpdate: "cascade" } ), +}, +(table) => { + return { + abUnique: uniqueIndex("_EmojiToStatus_AB_unique").on(table.a, table.b), + bIdx: index().on(table.b), + } +}); + +export const statusToUser = pgTable("_StatusToUser", { + a: uuid("A").notNull().references(() => status.id, { onDelete: "cascade", onUpdate: "cascade" } ), + b: uuid("B").notNull().references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ), +}, +(table) => { + return { + abUnique: uniqueIndex("_StatusToUser_AB_unique").on(table.a, table.b), + bIdx: index().on(table.b), + } +}); + +export const userPinnedNotes = pgTable("_UserPinnedNotes", { + a: uuid("A").notNull().references(() => status.id, { onDelete: "cascade", onUpdate: "cascade" } ), + b: uuid("B").notNull().references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ), +}, +(table) => { + return { + abUnique: uniqueIndex("_UserPinnedNotes_AB_unique").on(table.a, table.b), + bIdx: index().on(table.b), + } +}); + +export const attachment = pgTable("Attachment", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + url: text("url").notNull(), + remoteUrl: text("remote_url"), + thumbnailUrl: text("thumbnail_url"), + mimeType: text("mime_type").notNull(), + description: text("description"), + blurhash: text("blurhash"), + sha256: text("sha256"), + fps: integer("fps"), + duration: integer("duration"), + width: integer("width"), + height: integer("height"), + size: integer("size"), + statusId: uuid("statusId").references(() => status.id, { onDelete: "cascade", onUpdate: "cascade" } ), +}); + +export const notification = pgTable("Notification", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + type: text("type").notNull(), + createdAt: timestamp("createdAt", { precision: 3, mode: 'string' }).defaultNow().notNull(), + notifiedId: uuid("notifiedId").notNull().references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ), + accountId: uuid("accountId").notNull().references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ), + statusId: uuid("statusId").references(() => status.id, { onDelete: "cascade", onUpdate: "cascade" } ), +}); + +export const status = pgTable("Status", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + uri: text("uri"), + authorId: uuid("authorId").notNull().references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ), + createdAt: timestamp("createdAt", { precision: 3, mode: 'string' }).defaultNow().notNull(), + updatedAt: timestamp("updatedAt", { precision: 3, mode: 'string' }).notNull(), + reblogId: uuid("reblogId"), + content: text("content").default('').notNull(), + contentType: text("contentType").default('text/plain').notNull(), + visibility: text("visibility").notNull(), + inReplyToPostId: uuid("inReplyToPostId"), + quotingPostId: uuid("quotingPostId"), + instanceId: uuid("instanceId").references(() => instance.id, { onDelete: "cascade", onUpdate: "cascade" } ), + sensitive: boolean("sensitive").notNull(), + spoilerText: text("spoilerText").default('').notNull(), + applicationId: uuid("applicationId").references(() => application.id, { onDelete: "set null", onUpdate: "cascade" } ), + contentSource: text("contentSource").default('').notNull(), +}, +(table) => { + return { + uriKey: uniqueIndex("Status_uri_key").on(table.uri), + statusReblogIdFkey: foreignKey({ + columns: [table.reblogId], + foreignColumns: [table.id], + name: "Status_reblogId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + statusInReplyToPostIdFkey: foreignKey({ + columns: [table.inReplyToPostId], + foreignColumns: [table.id], + name: "Status_inReplyToPostId_fkey" + }).onUpdate("cascade").onDelete("set null"), + statusQuotingPostIdFkey: foreignKey({ + columns: [table.quotingPostId], + foreignColumns: [table.id], + name: "Status_quotingPostId_fkey" + }).onUpdate("cascade").onDelete("set null"), + } +}); + +export const instance = pgTable("Instance", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + baseUrl: text("base_url").notNull(), + name: text("name").notNull(), + version: text("version").notNull(), + logo: jsonb("logo").notNull(), + disableAutomoderation: boolean("disableAutomoderation").default(false).notNull(), +}); + +export const openIdAccount = pgTable("OpenIdAccount", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + userId: uuid("userId").references(() => user.id, { onDelete: "set null", onUpdate: "cascade" } ), + serverId: text("serverId").notNull(), + issuerId: text("issuerId").notNull(), +}); + +export const user = pgTable("User", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + uri: text("uri"), + username: text("username").notNull(), + displayName: text("displayName").notNull(), + password: text("password"), + email: text("email"), + note: text("note").default('').notNull(), + isAdmin: boolean("isAdmin").default(false).notNull(), + endpoints: jsonb("endpoints"), + source: jsonb("source").notNull(), + avatar: text("avatar").notNull(), + header: text("header").notNull(), + createdAt: timestamp("createdAt", { precision: 3, mode: 'string' }).defaultNow().notNull(), + updatedAt: timestamp("updatedAt", { precision: 3, mode: 'string' }).notNull(), + isBot: boolean("isBot").default(false).notNull(), + isLocked: boolean("isLocked").default(false).notNull(), + isDiscoverable: boolean("isDiscoverable").default(false).notNull(), + sanctions: text("sanctions").default('RRAY[').array(), + publicKey: text("publicKey").notNull(), + privateKey: text("privateKey"), + instanceId: uuid("instanceId").references(() => instance.id, { onDelete: "cascade", onUpdate: "cascade" } ), + disableAutomoderation: boolean("disableAutomoderation").default(false).notNull(), +}, +(table) => { + return { + uriKey: uniqueIndex("User_uri_key").on(table.uri), + usernameKey: uniqueIndex("User_username_key").on(table.username), + emailKey: uniqueIndex("User_email_key").on(table.email), + } +}); + +export const openIdLoginFlow = pgTable("OpenIdLoginFlow", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + codeVerifier: text("codeVerifier").notNull(), + applicationId: uuid("applicationId").references(() => application.id, { onDelete: "cascade", onUpdate: "cascade" } ), + issuerId: text("issuerId").notNull(), +}); + +export const flag = pgTable("Flag", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + flagType: text("flagType").default('other').notNull(), + createdAt: timestamp("createdAt", { precision: 3, mode: 'string' }).defaultNow().notNull(), + flaggeStatusId: uuid("flaggeStatusId").references(() => status.id, { onDelete: "cascade", onUpdate: "cascade" } ), + flaggedUserId: uuid("flaggedUserId").references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ), +}); + +export const modNote = pgTable("ModNote", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + notedStatusId: uuid("notedStatusId").references(() => status.id, { onDelete: "cascade", onUpdate: "cascade" } ), + notedUserId: uuid("notedUserId").references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ), + modId: uuid("modId").notNull().references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ), + note: text("note").notNull(), + createdAt: timestamp("createdAt", { precision: 3, mode: 'string' }).defaultNow().notNull(), +}); + +export const modTag = pgTable("ModTag", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + taggedStatusId: uuid("taggedStatusId").references(() => status.id, { onDelete: "cascade", onUpdate: "cascade" } ), + taggedUserId: uuid("taggedUserId").references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ), + modId: uuid("modId").notNull().references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ), + tag: text("tag").notNull(), + createdAt: timestamp("createdAt", { precision: 3, mode: 'string' }).defaultNow().notNull(), +}); \ No newline at end of file diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 00000000..7550d926 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,19 @@ +import type { Config } from "drizzle-kit"; +import { config } from "config-manager"; + +export default { + driver: "pg", + out: "./drizzle", + schema: "./drizzle/schema.ts", + dbCredentials: { + host: config.database.host, + port: Number(config.database.port), + user: config.database.username, + password: config.database.password, + database: config.database.database, + }, + // Print all statements + verbose: true, + // Always ask for confirmation + strict: true, +} satisfies Config; diff --git a/drizzle/0000_illegal_living_lightning.sql b/drizzle/0000_illegal_living_lightning.sql new file mode 100644 index 00000000..98c2e90e --- /dev/null +++ b/drizzle/0000_illegal_living_lightning.sql @@ -0,0 +1,462 @@ +-- Current sql file was generated after introspecting the database +-- If you want to run this migration please uncomment this code before executing migrations +/* +CREATE TABLE IF NOT EXISTS "_prisma_migrations" ( + "id" varchar(36) PRIMARY KEY NOT NULL, + "checksum" varchar(64) NOT NULL, + "finished_at" timestamp with time zone, + "migration_name" varchar(255) NOT NULL, + "logs" text, + "rolled_back_at" timestamp with time zone, + "started_at" timestamp with time zone DEFAULT now() NOT NULL, + "applied_steps_count" integer DEFAULT 0 NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Emoji" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "shortcode" text NOT NULL, + "url" text NOT NULL, + "visible_in_picker" boolean NOT NULL, + "instanceId" uuid, + "alt" text, + "content_type" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Like" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "likerId" uuid NOT NULL, + "likedId" uuid NOT NULL, + "createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "LysandObject" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "remote_id" text NOT NULL, + "type" text NOT NULL, + "uri" text NOT NULL, + "created_at" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "authorId" uuid, + "extra_data" jsonb NOT NULL, + "extensions" jsonb NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Relationship" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "ownerId" uuid NOT NULL, + "subjectId" uuid NOT NULL, + "following" boolean NOT NULL, + "showingReblogs" boolean NOT NULL, + "notifying" boolean NOT NULL, + "followedBy" boolean NOT NULL, + "blocking" boolean NOT NULL, + "blockedBy" boolean NOT NULL, + "muting" boolean NOT NULL, + "mutingNotifications" boolean NOT NULL, + "requested" boolean NOT NULL, + "domainBlocking" boolean NOT NULL, + "endorsed" boolean NOT NULL, + "languages" text[], + "note" text NOT NULL, + "createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp(3) NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Application" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "name" text NOT NULL, + "website" text, + "vapid_key" text, + "client_id" text NOT NULL, + "secret" text NOT NULL, + "scopes" text NOT NULL, + "redirect_uris" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Token" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "token_type" text NOT NULL, + "scope" text NOT NULL, + "access_token" text NOT NULL, + "code" text NOT NULL, + "created_at" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "userId" uuid, + "applicationId" uuid +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "_EmojiToUser" ( + "A" uuid NOT NULL, + "B" uuid NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "_EmojiToStatus" ( + "A" uuid NOT NULL, + "B" uuid NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "_StatusToUser" ( + "A" uuid NOT NULL, + "B" uuid NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "_UserPinnedNotes" ( + "A" uuid NOT NULL, + "B" uuid NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Attachment" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "url" text NOT NULL, + "remote_url" text, + "thumbnail_url" text, + "mime_type" text NOT NULL, + "description" text, + "blurhash" text, + "sha256" text, + "fps" integer, + "duration" integer, + "width" integer, + "height" integer, + "size" integer, + "statusId" uuid +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Notification" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "type" text NOT NULL, + "createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "notifiedId" uuid NOT NULL, + "accountId" uuid NOT NULL, + "statusId" uuid +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Status" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "uri" text, + "authorId" uuid NOT NULL, + "createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp(3) NOT NULL, + "reblogId" uuid, + "content" text DEFAULT '' NOT NULL, + "contentType" text DEFAULT 'text/plain' NOT NULL, + "visibility" text NOT NULL, + "inReplyToPostId" uuid, + "quotingPostId" uuid, + "instanceId" uuid, + "sensitive" boolean NOT NULL, + "spoilerText" text DEFAULT '' NOT NULL, + "applicationId" uuid, + "contentSource" text DEFAULT '' NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Instance" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "base_url" text NOT NULL, + "name" text NOT NULL, + "version" text NOT NULL, + "logo" jsonb NOT NULL, + "disableAutomoderation" boolean DEFAULT false NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "OpenIdAccount" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "userId" uuid, + "serverId" text NOT NULL, + "issuerId" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "User" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "uri" text, + "username" text NOT NULL, + "displayName" text NOT NULL, + "password" text, + "email" text, + "note" text DEFAULT '' NOT NULL, + "isAdmin" boolean DEFAULT false NOT NULL, + "endpoints" jsonb, + "source" jsonb NOT NULL, + "avatar" text NOT NULL, + "header" text NOT NULL, + "createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp(3) NOT NULL, + "isBot" boolean DEFAULT false NOT NULL, + "isLocked" boolean DEFAULT false NOT NULL, + "isDiscoverable" boolean DEFAULT false NOT NULL, + "sanctions" text[] DEFAULT 'RRAY[', + "publicKey" text NOT NULL, + "privateKey" text, + "instanceId" uuid, + "disableAutomoderation" boolean DEFAULT false NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "OpenIdLoginFlow" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "codeVerifier" text NOT NULL, + "applicationId" uuid, + "issuerId" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Flag" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "flagType" text DEFAULT 'other' NOT NULL, + "createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "flaggeStatusId" uuid, + "flaggedUserId" uuid +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "ModNote" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "notedStatusId" uuid, + "notedUserId" uuid, + "modId" uuid NOT NULL, + "note" text NOT NULL, + "createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "ModTag" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "taggedStatusId" uuid, + "taggedUserId" uuid, + "modId" uuid NOT NULL, + "tag" text NOT NULL, + "createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "LysandObject_remote_id_key" ON "LysandObject" ("remote_id");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "LysandObject_uri_key" ON "LysandObject" ("uri");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "Application_client_id_key" ON "Application" ("client_id");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "_EmojiToUser_AB_unique" ON "_EmojiToUser" ("A","B");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "_EmojiToUser_B_index" ON "_EmojiToUser" ("B");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "_EmojiToStatus_AB_unique" ON "_EmojiToStatus" ("A","B");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "_EmojiToStatus_B_index" ON "_EmojiToStatus" ("B");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "_StatusToUser_AB_unique" ON "_StatusToUser" ("A","B");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "_StatusToUser_B_index" ON "_StatusToUser" ("B");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "_UserPinnedNotes_AB_unique" ON "_UserPinnedNotes" ("A","B");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "_UserPinnedNotes_B_index" ON "_UserPinnedNotes" ("B");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "Status_uri_key" ON "Status" ("uri");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "User_uri_key" ON "User" ("uri");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "User_username_key" ON "User" ("username");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "User_email_key" ON "User" ("email");--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Emoji" ADD CONSTRAINT "Emoji_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "public"."Instance"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Like" ADD CONSTRAINT "Like_likerId_fkey" FOREIGN KEY ("likerId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Like" ADD CONSTRAINT "Like_likedId_fkey" FOREIGN KEY ("likedId") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "LysandObject" ADD CONSTRAINT "LysandObject_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "public"."LysandObject"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Relationship" ADD CONSTRAINT "Relationship_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Relationship" ADD CONSTRAINT "Relationship_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Token" ADD CONSTRAINT "Token_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Token" ADD CONSTRAINT "Token_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "public"."Application"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "_EmojiToUser" ADD CONSTRAINT "_EmojiToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."Emoji"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "_EmojiToUser" ADD CONSTRAINT "_EmojiToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "_EmojiToStatus" ADD CONSTRAINT "_EmojiToStatus_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."Emoji"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "_EmojiToStatus" ADD CONSTRAINT "_EmojiToStatus_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "_StatusToUser" ADD CONSTRAINT "_StatusToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "_StatusToUser" ADD CONSTRAINT "_StatusToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "_UserPinnedNotes" ADD CONSTRAINT "_UserPinnedNotes_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "_UserPinnedNotes" ADD CONSTRAINT "_UserPinnedNotes_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Attachment" ADD CONSTRAINT "Attachment_statusId_fkey" FOREIGN KEY ("statusId") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Notification" ADD CONSTRAINT "Notification_notifiedId_fkey" FOREIGN KEY ("notifiedId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Notification" ADD CONSTRAINT "Notification_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Notification" ADD CONSTRAINT "Notification_statusId_fkey" FOREIGN KEY ("statusId") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Status" ADD CONSTRAINT "Status_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Status" ADD CONSTRAINT "Status_reblogId_fkey" FOREIGN KEY ("reblogId") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Status" ADD CONSTRAINT "Status_inReplyToPostId_fkey" FOREIGN KEY ("inReplyToPostId") REFERENCES "public"."Status"("id") ON DELETE set null ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Status" ADD CONSTRAINT "Status_quotingPostId_fkey" FOREIGN KEY ("quotingPostId") REFERENCES "public"."Status"("id") ON DELETE set null ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Status" ADD CONSTRAINT "Status_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "public"."Instance"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Status" ADD CONSTRAINT "Status_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "public"."Application"("id") ON DELETE set null ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "OpenIdAccount" ADD CONSTRAINT "OpenIdAccount_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE set null ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "User" ADD CONSTRAINT "User_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "public"."Instance"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "OpenIdLoginFlow" ADD CONSTRAINT "OpenIdLoginFlow_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "public"."Application"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Flag" ADD CONSTRAINT "Flag_flaggeStatusId_fkey" FOREIGN KEY ("flaggeStatusId") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Flag" ADD CONSTRAINT "Flag_flaggedUserId_fkey" FOREIGN KEY ("flaggedUserId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "ModNote" ADD CONSTRAINT "ModNote_notedStatusId_fkey" FOREIGN KEY ("notedStatusId") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "ModNote" ADD CONSTRAINT "ModNote_notedUserId_fkey" FOREIGN KEY ("notedUserId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "ModNote" ADD CONSTRAINT "ModNote_modId_fkey" FOREIGN KEY ("modId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "ModTag" ADD CONSTRAINT "ModTag_taggedStatusId_fkey" FOREIGN KEY ("taggedStatusId") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "ModTag" ADD CONSTRAINT "ModTag_taggedUserId_fkey" FOREIGN KEY ("taggedUserId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "ModTag" ADD CONSTRAINT "ModTag_modId_fkey" FOREIGN KEY ("modId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +*/ \ No newline at end of file diff --git a/drizzle/db.ts b/drizzle/db.ts new file mode 100644 index 00000000..29ca4545 --- /dev/null +++ b/drizzle/db.ts @@ -0,0 +1,14 @@ +import { Client } from "pg"; +import { config } from "config-manager"; +import { drizzle } from "drizzle-orm/node-postgres"; +import * as schema from "./schema"; + +export const client = new Client({ + host: config.database.host, + port: Number(config.database.port), + user: config.database.username, + password: config.database.password, + database: config.database.database, +}); + +export const db = drizzle(client, { schema }); diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 00000000..a014f406 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,1867 @@ +{ + "id": "00000000-0000-0000-0000-000000000000", + "prevId": "", + "version": "5", + "dialect": "pg", + "tables": { + "_prisma_migrations": { + "name": "_prisma_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(36)", + "primaryKey": true, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "migration_name": { + "name": "migration_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "logs": { + "name": "logs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rolled_back_at": { + "name": "rolled_back_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "applied_steps_count": { + "name": "applied_steps_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Emoji": { + "name": "Emoji", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "shortcode": { + "name": "shortcode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visible_in_picker": { + "name": "visible_in_picker", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "instanceId": { + "name": "instanceId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "alt": { + "name": "alt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "Emoji_instanceId_fkey": { + "name": "Emoji_instanceId_fkey", + "tableFrom": "Emoji", + "tableTo": "Instance", + "schemaTo": "public", + "columnsFrom": [ + "instanceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Like": { + "name": "Like", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "likerId": { + "name": "likerId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "likedId": { + "name": "likedId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "Like_likerId_fkey": { + "name": "Like_likerId_fkey", + "tableFrom": "Like", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "likerId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Like_likedId_fkey": { + "name": "Like_likedId_fkey", + "tableFrom": "Like", + "tableTo": "Status", + "schemaTo": "public", + "columnsFrom": [ + "likedId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "LysandObject": { + "name": "LysandObject", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "remote_id": { + "name": "remote_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "authorId": { + "name": "authorId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "extra_data": { + "name": "extra_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "extensions": { + "name": "extensions", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "LysandObject_remote_id_key": { + "name": "LysandObject_remote_id_key", + "columns": [ + "remote_id" + ], + "isUnique": true + }, + "LysandObject_uri_key": { + "name": "LysandObject_uri_key", + "columns": [ + "uri" + ], + "isUnique": true + } + }, + "foreignKeys": { + "LysandObject_authorId_fkey": { + "name": "LysandObject_authorId_fkey", + "tableFrom": "LysandObject", + "tableTo": "LysandObject", + "schemaTo": "public", + "columnsFrom": [ + "authorId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Relationship": { + "name": "Relationship", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "ownerId": { + "name": "ownerId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "subjectId": { + "name": "subjectId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "following": { + "name": "following", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "showingReblogs": { + "name": "showingReblogs", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "notifying": { + "name": "notifying", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "followedBy": { + "name": "followedBy", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "blocking": { + "name": "blocking", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "blockedBy": { + "name": "blockedBy", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "muting": { + "name": "muting", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "mutingNotifications": { + "name": "mutingNotifications", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "requested": { + "name": "requested", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "domainBlocking": { + "name": "domainBlocking", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "endorsed": { + "name": "endorsed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "languages": { + "name": "languages", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "Relationship_ownerId_fkey": { + "name": "Relationship_ownerId_fkey", + "tableFrom": "Relationship", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "ownerId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Relationship_subjectId_fkey": { + "name": "Relationship_subjectId_fkey", + "tableFrom": "Relationship", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "subjectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Application": { + "name": "Application", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vapid_key": { + "name": "vapid_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "Application_client_id_key": { + "name": "Application_client_id_key", + "columns": [ + "client_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Token": { + "name": "Token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Token_userId_fkey": { + "name": "Token_userId_fkey", + "tableFrom": "Token", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Token_applicationId_fkey": { + "name": "Token_applicationId_fkey", + "tableFrom": "Token", + "tableTo": "Application", + "schemaTo": "public", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "_EmojiToUser": { + "name": "_EmojiToUser", + "schema": "", + "columns": { + "A": { + "name": "A", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "B": { + "name": "B", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "_EmojiToUser_AB_unique": { + "name": "_EmojiToUser_AB_unique", + "columns": [ + "A", + "B" + ], + "isUnique": true + }, + "_EmojiToUser_B_index": { + "name": "_EmojiToUser_B_index", + "columns": [ + "B" + ], + "isUnique": false + } + }, + "foreignKeys": { + "_EmojiToUser_A_fkey": { + "name": "_EmojiToUser_A_fkey", + "tableFrom": "_EmojiToUser", + "tableTo": "Emoji", + "schemaTo": "public", + "columnsFrom": [ + "A" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "_EmojiToUser_B_fkey": { + "name": "_EmojiToUser_B_fkey", + "tableFrom": "_EmojiToUser", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "B" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "_EmojiToStatus": { + "name": "_EmojiToStatus", + "schema": "", + "columns": { + "A": { + "name": "A", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "B": { + "name": "B", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "_EmojiToStatus_AB_unique": { + "name": "_EmojiToStatus_AB_unique", + "columns": [ + "A", + "B" + ], + "isUnique": true + }, + "_EmojiToStatus_B_index": { + "name": "_EmojiToStatus_B_index", + "columns": [ + "B" + ], + "isUnique": false + } + }, + "foreignKeys": { + "_EmojiToStatus_A_fkey": { + "name": "_EmojiToStatus_A_fkey", + "tableFrom": "_EmojiToStatus", + "tableTo": "Emoji", + "schemaTo": "public", + "columnsFrom": [ + "A" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "_EmojiToStatus_B_fkey": { + "name": "_EmojiToStatus_B_fkey", + "tableFrom": "_EmojiToStatus", + "tableTo": "Status", + "schemaTo": "public", + "columnsFrom": [ + "B" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "_StatusToUser": { + "name": "_StatusToUser", + "schema": "", + "columns": { + "A": { + "name": "A", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "B": { + "name": "B", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "_StatusToUser_AB_unique": { + "name": "_StatusToUser_AB_unique", + "columns": [ + "A", + "B" + ], + "isUnique": true + }, + "_StatusToUser_B_index": { + "name": "_StatusToUser_B_index", + "columns": [ + "B" + ], + "isUnique": false + } + }, + "foreignKeys": { + "_StatusToUser_A_fkey": { + "name": "_StatusToUser_A_fkey", + "tableFrom": "_StatusToUser", + "tableTo": "Status", + "schemaTo": "public", + "columnsFrom": [ + "A" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "_StatusToUser_B_fkey": { + "name": "_StatusToUser_B_fkey", + "tableFrom": "_StatusToUser", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "B" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "_UserPinnedNotes": { + "name": "_UserPinnedNotes", + "schema": "", + "columns": { + "A": { + "name": "A", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "B": { + "name": "B", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "_UserPinnedNotes_AB_unique": { + "name": "_UserPinnedNotes_AB_unique", + "columns": [ + "A", + "B" + ], + "isUnique": true + }, + "_UserPinnedNotes_B_index": { + "name": "_UserPinnedNotes_B_index", + "columns": [ + "B" + ], + "isUnique": false + } + }, + "foreignKeys": { + "_UserPinnedNotes_A_fkey": { + "name": "_UserPinnedNotes_A_fkey", + "tableFrom": "_UserPinnedNotes", + "tableTo": "Status", + "schemaTo": "public", + "columnsFrom": [ + "A" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "_UserPinnedNotes_B_fkey": { + "name": "_UserPinnedNotes_B_fkey", + "tableFrom": "_UserPinnedNotes", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "B" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Attachment": { + "name": "Attachment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remote_url": { + "name": "remote_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "thumbnail_url": { + "name": "thumbnail_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blurhash": { + "name": "blurhash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fps": { + "name": "fps", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "statusId": { + "name": "statusId", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Attachment_statusId_fkey": { + "name": "Attachment_statusId_fkey", + "tableFrom": "Attachment", + "tableTo": "Status", + "schemaTo": "public", + "columnsFrom": [ + "statusId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Notification": { + "name": "Notification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "notifiedId": { + "name": "notifiedId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "accountId": { + "name": "accountId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "statusId": { + "name": "statusId", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Notification_notifiedId_fkey": { + "name": "Notification_notifiedId_fkey", + "tableFrom": "Notification", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "notifiedId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notification_accountId_fkey": { + "name": "Notification_accountId_fkey", + "tableFrom": "Notification", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "accountId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notification_statusId_fkey": { + "name": "Notification_statusId_fkey", + "tableFrom": "Notification", + "tableTo": "Status", + "schemaTo": "public", + "columnsFrom": [ + "statusId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Status": { + "name": "Status", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorId": { + "name": "authorId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true + }, + "reblogId": { + "name": "reblogId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "contentType": { + "name": "contentType", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text/plain'" + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inReplyToPostId": { + "name": "inReplyToPostId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "quotingPostId": { + "name": "quotingPostId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "instanceId": { + "name": "instanceId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sensitive": { + "name": "sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "spoilerText": { + "name": "spoilerText", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "applicationId": { + "name": "applicationId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "contentSource": { + "name": "contentSource", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": { + "Status_uri_key": { + "name": "Status_uri_key", + "columns": [ + "uri" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Status_authorId_fkey": { + "name": "Status_authorId_fkey", + "tableFrom": "Status", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "authorId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Status_reblogId_fkey": { + "name": "Status_reblogId_fkey", + "tableFrom": "Status", + "tableTo": "Status", + "schemaTo": "public", + "columnsFrom": [ + "reblogId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Status_inReplyToPostId_fkey": { + "name": "Status_inReplyToPostId_fkey", + "tableFrom": "Status", + "tableTo": "Status", + "schemaTo": "public", + "columnsFrom": [ + "inReplyToPostId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Status_quotingPostId_fkey": { + "name": "Status_quotingPostId_fkey", + "tableFrom": "Status", + "tableTo": "Status", + "schemaTo": "public", + "columnsFrom": [ + "quotingPostId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Status_instanceId_fkey": { + "name": "Status_instanceId_fkey", + "tableFrom": "Status", + "tableTo": "Instance", + "schemaTo": "public", + "columnsFrom": [ + "instanceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Status_applicationId_fkey": { + "name": "Status_applicationId_fkey", + "tableFrom": "Status", + "tableTo": "Application", + "schemaTo": "public", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Instance": { + "name": "Instance", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "disableAutomoderation": { + "name": "disableAutomoderation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "OpenIdAccount": { + "name": "OpenIdAccount", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issuerId": { + "name": "issuerId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "OpenIdAccount_userId_fkey": { + "name": "OpenIdAccount_userId_fkey", + "tableFrom": "OpenIdAccount", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "User": { + "name": "User", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "displayName": { + "name": "displayName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "isAdmin": { + "name": "isAdmin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "endpoints": { + "name": "endpoints", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header": { + "name": "header", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true + }, + "isBot": { + "name": "isBot", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isLocked": { + "name": "isLocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isDiscoverable": { + "name": "isDiscoverable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sanctions": { + "name": "sanctions", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'RRAY['" + }, + "publicKey": { + "name": "publicKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "instanceId": { + "name": "instanceId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "disableAutomoderation": { + "name": "disableAutomoderation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "User_uri_key": { + "name": "User_uri_key", + "columns": [ + "uri" + ], + "isUnique": true + }, + "User_username_key": { + "name": "User_username_key", + "columns": [ + "username" + ], + "isUnique": true + }, + "User_email_key": { + "name": "User_email_key", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": { + "User_instanceId_fkey": { + "name": "User_instanceId_fkey", + "tableFrom": "User", + "tableTo": "Instance", + "schemaTo": "public", + "columnsFrom": [ + "instanceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "OpenIdLoginFlow": { + "name": "OpenIdLoginFlow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "codeVerifier": { + "name": "codeVerifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issuerId": { + "name": "issuerId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "OpenIdLoginFlow_applicationId_fkey": { + "name": "OpenIdLoginFlow_applicationId_fkey", + "tableFrom": "OpenIdLoginFlow", + "tableTo": "Application", + "schemaTo": "public", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Flag": { + "name": "Flag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "flagType": { + "name": "flagType", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'other'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "flaggeStatusId": { + "name": "flaggeStatusId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "flaggedUserId": { + "name": "flaggedUserId", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Flag_flaggeStatusId_fkey": { + "name": "Flag_flaggeStatusId_fkey", + "tableFrom": "Flag", + "tableTo": "Status", + "schemaTo": "public", + "columnsFrom": [ + "flaggeStatusId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Flag_flaggedUserId_fkey": { + "name": "Flag_flaggedUserId_fkey", + "tableFrom": "Flag", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "flaggedUserId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "ModNote": { + "name": "ModNote", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "notedStatusId": { + "name": "notedStatusId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "notedUserId": { + "name": "notedUserId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "modId": { + "name": "modId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ModNote_notedStatusId_fkey": { + "name": "ModNote_notedStatusId_fkey", + "tableFrom": "ModNote", + "tableTo": "Status", + "schemaTo": "public", + "columnsFrom": [ + "notedStatusId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModNote_notedUserId_fkey": { + "name": "ModNote_notedUserId_fkey", + "tableFrom": "ModNote", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "notedUserId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModNote_modId_fkey": { + "name": "ModNote_modId_fkey", + "tableFrom": "ModNote", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "modId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "ModTag": { + "name": "ModTag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "taggedStatusId": { + "name": "taggedStatusId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "taggedUserId": { + "name": "taggedUserId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "modId": { + "name": "modId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ModTag_taggedStatusId_fkey": { + "name": "ModTag_taggedStatusId_fkey", + "tableFrom": "ModTag", + "tableTo": "Status", + "schemaTo": "public", + "columnsFrom": [ + "taggedStatusId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModTag_taggedUserId_fkey": { + "name": "ModTag_taggedUserId_fkey", + "tableFrom": "ModTag", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "taggedUserId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModTag_modId_fkey": { + "name": "ModTag_modId_fkey", + "tableFrom": "ModTag", + "tableTo": "User", + "schemaTo": "public", + "columnsFrom": [ + "modId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 00000000..2a8db486 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "5", + "dialect": "pg", + "entries": [ + { + "idx": 0, + "version": "5", + "when": 1712805159664, + "tag": "0000_illegal_living_lightning", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/drizzle/schema.ts b/drizzle/schema.ts new file mode 100644 index 00000000..668a0747 --- /dev/null +++ b/drizzle/schema.ts @@ -0,0 +1,677 @@ +import { + pgTable, + timestamp, + text, + integer, + foreignKey, + uuid, + boolean, + uniqueIndex, + jsonb, + index, +} from "drizzle-orm/pg-core"; +import { relations, sql } from "drizzle-orm"; + +export const emoji = pgTable("Emoji", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + shortcode: text("shortcode").notNull(), + url: text("url").notNull(), + visibleInPicker: boolean("visible_in_picker").notNull(), + alt: text("alt"), + contentType: text("content_type").notNull(), + instanceId: uuid("instanceId").references(() => instance.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), +}); + +export const like = pgTable("Like", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + likerId: uuid("likerId") + .notNull() + .references(() => user.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + likedId: uuid("likedId") + .notNull() + .references(() => status.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + createdAt: timestamp("createdAt", { precision: 3, mode: "string" }) + .defaultNow() + .notNull(), +}); + +export const lysandObject = pgTable( + "LysandObject", + { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + remoteId: text("remote_id").notNull(), + type: text("type").notNull(), + uri: text("uri").notNull(), + createdAt: timestamp("created_at", { precision: 3, mode: "string" }) + .defaultNow() + .notNull(), + authorId: uuid("authorId"), + extraData: jsonb("extra_data").notNull(), + extensions: jsonb("extensions").notNull(), + }, + (table) => { + return { + remoteIdKey: uniqueIndex("LysandObject_remote_id_key").on( + table.remoteId, + ), + uriKey: uniqueIndex("LysandObject_uri_key").on(table.uri), + lysandObjectAuthorIdFkey: foreignKey({ + columns: [table.authorId], + foreignColumns: [table.id], + name: "LysandObject_authorId_fkey", + }) + .onUpdate("cascade") + .onDelete("cascade"), + }; + }, +); + +export const relationship = pgTable("Relationship", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + ownerId: uuid("ownerId") + .notNull() + .references(() => user.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + subjectId: uuid("subjectId") + .notNull() + .references(() => user.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + following: boolean("following").notNull(), + showingReblogs: boolean("showingReblogs").notNull(), + notifying: boolean("notifying").notNull(), + followedBy: boolean("followedBy").notNull(), + blocking: boolean("blocking").notNull(), + blockedBy: boolean("blockedBy").notNull(), + muting: boolean("muting").notNull(), + mutingNotifications: boolean("mutingNotifications").notNull(), + requested: boolean("requested").notNull(), + domainBlocking: boolean("domainBlocking").notNull(), + endorsed: boolean("endorsed").notNull(), + languages: text("languages").array(), + note: text("note").notNull(), + createdAt: timestamp("createdAt", { precision: 3, mode: "string" }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updatedAt", { + precision: 3, + mode: "string", + }).notNull(), +}); + +export const application = pgTable( + "Application", + { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + name: text("name").notNull(), + website: text("website"), + vapidKey: text("vapid_key"), + clientId: text("client_id").notNull(), + secret: text("secret").notNull(), + scopes: text("scopes").notNull(), + redirectUris: text("redirect_uris").notNull(), + }, + (table) => { + return { + clientIdKey: uniqueIndex("Application_client_id_key").on( + table.clientId, + ), + }; + }, +); + +export const token = pgTable("Token", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + tokenType: text("token_type").notNull(), + scope: text("scope").notNull(), + accessToken: text("access_token").notNull(), + code: text("code").notNull(), + createdAt: timestamp("created_at", { precision: 3, mode: "string" }) + .defaultNow() + .notNull(), + userId: uuid("userId").references(() => user.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + applicationId: uuid("applicationId").references(() => application.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), +}); + +export const attachment = pgTable("Attachment", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + url: text("url").notNull(), + remoteUrl: text("remote_url"), + thumbnailUrl: text("thumbnail_url"), + mimeType: text("mime_type").notNull(), + description: text("description"), + blurhash: text("blurhash"), + sha256: text("sha256"), + fps: integer("fps"), + duration: integer("duration"), + width: integer("width"), + height: integer("height"), + size: integer("size"), + statusId: uuid("statusId").references(() => status.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), +}); + +export const notification = pgTable("Notification", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + type: text("type").notNull(), + createdAt: timestamp("createdAt", { precision: 3, mode: "string" }) + .defaultNow() + .notNull(), + notifiedId: uuid("notifiedId") + .notNull() + .references(() => user.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + accountId: uuid("accountId") + .notNull() + .references(() => user.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + statusId: uuid("statusId").references(() => status.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), +}); + +export const status = pgTable( + "Status", + { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + uri: text("uri"), + authorId: uuid("authorId") + .notNull() + .references(() => user.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + createdAt: timestamp("createdAt", { precision: 3, mode: "string" }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updatedAt", { + precision: 3, + mode: "string", + }).notNull(), + reblogId: uuid("reblogId"), + content: text("content").default("").notNull(), + contentType: text("contentType").default("text/plain").notNull(), + visibility: text("visibility").notNull(), + inReplyToPostId: uuid("inReplyToPostId"), + quotingPostId: uuid("quotingPostId"), + instanceId: uuid("instanceId").references(() => instance.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + sensitive: boolean("sensitive").notNull(), + spoilerText: text("spoilerText").default("").notNull(), + applicationId: uuid("applicationId").references(() => application.id, { + onDelete: "set null", + onUpdate: "cascade", + }), + contentSource: text("contentSource").default("").notNull(), + }, + (table) => { + return { + uriKey: uniqueIndex("Status_uri_key").on(table.uri), + statusReblogIdFkey: foreignKey({ + columns: [table.reblogId], + foreignColumns: [table.id], + name: "Status_reblogId_fkey", + }) + .onUpdate("cascade") + .onDelete("cascade"), + statusInReplyToPostIdFkey: foreignKey({ + columns: [table.inReplyToPostId], + foreignColumns: [table.id], + name: "Status_inReplyToPostId_fkey", + }) + .onUpdate("cascade") + .onDelete("set null"), + statusQuotingPostIdFkey: foreignKey({ + columns: [table.quotingPostId], + foreignColumns: [table.id], + name: "Status_quotingPostId_fkey", + }) + .onUpdate("cascade") + .onDelete("set null"), + }; + }, +); + +export const instance = pgTable("Instance", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + baseUrl: text("base_url").notNull(), + name: text("name").notNull(), + version: text("version").notNull(), + logo: jsonb("logo").notNull(), + disableAutomoderation: boolean("disableAutomoderation") + .default(false) + .notNull(), +}); + +export const openIdAccount = pgTable("OpenIdAccount", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + userId: uuid("userId").references(() => user.id, { + onDelete: "set null", + onUpdate: "cascade", + }), + serverId: text("serverId").notNull(), + issuerId: text("issuerId").notNull(), +}); + +export const user = pgTable( + "User", + { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + uri: text("uri"), + username: text("username").notNull(), + displayName: text("displayName").notNull(), + password: text("password"), + email: text("email"), + note: text("note").default("").notNull(), + isAdmin: boolean("isAdmin").default(false).notNull(), + endpoints: jsonb("endpoints"), + source: jsonb("source").notNull(), + avatar: text("avatar").notNull(), + header: text("header").notNull(), + createdAt: timestamp("createdAt", { precision: 3, mode: "string" }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updatedAt", { + precision: 3, + mode: "string", + }) + .defaultNow() + .notNull(), + isBot: boolean("isBot").default(false).notNull(), + isLocked: boolean("isLocked").default(false).notNull(), + isDiscoverable: boolean("isDiscoverable").default(false).notNull(), + sanctions: text("sanctions").default("RRAY[").array(), + publicKey: text("publicKey").notNull(), + privateKey: text("privateKey"), + instanceId: uuid("instanceId").references(() => instance.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + disableAutomoderation: boolean("disableAutomoderation") + .default(false) + .notNull(), + }, + (table) => { + return { + uriKey: uniqueIndex("User_uri_key").on(table.uri), + usernameKey: uniqueIndex("User_username_key").on(table.username), + emailKey: uniqueIndex("User_email_key").on(table.email), + }; + }, +); + +export const openIdLoginFlow = pgTable("OpenIdLoginFlow", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + codeVerifier: text("codeVerifier").notNull(), + applicationId: uuid("applicationId").references(() => application.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + issuerId: text("issuerId").notNull(), +}); + +export const flag = pgTable("Flag", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + flagType: text("flagType").default("other").notNull(), + createdAt: timestamp("createdAt", { precision: 3, mode: "string" }) + .defaultNow() + .notNull(), + flaggeStatusId: uuid("flaggeStatusId").references(() => status.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + flaggedUserId: uuid("flaggedUserId").references(() => user.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), +}); + +export const modNote = pgTable("ModNote", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + notedStatusId: uuid("notedStatusId").references(() => status.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + notedUserId: uuid("notedUserId").references(() => user.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + modId: uuid("modId") + .notNull() + .references(() => user.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + note: text("note").notNull(), + createdAt: timestamp("createdAt", { precision: 3, mode: "string" }) + .defaultNow() + .notNull(), +}); + +export const modTag = pgTable("ModTag", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + taggedStatusId: uuid("taggedStatusId").references(() => status.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + taggedUserId: uuid("taggedUserId").references(() => user.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + modId: uuid("modId") + .notNull() + .references(() => user.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + tag: text("tag").notNull(), + createdAt: timestamp("createdAt", { precision: 3, mode: "string" }) + .defaultNow() + .notNull(), +}); + +export const emojiToUser = pgTable( + "_EmojiToUser", + { + a: uuid("A") + .notNull() + .references(() => emoji.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + b: uuid("B") + .notNull() + .references(() => user.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + }, + (table) => { + return { + abUnique: uniqueIndex("_EmojiToUser_AB_unique").on( + table.a, + table.b, + ), + bIdx: index().on(table.b), + }; + }, +); + +export const emojiToUserRelations = relations(emojiToUser, ({ one }) => ({ + emoji: one(emoji, { + fields: [emojiToUser.a], + references: [emoji.id], + }), + user: one(user, { + fields: [emojiToUser.b], + references: [user.id], + }), +})); + +export const emojiToStatus = pgTable( + "_EmojiToStatus", + { + a: uuid("A") + .notNull() + .references(() => emoji.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + b: uuid("B") + .notNull() + .references(() => status.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + }, + (table) => { + return { + abUnique: uniqueIndex("_EmojiToStatus_AB_unique").on( + table.a, + table.b, + ), + bIdx: index().on(table.b), + }; + }, +); + +export const statusToUser = pgTable( + "_StatusToUser", + { + a: uuid("A") + .notNull() + .references(() => status.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + b: uuid("B") + .notNull() + .references(() => user.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + }, + (table) => { + return { + abUnique: uniqueIndex("_StatusToUser_AB_unique").on( + table.a, + table.b, + ), + bIdx: index().on(table.b), + }; + }, +); + +export const userPinnedNotes = pgTable( + "_UserPinnedNotes", + { + a: uuid("A") + .notNull() + .references(() => status.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + b: uuid("B") + .notNull() + .references(() => user.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + }, + (table) => { + return { + abUnique: uniqueIndex("_UserPinnedNotes_AB_unique").on( + table.a, + table.b, + ), + bIdx: index().on(table.b), + }; + }, +); + +export const attachmentRelations = relations(attachment, ({ one }) => ({ + status: one(status, { + fields: [attachment.statusId], + references: [status.id], + }), +})); + +export const userRelations = relations(user, ({ many, one }) => ({ + emojis: many(emojiToUser), + pinnedNotes: many(userPinnedNotes), + statuses: many(status, { + relationName: "StatusToAuthor", + }), + likes: many(like), + relationships: many(relationship, { + relationName: "RelationshipToOwner", + }), + relationshipSubjects: many(relationship, { + relationName: "RelationshipToSubject", + }), + notifications: many(notification), + openIdAccounts: many(openIdAccount), + flags: many(flag), + modNotes: many(modNote), + modTags: many(modTag), + tokens: many(token), + instance: one(instance, { + fields: [user.instanceId], + references: [instance.id], + }), + mentionedIn: many(statusToUser), +})); + +export const relationshipRelations = relations(relationship, ({ one }) => ({ + owner: one(user, { + fields: [relationship.ownerId], + references: [user.id], + relationName: "RelationshipToOwner", + }), + subject: one(user, { + fields: [relationship.subjectId], + references: [user.id], + relationName: "RelationshipToSubject", + }), +})); + +export const tokenRelations = relations(token, ({ one }) => ({ + user: one(user, { + fields: [token.userId], + references: [user.id], + }), + application: one(application, { + fields: [token.applicationId], + references: [application.id], + }), +})); + +export const statusToUserRelations = relations(statusToUser, ({ one }) => ({ + status: one(status, { + fields: [statusToUser.a], + references: [status.id], + }), + user: one(user, { + fields: [statusToUser.b], + references: [user.id], + }), +})); + +export const userPinnedNotesRelations = relations( + userPinnedNotes, + ({ one }) => ({ + status: one(status, { + fields: [userPinnedNotes.a], + references: [status.id], + }), + user: one(user, { + fields: [userPinnedNotes.b], + references: [user.id], + }), + }), +); + +export const statusRelations = relations(status, ({ many, one }) => ({ + emojis: many(emojiToStatus), + author: one(user, { + fields: [status.authorId], + references: [user.id], + relationName: "StatusToAuthor", + }), + attachments: many(attachment), + mentions: many(statusToUser), + reblog: one(status, { + fields: [status.reblogId], + references: [status.id], + relationName: "StatusToReblog", + }), + usersThatHavePinned: many(userPinnedNotes), + inReplyTo: one(status, { + fields: [status.inReplyToPostId], + references: [status.id], + relationName: "StatusToReplying", + }), + quoting: one(status, { + fields: [status.quotingPostId], + references: [status.id], + relationName: "StatusToQuoting", + }), + application: one(application, { + fields: [status.applicationId], + references: [application.id], + }), + quotes: many(status, { + relationName: "StatusToQuoting", + }), + replies: many(status, { + relationName: "StatusToReplying", + }), + likes: many(like), + reblogs: many(status, { + relationName: "StatusToReblog", + }), +})); + +export const likeRelations = relations(like, ({ one }) => ({ + liker: one(user, { + fields: [like.likerId], + references: [user.id], + }), + liked: one(status, { + fields: [like.likedId], + references: [status.id], + }), +})); + +export const emojiRelations = relations(emoji, ({ one, many }) => ({ + instance: one(instance, { + fields: [emoji.instanceId], + references: [instance.id], + }), + users: many(emojiToUser), + statuses: many(emojiToStatus), +})); + +export const instanceRelations = relations(instance, ({ many }) => ({ + users: many(user), + statuses: many(status), + emojis: many(emoji), +})); + +export const emojiToStatusRelations = relations(emojiToStatus, ({ one }) => ({ + emoji: one(emoji, { + fields: [emojiToStatus.a], + references: [emoji.id], + }), + status: one(status, { + fields: [emojiToStatus.b], + references: [status.id], + }), +})); diff --git a/index.ts b/index.ts index 75fa44e3..6277261c 100644 --- a/index.ts +++ b/index.ts @@ -2,13 +2,15 @@ import { exists, mkdir, writeFile } from "node:fs/promises"; import { dirname } from "node:path"; import { connectMeili } from "@meilisearch"; import { moduleIsEntry } from "@module"; -import type { PrismaClientInitializationError } from "@prisma/client/runtime/library"; import { initializeRedisCache } from "@redis"; import { config } from "config-manager"; import { LogLevel, LogManager, MultiLogManager } from "log-manager"; -import { client } from "~database/datasource"; import { createServer } from "~server"; +import { db, client as pgClient } from "~drizzle/db"; +import { status } from "~drizzle/schema"; +import { count, sql } from "drizzle-orm"; +await pgClient.connect(); const timeAtStart = performance.now(); // Create requests file if it doesnt exist @@ -43,16 +45,18 @@ if (config.meilisearch.enabled) { await connectMeili(dualLogger); } -if (redisCache) { - client.$use(redisCache); -} - // Check if database is reachable let postCount = 0; try { - postCount = await client.status.count(); + postCount = ( + await db + .select({ + count: count(), + }) + .from(status) + )[0].count; } catch (e) { - const error = e as PrismaClientInitializationError; + const error = e as Error; await logger.logError(LogLevel.CRITICAL, "Database", error); await consoleLogger.logError(LogLevel.CRITICAL, "Database", error); process.exit(1); diff --git a/package.json b/package.json index 62135513..5ce39c19 100644 --- a/package.json +++ b/package.json @@ -1,134 +1,139 @@ { - "name": "lysand", - "module": "index.ts", - "type": "module", - "version": "0.4.0", - "description": "A project to build a federated social network", - "author": { - "email": "contact@cpluspatch.com", - "name": "CPlusPatch", - "url": "https://cpluspatch.com" - }, - "bugs": { - "url": "https://github.com/lysand-org/lysand/issues" - }, - "icon": "https://github.com/lysand-org/lysand", - "license": "AGPL-3.0", - "keywords": ["federated", "activitypub", "bun"], - "workspaces": ["packages/*"], - "maintainers": [ - { - "email": "contact@cpluspatch.com", - "name": "CPlusPatch", - "url": "https://cpluspatch.com" - } - ], - "repository": { - "type": "git", - "url": "git+https://github.com/lysand-org/lysand.git" - }, - "private": true, - "scripts": { - "dev": "bun run --watch index.ts", - "vite:dev": "bunx --bun vite pages", - "vite:build": "bunx --bun vite build pages", - "fe:dev": "bun --bun nuxt dev packages/frontend", - "fe:build": "bun --bun nuxt build packages/frontend", - "fe:analyze": "bun --bun nuxt analyze packages/frontend", - "start": "NITRO_PORT=5173 bun run dist/frontend/server/index.mjs & NODE_ENV=production bun run dist/index.js --prod", - "migrate-dev": "bun prisma migrate dev", - "migrate": "bun prisma migrate deploy", - "lint": "bunx --bun eslint --config .eslintrc.cjs --ext .ts .", - "prod-build": "bun run build.ts", - "prisma": "DATABASE_URL=$(bun run prisma.ts) bunx prisma", - "generate": "bun prisma generate", - "benchmark:timeline": "bun run benchmarks/timelines.ts", - "cloc": "cloc . --exclude-dir node_modules,dist", - "cli": "bun run cli.ts" - }, - "trustedDependencies": [ - "@biomejs/biome", - "@fortawesome/fontawesome-common-types", - "@fortawesome/free-regular-svg-icons", - "@fortawesome/free-solid-svg-icons", - "@prisma/client", - "@prisma/engines", - "esbuild", - "json-editor-vue", - "msgpackr-extract", - "nuxt-app", - "prisma", - "sharp", - "vue-demi" - ], - "devDependencies": { - "@biomejs/biome": "1.6.4", - "@img/sharp-wasm32": "^0.33.3", - "@julr/unocss-preset-forms": "^0.1.0", - "@nuxtjs/seo": "^2.0.0-rc.10", - "@nuxtjs/tailwindcss": "^6.11.4", - "@types/cli-table": "^0.3.4", - "@types/html-to-text": "^9.0.4", - "@types/ioredis": "^5.0.0", - "@types/jsonld": "^1.5.13", - "@types/mime-types": "^2.1.4", - "@typescript-eslint/eslint-plugin": "latest", - "@unocss/cli": "latest", - "@unocss/transformer-directives": "^0.59.0", - "@vitejs/plugin-vue": "latest", - "@vueuse/head": "^2.0.0", - "activitypub-types": "^1.0.3", - "bun-types": "latest", - "shiki": "^1.2.4", - "typescript": "latest", - "unocss": "latest", - "untyped": "^1.4.2", - "vite": "^5.2.8", - "vite-ssr": "^0.17.1", - "vue": "^3.3.9", - "vue-router": "^4.2.5", - "vue-tsc": "latest" - }, - "peerDependencies": { - "typescript": "^5.3.2" - }, - "dependencies": { - "@aws-sdk/client-s3": "^3.461.0", - "@iarna/toml": "^2.2.5", - "@json2csv/plainjs": "^7.0.6", - "@prisma/client": "^5.6.0", - "blurhash": "^2.0.5", - "bullmq": "latest", - "chalk": "^5.3.0", - "cli-parser": "workspace:*", - "cli-table": "^0.3.11", - "config-manager": "workspace:*", - "eventemitter3": "^5.0.1", - "extract-zip": "^2.0.1", - "html-to-text": "^9.0.5", - "ioredis": "^5.3.2", - "ip-matching": "^2.1.2", - "iso-639-1": "^3.1.0", - "isomorphic-dompurify": "latest", - "jsonld": "^8.3.1", - "linkify-html": "^4.1.3", - "linkify-string": "^4.1.3", - "linkifyjs": "^4.1.3", - "log-manager": "workspace:*", - "marked": "latest", - "media-manager": "workspace:*", - "megalodon": "^10.0.0", - "meilisearch": "latest", - "merge-deep-ts": "^1.2.6", - "mime-types": "^2.1.35", - "next-route-matcher": "^1.0.1", - "oauth4webapi": "^2.4.0", - "prisma": "^5.6.0", - "prisma-json-types-generator": "^3.0.4", - "prisma-redis-middleware": "^4.8.0", - "request-parser": "workspace:*", - "semver": "^7.5.4", - "sharp": "^0.33.3", - "strip-ansi": "^7.1.0" + "name": "lysand", + "module": "index.ts", + "type": "module", + "version": "0.4.0", + "description": "A project to build a federated social network", + "author": { + "email": "contact@cpluspatch.com", + "name": "CPlusPatch", + "url": "https://cpluspatch.com" + }, + "bugs": { + "url": "https://github.com/lysand-org/lysand/issues" + }, + "icon": "https://github.com/lysand-org/lysand", + "license": "AGPL-3.0", + "keywords": ["federated", "activitypub", "bun"], + "workspaces": ["packages/*"], + "maintainers": [ + { + "email": "contact@cpluspatch.com", + "name": "CPlusPatch", + "url": "https://cpluspatch.com" } + ], + "repository": { + "type": "git", + "url": "git+https://github.com/lysand-org/lysand.git" + }, + "private": true, + "scripts": { + "dev": "bun run --watch index.ts", + "vite:dev": "bunx --bun vite pages", + "vite:build": "bunx --bun vite build pages", + "fe:dev": "bun --bun nuxt dev packages/frontend", + "fe:build": "bun --bun nuxt build packages/frontend", + "fe:analyze": "bun --bun nuxt analyze packages/frontend", + "start": "NITRO_PORT=5173 bun run dist/frontend/server/index.mjs & NODE_ENV=production bun run dist/index.js --prod", + "migrate-dev": "bun prisma migrate dev", + "migrate": "bun prisma migrate deploy", + "lint": "bunx --bun eslint --config .eslintrc.cjs --ext .ts .", + "prod-build": "bun run build.ts", + "prisma": "DATABASE_URL=$(bun run prisma.ts) bunx prisma", + "generate": "bun prisma generate", + "benchmark:timeline": "bun run benchmarks/timelines.ts", + "cloc": "cloc . --exclude-dir node_modules,dist", + "cli": "bun run cli.ts" + }, + "trustedDependencies": [ + "@biomejs/biome", + "@fortawesome/fontawesome-common-types", + "@fortawesome/free-regular-svg-icons", + "@fortawesome/free-solid-svg-icons", + "@prisma/client", + "@prisma/engines", + "es5-ext", + "esbuild", + "json-editor-vue", + "msgpackr-extract", + "nuxt-app", + "prisma", + "sharp", + "vue-demi" + ], + "devDependencies": { + "@biomejs/biome": "1.6.4", + "@img/sharp-wasm32": "^0.33.3", + "@julr/unocss-preset-forms": "^0.1.0", + "@nuxtjs/seo": "^2.0.0-rc.10", + "@nuxtjs/tailwindcss": "^6.11.4", + "@types/cli-table": "^0.3.4", + "@types/html-to-text": "^9.0.4", + "@types/ioredis": "^5.0.0", + "@types/jsonld": "^1.5.13", + "@types/mime-types": "^2.1.4", + "@types/pg": "^8.11.5", + "@typescript-eslint/eslint-plugin": "latest", + "@unocss/cli": "latest", + "@unocss/transformer-directives": "^0.59.0", + "@vitejs/plugin-vue": "latest", + "@vueuse/head": "^2.0.0", + "activitypub-types": "^1.0.3", + "bun-types": "latest", + "drizzle-kit": "^0.20.14", + "shiki": "^1.2.4", + "typescript": "latest", + "unocss": "latest", + "untyped": "^1.4.2", + "vite": "^5.2.8", + "vite-ssr": "^0.17.1", + "vue": "^3.3.9", + "vue-router": "^4.2.5", + "vue-tsc": "latest" + }, + "peerDependencies": { + "typescript": "^5.3.2" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.461.0", + "@iarna/toml": "^2.2.5", + "@json2csv/plainjs": "^7.0.6", + "@prisma/client": "^5.6.0", + "blurhash": "^2.0.5", + "bullmq": "latest", + "chalk": "^5.3.0", + "cli-parser": "workspace:*", + "cli-table": "^0.3.11", + "config-manager": "workspace:*", + "drizzle-orm": "^0.30.7", + "eventemitter3": "^5.0.1", + "extract-zip": "^2.0.1", + "html-to-text": "^9.0.5", + "ioredis": "^5.3.2", + "ip-matching": "^2.1.2", + "iso-639-1": "^3.1.0", + "isomorphic-dompurify": "latest", + "jsonld": "^8.3.1", + "linkify-html": "^4.1.3", + "linkify-string": "^4.1.3", + "linkifyjs": "^4.1.3", + "log-manager": "workspace:*", + "marked": "latest", + "media-manager": "workspace:*", + "megalodon": "^10.0.0", + "meilisearch": "latest", + "merge-deep-ts": "^1.2.6", + "mime-types": "^2.1.35", + "next-route-matcher": "^1.0.1", + "oauth4webapi": "^2.4.0", + "pg": "^8.11.5", + "prisma": "^5.6.0", + "prisma-json-types-generator": "^3.0.4", + "prisma-redis-middleware": "^4.8.0", + "request-parser": "workspace:*", + "semver": "^7.5.4", + "sharp": "^0.33.3", + "strip-ansi": "^7.1.0" + } } diff --git a/server/api/api/v1/accounts/[id]/statuses.ts b/server/api/api/v1/accounts/[id]/statuses.ts index 59268581..2c1fa70a 100644 --- a/server/api/api/v1/accounts/[id]/statuses.ts +++ b/server/api/api/v1/accounts/[id]/statuses.ts @@ -1,11 +1,14 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { fetchTimeline } from "@timelines"; +import { sql } from "drizzle-orm"; import { client } from "~database/datasource"; import { + findManyStatuses, statusToAPI, type StatusWithRelations, } from "~database/entities/Status"; +import { findFirstUser } from "~database/entities/User"; import { statusAndUserRelations, userRelations, @@ -52,38 +55,30 @@ export default apiRoute<{ pinned, } = extraData.parsedRequest; - const user = await client.user.findUnique({ - where: { id }, - include: userRelations, + const user = await findFirstUser({ + where: (user, { eq }) => eq(user.id, id), }); if (!user) return errorResponse("User not found", 404); if (pinned) { const { objects, link } = await fetchTimeline( - client.status, + findManyStatuses, { - where: { - authorId: id, - reblogId: null, - pinnedBy: { - some: { - id: user.id, - }, - }, - // If only_media is true, only return statuses with attachments - attachments: only_media ? { some: {} } : undefined, - id: { - lt: max_id, - gt: min_id, - gte: since_id, - }, - }, - include: statusAndUserRelations, - take: Number(limit), - orderBy: { - id: "desc", - }, + // @ts-ignore + where: (status, { and, lt, gt, gte, eq, sql }) => + and( + max_id ? lt(status.id, max_id) : undefined, + since_id ? gte(status.id, since_id) : undefined, + min_id ? gt(status.id, min_id) : undefined, + eq(status.authorId, id), + sql`EXISTS (SELECT 1 FROM _UserPinnedNotes WHERE _UserPinnedNotes.status_id = ${status.id} AND _UserPinnedNotes.user_id = ${user.id})`, + only_media + ? sql`EXISTS (SELECT 1 FROM "Attachment" WHERE "Attachment"."statusId" = ${status.id})` + : undefined, + ), + // @ts-expect-error Yes I KNOW the types are wrong + orderBy: (status, { desc }) => desc(status.id), }, req, ); @@ -100,22 +95,22 @@ export default apiRoute<{ } const { objects, link } = await fetchTimeline( - client.status, + findManyStatuses, { - where: { - authorId: id, - reblogId: exclude_reblogs ? null : undefined, - id: { - lt: max_id, - gt: min_id, - gte: since_id, - }, - }, - include: statusAndUserRelations, - take: Number(limit), - orderBy: { - id: "desc", - }, + // @ts-ignore + where: (status, { and, lt, gt, gte, eq, sql }) => + and( + max_id ? lt(status.id, max_id) : undefined, + since_id ? gte(status.id, since_id) : undefined, + min_id ? gt(status.id, min_id) : undefined, + eq(status.authorId, id), + only_media + ? sql`EXISTS (SELECT 1 FROM "Attachment" WHERE "Attachment"."statusId" = ${status.id})` + : undefined, + exclude_reblogs ? eq(status.reblogId, null) : undefined, + ), + // @ts-expect-error Yes I KNOW the types are wrong + orderBy: (status, { desc }) => desc(status.id), }, req, ); diff --git a/server/api/api/v1/apps/verify_credentials/index.ts b/server/api/api/v1/apps/verify_credentials/index.ts index e26cf739..6ddc8fad 100644 --- a/server/api/api/v1/apps/verify_credentials/index.ts +++ b/server/api/api/v1/apps/verify_credentials/index.ts @@ -27,8 +27,8 @@ export default apiRoute(async (req, matchedRoute, extraData) => { return jsonResponse({ name: application.name, website: application.website, - vapid_key: application.vapid_key, - redirect_uris: application.redirect_uris, + vapid_key: application.vapidKey, + redirect_uris: application.redirectUris, scopes: application.scopes, }); }); diff --git a/server/api/api/v1/blocks/index.ts b/server/api/api/v1/blocks/index.ts index 79e115dc..d354b7f3 100644 --- a/server/api/api/v1/blocks/index.ts +++ b/server/api/api/v1/blocks/index.ts @@ -1,9 +1,11 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { fetchTimeline } from "@timelines"; -import { client } from "~database/datasource"; -import { userToAPI, type UserWithRelations } from "~database/entities/User"; -import { userRelations } from "~database/entities/relations"; +import { + findManyUsers, + userToAPI, + type UserWithRelations, +} from "~database/entities/User"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -28,25 +30,22 @@ export default apiRoute<{ if (!user) return errorResponse("Unauthorized", 401); - const { max_id, since_id, limit = 40 } = extraData.parsedRequest; + const { max_id, since_id, min_id, limit = 40 } = extraData.parsedRequest; const { objects: blocks, link } = await fetchTimeline( - client.user, + findManyUsers, { - where: { - relationshipSubjects: { - some: { - ownerId: user.id, - blocking: true, - }, - }, - id: { - lt: max_id, - gte: since_id, - }, - }, - include: userRelations, - take: Number(limit), + // @ts-expect-error Yes I KNOW the types are wrong + where: (subject, { lt, gte, gt, and, sql }) => + and( + max_id ? lt(subject.id, max_id) : undefined, + since_id ? gte(subject.id, since_id) : undefined, + min_id ? gt(subject.id, min_id) : undefined, + sql`EXISTS (SELECT 1 FROM "Relationship" WHERE "Relationship"."subjectId" = ${subject.id} AND "Relationship"."ownerId" = ${user.id} AND "Relationship"."blocking" = true)`, + ), + limit: Number(limit), + // @ts-expect-error Yes I KNOW the types are wrong + orderBy: (subject, { desc }) => desc(subject.id), }, req, ); diff --git a/server/api/api/v1/custom_emojis/index.ts b/server/api/api/v1/custom_emojis/index.ts index 80e91a25..2cd886ed 100644 --- a/server/api/api/v1/custom_emojis/index.ts +++ b/server/api/api/v1/custom_emojis/index.ts @@ -2,6 +2,7 @@ import { apiRoute, applyConfig } from "@api"; import { jsonResponse } from "@response"; import { client } from "~database/datasource"; import { emojiToAPI } from "~database/entities/Emoji"; +import { db } from "~drizzle/db"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -16,9 +17,10 @@ export const meta = applyConfig({ }); export default apiRoute(async () => { - const emojis = await client.emoji.findMany({ - where: { - instanceId: null, + const emojis = await db.query.emoji.findMany({ + where: (emoji, { isNull }) => isNull(emoji.instanceId), + with: { + instance: true, }, }); diff --git a/server/api/api/v1/instance/index.ts b/server/api/api/v1/instance/index.ts index f16775b9..e16d310b 100644 --- a/server/api/api/v1/instance/index.ts +++ b/server/api/api/v1/instance/index.ts @@ -1,8 +1,11 @@ import { apiRoute, applyConfig } from "@api"; import { jsonResponse } from "@response"; +import { count, isNull } from "drizzle-orm"; import { client } from "~database/datasource"; import { userToAPI } from "~database/entities/User"; import { userRelations } from "~database/entities/relations"; +import { db } from "~drizzle/db"; +import { status, user } from "~drizzle/schema"; import manifest from "~package.json"; import type { APIInstance } from "~types/entities/instance"; @@ -24,16 +27,23 @@ export default apiRoute(async (req, matchedRoute, extraData) => { // Get software version from package.json const version = manifest.version; - const statusCount = await client.status.count({ - where: { - instanceId: null, - }, - }); - const userCount = await client.user.count({ - where: { - instanceId: null, - }, - }); + const statusCount = ( + await db + .select({ + count: count(), + }) + .from(status) + .where(isNull(status.instanceId)) + )[0].count; + + const userCount = ( + await db + .select({ + count: count(), + }) + .from(user) + .where(isNull(user.instanceId)) + )[0].count; // Get the first created admin user const contactAccount = await client.user.findFirst({ @@ -47,10 +57,6 @@ export default apiRoute(async (req, matchedRoute, extraData) => { include: userRelations, }); - if (!contactAccount) { - throw new Error("No admin user found"); - } - // Get user that have posted once in the last 30 days const monthlyActiveUsers = await client.user.count({ where: { @@ -189,7 +195,8 @@ export default apiRoute(async (req, matchedRoute, extraData) => { }, vapid_public_key: "", }, - contact_account: userToAPI(contactAccount), + // @ts-expect-error Sometimes there just isnt an admin + contact_account: contactAccount ? userToAPI(contactAccount) : undefined, } satisfies APIInstance & { pleroma: object; }); diff --git a/server/api/api/v1/mutes/index.ts b/server/api/api/v1/mutes/index.ts index 64be2713..00d21e02 100644 --- a/server/api/api/v1/mutes/index.ts +++ b/server/api/api/v1/mutes/index.ts @@ -2,7 +2,11 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { fetchTimeline } from "@timelines"; import { client } from "~database/datasource"; -import { userToAPI, type UserWithRelations } from "~database/entities/User"; +import { + findManyUsers, + userToAPI, + type UserWithRelations, +} from "~database/entities/User"; import { userRelations } from "~database/entities/relations"; export const meta = applyConfig({ @@ -21,30 +25,28 @@ export const meta = applyConfig({ export default apiRoute<{ max_id?: string; since_id?: string; + min_id?: string; limit?: number; }>(async (req, matchedRoute, extraData) => { const { user } = extraData.auth; - const { max_id, since_id, limit = 40 } = extraData.parsedRequest; + const { max_id, since_id, limit = 40, min_id } = extraData.parsedRequest; if (!user) return errorResponse("Unauthorized", 401); const { objects: blocks, link } = await fetchTimeline( - client.user, + findManyUsers, { - where: { - relationshipSubjects: { - some: { - ownerId: user.id, - muting: true, - }, - }, - id: { - lt: max_id, - gte: since_id, - }, - }, - include: userRelations, - take: Number(limit), + // @ts-expect-error Yes I KNOW the types are wrong + where: (subject, { lt, gte, gt, and, sql }) => + and( + max_id ? lt(subject.id, max_id) : undefined, + since_id ? gte(subject.id, since_id) : undefined, + min_id ? gt(subject.id, min_id) : undefined, + sql`EXISTS (SELECT 1 FROM "Relationship" WHERE "Relationship"."subjectId" = ${subject.id} AND "Relationship"."ownerId" = ${user.id} AND "Relationship"."muting" = true)`, + ), + limit: Number(limit), + // @ts-expect-error Yes I KNOW the types are wrong + orderBy: (subject, { desc }) => desc(subject.id), }, req, ); diff --git a/server/api/api/v1/statuses/[id]/context.ts b/server/api/api/v1/statuses/[id]/context.ts index 0c0fff7d..1aa64fd9 100644 --- a/server/api/api/v1/statuses/[id]/context.ts +++ b/server/api/api/v1/statuses/[id]/context.ts @@ -1,12 +1,13 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; -import { client } from "~database/datasource"; +import type { Relationship } from "~database/entities/Relationship"; import { + findFirstStatuses, getAncestors, getDescendants, statusToAPI, } from "~database/entities/Status"; -import { statusAndUserRelations } from "~database/entities/relations"; +import { db } from "~drizzle/db"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -30,16 +31,47 @@ export default apiRoute(async (req, matchedRoute, extraData) => { const { user } = extraData.auth; - const foundStatus = await client.status.findUnique({ - where: { id }, - include: statusAndUserRelations, + const foundStatus = await findFirstStatuses({ + where: (status, { eq }) => eq(status.id, id), }); if (!foundStatus) return errorResponse("Record not found", 404); + const relations = user + ? await db.query.relationship.findMany({ + where: (relationship, { eq }) => + eq(relationship.ownerId, user.id), + }) + : null; + + const relationSubjects = user + ? await db.query.relationship.findMany({ + where: (relationship, { eq }) => + eq(relationship.subjectId, user.id), + }) + : null; + // Get all ancestors - const ancestors = await getAncestors(foundStatus, user); - const descendants = await getDescendants(foundStatus, user); + const ancestors = await getAncestors( + foundStatus, + user + ? { + ...user, + relationships: relations as Relationship[], + relationshipSubjects: relationSubjects as Relationship[], + } + : null, + ); + const descendants = await getDescendants( + foundStatus, + user + ? { + ...user, + relationships: relations as Relationship[], + relationshipSubjects: relationSubjects as Relationship[], + } + : null, + ); return jsonResponse({ ancestors: await Promise.all( diff --git a/server/api/api/v1/statuses/[id]/favourite.ts b/server/api/api/v1/statuses/[id]/favourite.ts index 7028bf7a..de9355fa 100644 --- a/server/api/api/v1/statuses/[id]/favourite.ts +++ b/server/api/api/v1/statuses/[id]/favourite.ts @@ -2,7 +2,11 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { client } from "~database/datasource"; import { createLike } from "~database/entities/Like"; -import { isViewableByUser, statusToAPI } from "~database/entities/Status"; +import { + findFirstStatuses, + isViewableByUser, + statusToAPI, +} from "~database/entities/Status"; import { statusAndUserRelations } from "~database/entities/relations"; import type { APIStatus } from "~types/entities/status"; @@ -28,9 +32,8 @@ export default apiRoute(async (req, matchedRoute, extraData) => { if (!user) return errorResponse("Unauthorized", 401); - const status = await client.status.findUnique({ - where: { id }, - include: statusAndUserRelations, + const status = await findFirstStatuses({ + where: (status, { eq }) => eq(status.id, id), }); // Check if user is authorized to view this status (if it's private) @@ -51,6 +54,6 @@ export default apiRoute(async (req, matchedRoute, extraData) => { return jsonResponse({ ...(await statusToAPI(status, user)), favourited: true, - favourites_count: status._count.likes + 1, + favourites_count: status.likeCount + 1, } as APIStatus); }); diff --git a/server/api/api/v1/statuses/[id]/favourited_by.ts b/server/api/api/v1/statuses/[id]/favourited_by.ts index 7c554e5e..5a23f6cf 100644 --- a/server/api/api/v1/statuses/[id]/favourited_by.ts +++ b/server/api/api/v1/statuses/[id]/favourited_by.ts @@ -1,13 +1,12 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { fetchTimeline } from "@timelines"; -import { client } from "~database/datasource"; -import { isViewableByUser } from "~database/entities/Status"; -import { userToAPI, type UserWithRelations } from "~database/entities/User"; +import { findFirstStatuses, isViewableByUser } from "~database/entities/Status"; import { - statusAndUserRelations, - userRelations, -} from "~database/entities/relations"; + findManyUsers, + userToAPI, + type UserWithRelations, +} from "~database/entities/User"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -34,9 +33,8 @@ export default apiRoute<{ const { user } = extraData.auth; - const status = await client.status.findUnique({ - where: { id }, - include: statusAndUserRelations, + const status = await findFirstStatuses({ + where: (status, { eq }) => eq(status.id, id), }); // Check if user is authorized to view this status (if it's private) @@ -50,32 +48,18 @@ export default apiRoute<{ if (limit < 1) return errorResponse("Invalid limit", 400); const { objects, link } = await fetchTimeline( - client.user, + findManyUsers, { - where: { - likes: { - some: { - likedId: status.id, - }, - }, - id: { - lt: max_id, - gte: since_id, - gt: min_id, - }, - }, - include: { - ...userRelations, - likes: { - where: { - likedId: status.id, - }, - }, - }, - take: Number(limit), - orderBy: { - id: "desc", - }, + // @ts-ignore + where: (liker, { and, lt, gt, gte, eq, sql }) => + and( + max_id ? lt(liker.id, max_id) : undefined, + since_id ? gte(liker.id, since_id) : undefined, + min_id ? gt(liker.id, min_id) : undefined, + sql`EXISTS (SELECT 1 FROM "Like" WHERE "Like"."likedId" = ${status.id} AND "Like"."likerId" = ${liker.id})`, + ), + // @ts-expect-error Yes I KNOW the types are wrong + orderBy: (liker, { desc }) => desc(liker.id), }, req, ); diff --git a/server/api/api/v1/statuses/[id]/index.ts b/server/api/api/v1/statuses/[id]/index.ts index a0f6c476..ab0e8033 100644 --- a/server/api/api/v1/statuses/[id]/index.ts +++ b/server/api/api/v1/statuses/[id]/index.ts @@ -1,14 +1,17 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { sanitizeHtml } from "@sanitization"; +import { eq } from "drizzle-orm"; import { parse } from "marked"; -import { client } from "~database/datasource"; import { editStatus, + findFirstStatuses, isViewableByUser, statusToAPI, } from "~database/entities/Status"; import { statusAndUserRelations } from "~database/entities/relations"; +import { db } from "~drizzle/db"; +import { status } from "~drizzle/schema"; export const meta = applyConfig({ allowedMethods: ["GET", "DELETE", "PUT"], @@ -42,37 +45,32 @@ export default apiRoute<{ const { user } = extraData.auth; - const status = await client.status.findUnique({ - where: { id }, - include: statusAndUserRelations, + const foundStatus = await findFirstStatuses({ + where: (status, { eq }) => eq(status.id, id), }); const config = await extraData.configManager.getConfig(); // Check if user is authorized to view this status (if it's private) - if (!status || !isViewableByUser(status, user)) + if (!foundStatus || !isViewableByUser(foundStatus, user)) return errorResponse("Record not found", 404); if (req.method === "GET") { - return jsonResponse(await statusToAPI(status)); + return jsonResponse(await statusToAPI(foundStatus)); } if (req.method === "DELETE") { - if (status.authorId !== user?.id) { + if (foundStatus.authorId !== user?.id) { return errorResponse("Unauthorized", 401); } // TODO: Implement delete and redraft functionality - // Get associated Status object - // Delete status and all associated objects - await client.status.delete({ - where: { id }, - }); + await db.delete(status).where(eq(status.id, id)); return jsonResponse( { - ...(await statusToAPI(status, user)), + ...(await statusToAPI(foundStatus, user)), // TODO: Add // text: Add source text // poll: Add source poll @@ -82,7 +80,7 @@ export default apiRoute<{ ); } if (req.method === "PUT") { - if (status.authorId !== user?.id) { + if (foundStatus.authorId !== user?.id) { return errorResponse("Unauthorized", 401); } @@ -191,21 +189,19 @@ export default apiRoute<{ } // Check if media attachments are all valid + if (media_ids && media_ids.length > 0) { + const foundAttachments = await db.query.attachment.findMany({ + where: (attachment, { inArray }) => + inArray(attachment.id, media_ids), + }); - const foundAttachments = await client.attachment.findMany({ - where: { - id: { - in: media_ids ?? [], - }, - }, - }); - - if (foundAttachments.length !== (media_ids ?? []).length) { - return errorResponse("Invalid media IDs", 422); + if (foundAttachments.length !== (media_ids ?? []).length) { + return errorResponse("Invalid media IDs", 422); + } } // Update status - const newStatus = await editStatus(status, { + const newStatus = await editStatus(foundStatus, { content: sanitizedStatus, content_type, media_attachments: media_ids, @@ -213,6 +209,10 @@ export default apiRoute<{ sensitive: sensitive ?? false, }); + if (!newStatus) { + return errorResponse("Failed to update status", 500); + } + return jsonResponse(await statusToAPI(newStatus, user)); } diff --git a/server/api/api/v1/statuses/[id]/pin.ts b/server/api/api/v1/statuses/[id]/pin.ts index 21bf22bd..19143766 100644 --- a/server/api/api/v1/statuses/[id]/pin.ts +++ b/server/api/api/v1/statuses/[id]/pin.ts @@ -1,8 +1,10 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { client } from "~database/datasource"; -import { statusToAPI } from "~database/entities/Status"; +import { findFirstStatuses, statusToAPI } from "~database/entities/Status"; import { statusAndUserRelations } from "~database/entities/relations"; +import { db } from "~drizzle/db"; +import { statusToUser } from "~drizzle/schema"; export const meta = applyConfig({ allowedMethods: ["POST"], @@ -26,34 +28,34 @@ export default apiRoute(async (req, matchedRoute, extraData) => { if (!user) return errorResponse("Unauthorized", 401); - let status = await client.status.findUnique({ - where: { id }, - include: statusAndUserRelations, + const foundStatus = await findFirstStatuses({ + where: (status, { eq }) => eq(status.id, id), }); // Check if status exists - if (!status) return errorResponse("Record not found", 404); + if (!foundStatus) return errorResponse("Record not found", 404); // Check if status is user's - if (status.authorId !== user.id) return errorResponse("Unauthorized", 401); + if (foundStatus.authorId !== user.id) + return errorResponse("Unauthorized", 401); - await client.user.update({ - where: { id: user.id }, - data: { - pinnedNotes: { - connect: { - id: status.id, - }, - }, - }, + // Check if post is already pinned + if ( + await db.query.statusToUser.findFirst({ + where: (statusToUser, { and, eq }) => + and( + eq(statusToUser.a, foundStatus.id), + eq(statusToUser.b, user.id), + ), + }) + ) { + return errorResponse("Already pinned", 422); + } + + await db.insert(statusToUser).values({ + a: foundStatus.id, + b: user.id, }); - status = await client.status.findUnique({ - where: { id }, - include: statusAndUserRelations, - }); - - if (!status) return errorResponse("Record not found", 404); - - return jsonResponse(statusToAPI(status, user)); + return jsonResponse(statusToAPI(foundStatus, user)); }); diff --git a/server/api/api/v1/statuses/[id]/reblog.ts b/server/api/api/v1/statuses/[id]/reblog.ts index 9f4c33fb..fdf8f8d3 100644 --- a/server/api/api/v1/statuses/[id]/reblog.ts +++ b/server/api/api/v1/statuses/[id]/reblog.ts @@ -1,9 +1,13 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; -import { client } from "~database/datasource"; -import { isViewableByUser, statusToAPI } from "~database/entities/Status"; -import type { UserWithRelations } from "~database/entities/User"; +import { + findFirstStatuses, + isViewableByUser, + statusToAPI, +} from "~database/entities/Status"; import { statusAndUserRelations } from "~database/entities/relations"; +import { db } from "~drizzle/db"; +import { notification, status } from "~drizzle/schema"; export const meta = applyConfig({ allowedMethods: ["POST"], @@ -31,47 +35,57 @@ export default apiRoute<{ const { visibility = "public" } = extraData.parsedRequest; - const status = await client.status.findUnique({ - where: { id }, - include: statusAndUserRelations, + const foundStatus = await findFirstStatuses({ + where: (status, { eq }) => eq(status.id, id), }); // Check if user is authorized to view this status (if it's private) - if (!status || !isViewableByUser(status, user)) + if (!foundStatus || !isViewableByUser(foundStatus, user)) return errorResponse("Record not found", 404); - const existingReblog = await client.status.findFirst({ - where: { - authorId: user.id, - reblogId: status.id, - }, + const existingReblog = await db.query.status.findFirst({ + where: (status, { and, eq }) => + and(eq(status.authorId, user.id), eq(status.reblogId, status.id)), }); if (existingReblog) { return errorResponse("Already reblogged", 422); } - const newReblog = await client.status.create({ - data: { - authorId: user.id, - reblogId: status.id, - visibility, - sensitive: false, - }, - include: statusAndUserRelations, + const newReblog = ( + await db + .insert(status) + .values({ + authorId: user.id, + reblogId: foundStatus.id, + visibility, + sensitive: false, + updatedAt: new Date().toISOString(), + }) + .returning() + )[0]; + + if (!newReblog) { + return errorResponse("Failed to reblog", 500); + } + + const finalNewReblog = await findFirstStatuses({ + where: (status, { eq }) => eq(status.id, newReblog.id), }); + if (!finalNewReblog) { + return errorResponse("Failed to reblog", 500); + } + // Create notification for reblog if reblogged user is on the same instance - if (status.author.instanceId === user.instanceId) { - await client.notification.create({ - data: { - accountId: user.id, - notifiedId: status.authorId, - type: "reblog", - statusId: status.reblogId, - }, + if (foundStatus.author.instanceId === user.instanceId) { + await db.insert(notification).values({ + accountId: user.id, + notifiedId: foundStatus.authorId, + type: "reblog", + statusId: foundStatus.reblogId, }); } - return jsonResponse(await statusToAPI(newReblog, user)); + return jsonResponse(await statusToAPI(finalNewReblog, user)); }); diff --git a/server/api/api/v1/statuses/[id]/reblogged_by.ts b/server/api/api/v1/statuses/[id]/reblogged_by.ts index 0a6087c1..8b06c016 100644 --- a/server/api/api/v1/statuses/[id]/reblogged_by.ts +++ b/server/api/api/v1/statuses/[id]/reblogged_by.ts @@ -1,13 +1,12 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { fetchTimeline } from "@timelines"; -import { client } from "~database/datasource"; -import { isViewableByUser } from "~database/entities/Status"; -import { type UserWithRelations, userToAPI } from "~database/entities/User"; +import { findFirstStatuses, isViewableByUser } from "~database/entities/Status"; import { - statusAndUserRelations, - userRelations, -} from "~database/entities/relations"; + type UserWithRelations, + userToAPI, + findManyUsers, +} from "~database/entities/User"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -34,9 +33,8 @@ export default apiRoute<{ const { user } = extraData.auth; - const status = await client.status.findUnique({ - where: { id }, - include: statusAndUserRelations, + const status = await findFirstStatuses({ + where: (status, { eq }) => eq(status.id, id), }); // Check if user is authorized to view this status (if it's private) @@ -55,33 +53,18 @@ export default apiRoute<{ if (limit < 1) return errorResponse("Invalid limit", 400); const { objects, link } = await fetchTimeline( - client.user, + findManyUsers, { - where: { - statuses: { - some: { - reblogId: status.id, - }, - }, - id: { - lt: max_id ?? undefined, - gte: since_id ?? undefined, - gt: min_id ?? undefined, - }, - }, - include: { - ...userRelations, - statuses: { - where: { - reblogId: status.id, - }, - include: statusAndUserRelations, - }, - }, - take: Number(limit), - orderBy: { - id: "desc", - }, + // @ts-ignore + where: (reblogger, { and, lt, gt, gte, eq, sql }) => + and( + max_id ? lt(reblogger.id, max_id) : undefined, + since_id ? gte(reblogger.id, since_id) : undefined, + min_id ? gt(reblogger.id, min_id) : undefined, + sql`EXISTS (SELECT 1 FROM "Status" WHERE "Status"."reblogId" = ${status.id} AND "Status"."authorId" = ${reblogger.id})`, + ), + // @ts-expect-error Yes I KNOW the types are wrong + orderBy: (liker, { desc }) => desc(liker.id), }, req, ); diff --git a/server/api/api/v1/statuses/[id]/unfavourite.ts b/server/api/api/v1/statuses/[id]/unfavourite.ts index 6ec2c9d5..5705da7a 100644 --- a/server/api/api/v1/statuses/[id]/unfavourite.ts +++ b/server/api/api/v1/statuses/[id]/unfavourite.ts @@ -2,7 +2,11 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { client } from "~database/datasource"; import { deleteLike } from "~database/entities/Like"; -import { isViewableByUser, statusToAPI } from "~database/entities/Status"; +import { + findFirstStatuses, + isViewableByUser, + statusToAPI, +} from "~database/entities/Status"; import { statusAndUserRelations } from "~database/entities/relations"; import type { APIStatus } from "~types/entities/status"; @@ -28,20 +32,19 @@ export default apiRoute(async (req, matchedRoute, extraData) => { if (!user) return errorResponse("Unauthorized", 401); - const status = await client.status.findUnique({ - where: { id }, - include: statusAndUserRelations, + const foundStatus = await findFirstStatuses({ + where: (status, { eq }) => eq(status.id, id), }); // Check if user is authorized to view this status (if it's private) - if (!status || !isViewableByUser(status, user)) + if (!foundStatus || !isViewableByUser(foundStatus, user)) return errorResponse("Record not found", 404); - await deleteLike(user, status); + await deleteLike(user, foundStatus); return jsonResponse({ - ...(await statusToAPI(status, user)), + ...(await statusToAPI(foundStatus, user)), favourited: false, - favourites_count: status._count.likes - 1, + favourites_count: foundStatus.likeCount - 1, } as APIStatus); }); diff --git a/server/api/api/v1/statuses/[id]/unreblog.ts b/server/api/api/v1/statuses/[id]/unreblog.ts index 86ae6d46..6b3ad75f 100644 --- a/server/api/api/v1/statuses/[id]/unreblog.ts +++ b/server/api/api/v1/statuses/[id]/unreblog.ts @@ -1,8 +1,13 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; -import { client } from "~database/datasource"; -import { isViewableByUser, statusToAPI } from "~database/entities/Status"; -import { statusAndUserRelations } from "~database/entities/relations"; +import { eq } from "drizzle-orm"; +import { + findFirstStatuses, + isViewableByUser, + statusToAPI, +} from "~database/entities/Status"; +import { db } from "~drizzle/db"; +import { status } from "~drizzle/schema"; import type { APIStatus } from "~types/entities/status"; export const meta = applyConfig({ @@ -27,33 +32,28 @@ export default apiRoute(async (req, matchedRoute, extraData) => { if (!user) return errorResponse("Unauthorized", 401); - const status = await client.status.findUnique({ - where: { id }, - include: statusAndUserRelations, + const foundStatus = await findFirstStatuses({ + where: (status, { eq }) => eq(status.id, id), }); // Check if user is authorized to view this status (if it's private) - if (!status || !isViewableByUser(status, user)) + if (!foundStatus || !isViewableByUser(foundStatus, user)) return errorResponse("Record not found", 404); - const existingReblog = await client.status.findFirst({ - where: { - authorId: user.id, - reblogId: status.id, - }, + const existingReblog = await findFirstStatuses({ + where: (status, { eq }) => + eq(status.authorId, user.id) && eq(status.reblogId, foundStatus.id), }); if (!existingReblog) { return errorResponse("Not already reblogged", 422); } - await client.status.delete({ - where: { id: existingReblog.id }, - }); + await db.delete(status).where(eq(status.id, existingReblog.id)); return jsonResponse({ - ...(await statusToAPI(status, user)), + ...(await statusToAPI(foundStatus, user)), reblogged: false, - reblogs_count: status._count.reblogs - 1, + reblogs_count: foundStatus.reblogCount - 1, } as APIStatus); }); diff --git a/server/api/api/v1/statuses/index.ts b/server/api/api/v1/statuses/index.ts index 92779c34..26358d2d 100644 --- a/server/api/api/v1/statuses/index.ts +++ b/server/api/api/v1/statuses/index.ts @@ -8,6 +8,7 @@ import type { StatusWithRelations } from "~database/entities/Status"; import { createNewStatus, federateStatus, + findFirstStatuses, parseTextMentions, statusToAPI, } from "~database/entities/Status"; @@ -46,9 +47,9 @@ export default apiRoute<{ scheduled_at?: string; local_only?: boolean; content_type?: string; + federate?: boolean; }>(async (req, matchedRoute, extraData) => { const { user, token } = extraData.auth; - const application = await getFromToken(token); if (!user) return errorResponse("Unauthorized", 401); @@ -69,6 +70,7 @@ export default apiRoute<{ spoiler_text, visibility, content_type, + federate = true, } = extraData.parsedRequest; // Validate status @@ -173,9 +175,8 @@ export default apiRoute<{ let quote: StatusWithRelations | null = null; if (in_reply_to_id) { - replyStatus = await client.status.findUnique({ - where: { id: in_reply_to_id }, - include: statusAndUserRelations, + replyStatus = await findFirstStatuses({ + where: (status, { eq }) => eq(status.id, in_reply_to_id), }); if (!replyStatus) { @@ -184,9 +185,8 @@ export default apiRoute<{ } if (quote_id) { - quote = await client.status.findUnique({ - where: { id: quote_id }, - include: statusAndUserRelations, + quote = await findFirstStatuses({ + where: (status, { eq }) => eq(status.id, quote_id), }); if (!quote) { @@ -233,7 +233,13 @@ export default apiRoute<{ quote ?? undefined, ); - await federateStatus(newStatus); + if (!newStatus) { + return errorResponse("Failed to create status", 500); + } + + if (federate) { + await federateStatus(newStatus); + } return jsonResponse(await statusToAPI(newStatus, user)); }); diff --git a/server/api/api/v1/timelines/home.ts b/server/api/api/v1/timelines/home.ts index 441f0a61..a46e47d5 100644 --- a/server/api/api/v1/timelines/home.ts +++ b/server/api/api/v1/timelines/home.ts @@ -5,8 +5,10 @@ import { client } from "~database/datasource"; import { type StatusWithRelations, statusToAPI, + findManyStatuses, } from "~database/entities/Status"; import { statusAndUserRelations } from "~database/entities/relations"; +import { db } from "~drizzle/db"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -39,48 +41,40 @@ export default apiRoute<{ if (!user) return errorResponse("Unauthorized", 401); + const followers = await db.query.relationship.findMany({ + where: (relationship, { eq, and }) => + and( + eq(relationship.subjectId, user.id), + eq(relationship.following, true), + ), + }); + const { objects, link } = await fetchTimeline( - client.status, + findManyStatuses, { - where: { - id: { - lt: max_id ?? undefined, - gte: since_id ?? undefined, - gt: min_id ?? undefined, - }, - OR: [ - { - author: { - OR: [ - { - relationshipSubjects: { - some: { - ownerId: user.id, - following: true, - }, - }, - }, - { - id: user.id, - }, - ], - }, - }, - { - // Include posts where the user is mentioned in addition to posts by followed users - mentions: { - some: { - id: user.id, - }, - }, - }, - ], - }, - include: statusAndUserRelations, - take: Number(limit), - orderBy: { - id: "desc", - }, + // @ts-expect-error Yes I KNOW the types are wrong + where: (status, { lt, gte, gt, and, or, eq, inArray, sql }) => + or( + and( + max_id ? lt(status.id, max_id) : undefined, + since_id ? gte(status.id, since_id) : undefined, + min_id ? gt(status.id, min_id) : undefined, + ), + eq(status.authorId, user.id), + /* inArray( + status.authorId, + followers.map((f) => f.ownerId), + ), */ + // All statuses where the user is mentioned, using table StatusToUser which has a: status.id and b: user.id + // WHERE format (... = ...) + sql`EXISTS (SELECT 1 FROM "StatusToUser" WHERE "StatusToUser"."a" = ${status.id} AND "StatusToUser"."b" = ${user.id})`, + // All statuses from users that the user is following + // WHERE format (... = ...) + sql`EXISTS (SELECT 1 FROM "Relationship" WHERE "Relationship"."subjectId" = ${status.authorId} AND "Relationship"."ownerId" = ${user.id} AND "Relationship"."following" = true)`, + ), + limit: Number(limit), + // @ts-expect-error Yes I KNOW the types are wrong + orderBy: (status, { desc }) => desc(status.id), }, req, ); diff --git a/server/api/api/v1/timelines/public.ts b/server/api/api/v1/timelines/public.ts index 81e3642f..817e1dfc 100644 --- a/server/api/api/v1/timelines/public.ts +++ b/server/api/api/v1/timelines/public.ts @@ -3,6 +3,7 @@ import { errorResponse, jsonResponse } from "@response"; import { fetchTimeline } from "@timelines"; import { client } from "~database/datasource"; import { + findManyStatuses, statusToAPI, type StatusWithRelations, } from "~database/entities/Status"; @@ -49,27 +50,23 @@ export default apiRoute<{ } const { objects, link } = await fetchTimeline( - client.status, + findManyStatuses, { - where: { - id: { - lt: max_id ?? undefined, - gte: since_id ?? undefined, - gt: min_id ?? undefined, - }, - instanceId: remote - ? { - not: null, - } - : local - ? null - : undefined, - }, - include: statusAndUserRelations, - take: Number(limit), - orderBy: { - id: "desc", - }, + // @ts-expect-error Yes I KNOW the types are wrong + where: (status, { lt, gte, gt, and, isNull, isNotNull }) => + and( + max_id ? lt(status.id, max_id) : undefined, + since_id ? gte(status.id, since_id) : undefined, + min_id ? gt(status.id, min_id) : undefined, + remote + ? isNotNull(status.instanceId) + : local + ? isNull(status.instanceId) + : undefined, + ), + limit: Number(limit), + // @ts-expect-error Yes I KNOW the types are wrong + orderBy: (status, { desc }) => desc(status.id), }, req, ); diff --git a/server/api/api/v2/media/index.ts b/server/api/api/v2/media/index.ts index 31810dbf..d649991c 100644 --- a/server/api/api/v2/media/index.ts +++ b/server/api/api/v2/media/index.ts @@ -4,8 +4,9 @@ import { encode } from "blurhash"; import type { MediaBackend } from "media-manager"; import { MediaBackendType } from "media-manager"; import sharp from "sharp"; -import { client } from "~database/datasource"; import { attachmentToAPI, getUrl } from "~database/entities/Attachment"; +import { db } from "~drizzle/db"; +import { attachment } from "~drizzle/schema"; import { LocalMediaBackend, S3MediaBackend } from "~packages/media-manager"; export const meta = applyConfig({ @@ -134,19 +135,22 @@ export default apiRoute<{ thumbnailUrl = getUrl(path, config); } - const newAttachment = await client.attachment.create({ - data: { - url, - thumbnail_url: thumbnailUrl, - sha256: sha256.update(await file.arrayBuffer()).digest("hex"), - mime_type: file.type, - description: description ?? "", - size: file.size, - blurhash: blurhash ?? undefined, - width: metadata?.width ?? undefined, - height: metadata?.height ?? undefined, - }, - }); + const newAttachment = ( + await db + .insert(attachment) + .values({ + url, + thumbnailUrl, + sha256: sha256.update(await file.arrayBuffer()).digest("hex"), + mimeType: file.type, + description: description ?? "", + size: file.size, + blurhash: blurhash ?? undefined, + width: metadata?.width ?? undefined, + height: metadata?.height ?? undefined, + }) + .returning() + )[0]; // TODO: Add job to process videos and other media diff --git a/tests/api.test.ts b/tests/api.test.ts index 5399e6e0..dca97fd3 100644 --- a/tests/api.test.ts +++ b/tests/api.test.ts @@ -10,22 +10,34 @@ import { import type { APIEmoji } from "~types/entities/emoji"; import type { APIInstance } from "~types/entities/instance"; import { sendTestRequest, wrapRelativeUrl } from "./utils"; +import { db } from "~drizzle/db"; +import { inArray } from "drizzle-orm"; +import { application, user } from "~drizzle/schema"; const base_url = config.http.base_url; let token: Token; -let user: UserWithRelations; +let dummyUser: UserWithRelations; describe("API Tests", () => { beforeAll(async () => { + await db.delete(user).where(inArray(user.username, ["test", "test2"])); + await db + .delete(application) + .where(inArray(application.clientId, ["test"])); + // Initialize test user - user = await createNewLocalUser({ + dummyUser = await createNewLocalUser({ email: "test@test.com", username: "test", password: "test", display_name: "", }); + if (!dummyUser) { + throw new Error("Failed to create test user"); + } + token = await client.token.create({ data: { access_token: "test", @@ -45,7 +57,7 @@ describe("API Tests", () => { token_type: TokenType.BEARER, user: { connect: { - id: user.id, + id: dummyUser.id, }, }, }, @@ -53,19 +65,10 @@ describe("API Tests", () => { }); afterAll(async () => { - await client.user.deleteMany({ - where: { - username: { - in: ["test", "test2"], - }, - }, - }); - - await client.application.deleteMany({ - where: { - client_id: "test", - }, - }); + await db.delete(user).where(inArray(user.username, ["test", "test2"])); + await db + .delete(application) + .where(inArray(application.clientId, ["test"])); }); describe("GET /api/v1/instance", () => { @@ -89,7 +92,7 @@ describe("API Tests", () => { const instance = (await response.json()) as APIInstance; - expect(instance.uri).toBe(new URL(config.http.base_url).hostname); + expect(instance.uri).toBe(config.http.base_url); expect(instance.title).toBeDefined(); expect(instance.description).toBeDefined(); expect(instance.email).toBeDefined(); diff --git a/tests/api/statuses.test.ts b/tests/api/statuses.test.ts index a6b5dd7b..3599a313 100644 --- a/tests/api/statuses.test.ts +++ b/tests/api/statuses.test.ts @@ -126,6 +126,7 @@ describe("API Tests", () => { status: "Hello, world!", visibility: "public", media_ids: [media1?.id], + federate: false, }), }, ), @@ -173,6 +174,7 @@ describe("API Tests", () => { status: "This is a reply!", visibility: "public", in_reply_to_id: status?.id, + federate: false, }), }, ), diff --git a/tests/utils.ts b/tests/utils.ts index 53d23be2..fd022032 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -1,4 +1,4 @@ -import { server } from "~index"; +// import { server } from "~index"; /** * This allows us to send a test request to the server even when it isnt running @@ -7,7 +7,9 @@ import { server } from "~index"; * @returns Response from the server */ export async function sendTestRequest(req: Request) { - return server.fetch(req); + console.log(req); + return fetch(req); + // return server.fetch(req); } export function wrapRelativeUrl(url: string, base_url: string) { diff --git a/utils/meilisearch.ts b/utils/meilisearch.ts index 0e1234ff..655ad305 100644 --- a/utils/meilisearch.ts +++ b/utils/meilisearch.ts @@ -1,9 +1,10 @@ -import type { Status, User } from "@prisma/client"; import chalk from "chalk"; import { config } from "config-manager"; import { LogLevel, type LogManager, type MultiLogManager } from "log-manager"; import { Meilisearch } from "meilisearch"; import { client } from "~database/datasource"; +import type { Status } from "~database/entities/Status"; +import type { User } from "~database/entities/User"; export const meilisearch = new Meilisearch({ host: `${config.meilisearch.host}:${config.meilisearch.port}`, diff --git a/utils/timelines.ts b/utils/timelines.ts index a58632e0..f75019d6 100644 --- a/utils/timelines.ts +++ b/utils/timelines.ts @@ -1,20 +1,23 @@ -import type { Status, User, Prisma, Notification } from "@prisma/client"; +import type { findManyStatuses, Status } from "~database/entities/Status"; +import type { findManyUsers, User } from "~database/entities/User"; +import type { Notification } from "~database/entities/Notification"; +import type { db } from "~drizzle/db"; export async function fetchTimeline( model: - | Prisma.StatusDelegate - | Prisma.UserDelegate - | Prisma.NotificationDelegate, + | typeof findManyStatuses + | typeof findManyUsers + | typeof db.query.notification.findMany, args: - | Prisma.StatusFindManyArgs - | Prisma.UserFindManyArgs - | Prisma.NotificationFindManyArgs, + | Parameters[0] + | Parameters[0] + | Parameters[0], req: Request, ) { // BEFORE: Before in a top-to-bottom order, so the most recent posts // AFTER: After in a top-to-bottom order, so the oldest posts // @ts-expect-error This is a hack to get around the fact that Prisma doesn't have a common base type for all models - const objects = (await model.findMany(args)) as T[]; + const objects = (await model(args)) as T[]; // Constuct HTTP Link header (next and prev) only if there are more statuses const linkHeader = []; @@ -22,15 +25,11 @@ export async function fetchTimeline( if (objects.length > 0) { // Check if there are statuses before the first one // @ts-expect-error This is a hack to get around the fact that Prisma doesn't have a common base type for all models - const objectsBefore = await model.findMany({ + const objectsBefore = await model({ ...args, - where: { - ...args.where, - id: { - gt: objects[0].id, - }, - }, - take: 1, + // @ts-expect-error this hack breaks typing :( + where: (object, { gt }) => gt(object.id, objects[0].id), + limit: 1, }); if (objectsBefore.length > 0) { @@ -41,18 +40,16 @@ export async function fetchTimeline( ); } - if (objects.length < (args.take ?? Number.POSITIVE_INFINITY)) { + if ( + objects.length < (Number(args?.limit) ?? Number.POSITIVE_INFINITY) + ) { // Check if there are statuses after the last one - // @ts-expect-error This is a hack to get around the fact that Prisma doesn't have a common base type for all models - const objectsAfter = await model.findMany({ + // @ts-expect-error hack again + const objectsAfter = await model({ ...args, - where: { - ...args.where, - id: { - lt: objects.at(-1)?.id, - }, - }, - take: 1, + // @ts-expect-error this hack breaks typing :( + where: (object, { lt }) => lt(object.id, objects.at(-1).id), + limit: 1, }); if (objectsAfter.length > 0) {