From f7abe06a602fdf773b4d5f3ea385330640ea00e5 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Thu, 11 Apr 2024 01:39:07 -1000 Subject: [PATCH] Begin moving project to use Drizzle instead of prisma --- bun.lockb | Bin 609140 -> 650248 bytes database/entities/Application.ts | 21 +- database/entities/Attachment.ts | 40 +- database/entities/Emoji.ts | 113 +- database/entities/Federation.ts | 5 +- database/entities/Instance.ts | 33 +- database/entities/Like.ts | 54 +- database/entities/Notification.ts | 5 +- database/entities/Object.ts | 79 +- database/entities/Queue.ts | 65 - database/entities/Relationship.ts | 69 +- database/entities/Status.ts | 981 ++++++--- database/entities/User.ts | 585 ++++-- drizzle-scanned/0000_third_misty_knight.sql | 462 ++++ drizzle-scanned/meta/0000_snapshot.json | 1867 +++++++++++++++++ drizzle-scanned/meta/_journal.json | 13 + drizzle-scanned/schema.ts | 292 +++ drizzle.config.ts | 19 + drizzle/0000_illegal_living_lightning.sql | 462 ++++ drizzle/db.ts | 14 + drizzle/meta/0000_snapshot.json | 1867 +++++++++++++++++ drizzle/meta/_journal.json | 13 + drizzle/schema.ts | 677 ++++++ index.ts | 20 +- package.json | 267 +-- server/api/api/v1/accounts/[id]/statuses.ts | 75 +- .../api/v1/apps/verify_credentials/index.ts | 4 +- server/api/api/v1/blocks/index.ts | 37 +- server/api/api/v1/custom_emojis/index.ts | 8 +- server/api/api/v1/instance/index.ts | 37 +- server/api/api/v1/mutes/index.ts | 36 +- server/api/api/v1/statuses/[id]/context.ts | 46 +- server/api/api/v1/statuses/[id]/favourite.ts | 13 +- .../api/api/v1/statuses/[id]/favourited_by.ts | 52 +- server/api/api/v1/statuses/[id]/index.ts | 50 +- server/api/api/v1/statuses/[id]/pin.ts | 48 +- server/api/api/v1/statuses/[id]/reblog.ts | 72 +- .../api/api/v1/statuses/[id]/reblogged_by.ts | 53 +- .../api/api/v1/statuses/[id]/unfavourite.ts | 19 +- server/api/api/v1/statuses/[id]/unreblog.ts | 34 +- server/api/api/v1/statuses/index.ts | 22 +- server/api/api/v1/timelines/home.ts | 74 +- server/api/api/v1/timelines/public.ts | 37 +- server/api/api/v2/media/index.ts | 32 +- tests/api.test.ts | 37 +- tests/api/statuses.test.ts | 2 + tests/utils.ts | 6 +- utils/meilisearch.ts | 3 +- utils/timelines.ts | 49 +- 49 files changed, 7602 insertions(+), 1267 deletions(-) create mode 100644 drizzle-scanned/0000_third_misty_knight.sql create mode 100644 drizzle-scanned/meta/0000_snapshot.json create mode 100644 drizzle-scanned/meta/_journal.json create mode 100644 drizzle-scanned/schema.ts create mode 100644 drizzle.config.ts create mode 100644 drizzle/0000_illegal_living_lightning.sql create mode 100644 drizzle/db.ts create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/_journal.json create mode 100644 drizzle/schema.ts diff --git a/bun.lockb b/bun.lockb index d70cd3853134b18ce7266b5f61b9d1419fc1800f..f8539fc99c7658f82bf20397b1d87dc5cd1b0d45 100755 GIT binary patch delta 147807 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%0Am}Ne59Kjfy#nYx1L)DRXMj3} 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 G?EeG84E?wO delta 122390 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 /** * 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) {