Begin moving project to use Drizzle instead of prisma

This commit is contained in:
Jesse Wierzbinski 2024-04-11 01:39:07 -10:00
parent b107bed935
commit f7abe06a60
No known key found for this signature in database
49 changed files with 7602 additions and 1267 deletions

BIN
bun.lockb

Binary file not shown.

View file

@ -1,10 +1,9 @@
import type { Application } from "@prisma/client"; import type { InferSelectModel } from "drizzle-orm";
import { client } from "~database/datasource"; import { db } from "~drizzle/db";
import type { application } from "~drizzle/schema";
import type { APIApplication } from "~types/entities/application"; import type { APIApplication } from "~types/entities/application";
/** export type Application = InferSelectModel<typeof application>;
* Represents an application that can authenticate with the API.
*/
/** /**
* Retrieves the application associated with the given access token. * Retrieves the application associated with the given access token.
@ -14,16 +13,14 @@ import type { APIApplication } from "~types/entities/application";
export const getFromToken = async ( export const getFromToken = async (
token: string, token: string,
): Promise<Application | null> => { ): Promise<Application | null> => {
const dbToken = await client.token.findFirst({ const result = await db.query.token.findFirst({
where: { where: (tokens, { eq }) => eq(tokens.accessToken, token),
access_token: token, with: {
},
include: {
application: true, application: true,
}, },
}); });
return dbToken?.application || null; return result?.application || null;
}; };
/** /**
@ -34,6 +31,6 @@ export const applicationToAPI = (app: Application): APIApplication => {
return { return {
name: app.name, name: app.name,
website: app.website, website: app.website,
vapid_key: app.vapid_key, vapid_key: app.vapidKey,
}; };
}; };

View file

@ -1,21 +1,24 @@
import type { Attachment } from "@prisma/client";
import type { Config } from "config-manager"; import type { Config } from "config-manager";
import { MediaBackendType } from "media-manager"; import { MediaBackendType } from "media-manager";
import type { APIAsyncAttachment } from "~types/entities/async_attachment"; import type { APIAsyncAttachment } from "~types/entities/async_attachment";
import type { APIAttachment } from "~types/entities/attachment"; import type { APIAttachment } from "~types/entities/attachment";
import type * as Lysand from "lysand-types"; 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<typeof attachment>;
export const attachmentToAPI = ( export const attachmentToAPI = (
attachment: Attachment, attachment: Attachment,
): APIAsyncAttachment | APIAttachment => { ): APIAsyncAttachment | APIAttachment => {
let type = "unknown"; let type = "unknown";
if (attachment.mime_type.startsWith("image/")) { if (attachment.mimeType.startsWith("image/")) {
type = "image"; type = "image";
} else if (attachment.mime_type.startsWith("video/")) { } else if (attachment.mimeType.startsWith("video/")) {
type = "video"; type = "video";
} else if (attachment.mime_type.startsWith("audio/")) { } else if (attachment.mimeType.startsWith("audio/")) {
type = "audio"; type = "audio";
} }
@ -23,8 +26,8 @@ export const attachmentToAPI = (
id: attachment.id, id: attachment.id,
type: type as "image" | "video" | "audio" | "unknown", type: type as "image" | "video" | "audio" | "unknown",
url: attachment.url, url: attachment.url,
remote_url: attachment.remote_url, remote_url: attachment.remoteUrl,
preview_url: attachment.thumbnail_url, preview_url: attachment.thumbnailUrl,
text_url: null, text_url: null,
meta: { meta: {
width: attachment.width || undefined, width: attachment.width || undefined,
@ -63,7 +66,7 @@ export const attachmentToLysand = (
attachment: Attachment, attachment: Attachment,
): Lysand.ContentFormat => { ): Lysand.ContentFormat => {
return { return {
[attachment.mime_type]: { [attachment.mimeType]: {
content: attachment.url, content: attachment.url,
blurhash: attachment.blurhash ?? undefined, blurhash: attachment.blurhash ?? undefined,
description: attachment.description ?? undefined, description: attachment.description ?? undefined,
@ -82,13 +85,15 @@ export const attachmentToLysand = (
}; };
export const attachmentFromLysand = async ( export const attachmentFromLysand = async (
attachment: Lysand.ContentFormat, attachmentToConvert: Lysand.ContentFormat,
): Promise<Attachment> => { ): Promise<InferSelectModel<typeof attachment>> => {
const key = Object.keys(attachment)[0]; const key = Object.keys(attachmentToConvert)[0];
const value = attachment[key]; const value = attachmentToConvert[key];
return await client.attachment.create({ const result = await db
data: { .insert(attachment)
.values({
mimeType: key,
url: value.content, url: value.content,
description: value.description || undefined, description: value.description || undefined,
duration: value.duration || undefined, duration: value.duration || undefined,
@ -97,10 +102,11 @@ export const attachmentFromLysand = async (
size: value.size || undefined, size: value.size || undefined,
width: value.width || undefined, width: value.width || undefined,
sha256: value.hash?.sha256 || undefined, sha256: value.hash?.sha256 || undefined,
mime_type: key,
blurhash: value.blurhash || undefined, blurhash: value.blurhash || undefined,
}, })
}); .returning();
return result[0];
}; };
export const getUrl = (name: string, config: Config) => { export const getUrl = (name: string, config: Config) => {

View file

@ -1,33 +1,36 @@
import type { Emoji } from "@prisma/client";
import { client } from "~database/datasource";
import type { APIEmoji } from "~types/entities/emoji"; import type { APIEmoji } from "~types/entities/emoji";
import type * as Lysand from "lysand-types"; import type * as Lysand from "lysand-types";
import { addInstanceIfNotExists } from "./Instance"; import { addInstanceIfNotExists } from "./Instance";
import { db } from "~drizzle/db";
import { emoji, instance } from "~drizzle/schema";
import { and, eq, type InferSelectModel } from "drizzle-orm";
/** export type EmojiWithInstance = InferSelectModel<typeof emoji> & {
* Represents an emoji entity in the database. instance: InferSelectModel<typeof instance> | null;
*/ };
/** /**
* Used for parsing emojis from local text * Used for parsing emojis from local text
* @param text The text to parse * @param text The text to parse
* @returns An array of emojis * @returns An array of emojis
*/ */
export const parseEmojis = async (text: string): Promise<Emoji[]> => { export const parseEmojis = async (text: string) => {
const regex = /:[a-zA-Z0-9_]+:/g; const regex = /:[a-zA-Z0-9_]+:/g;
const matches = text.match(regex); const matches = text.match(regex);
if (!matches) return []; if (!matches) return [];
return await client.emoji.findMany({ const emojis = await db.query.emoji.findMany({
where: { where: (emoji, { eq, or }) =>
shortcode: { or(
in: matches.map((match) => match.replace(/:/g, "")), ...matches
}, .map((match) => match.replace(/:/g, ""))
instanceId: null, .map((match) => eq(emoji.shortcode, match)),
}, ),
include: { with: {
instance: true, instance: true,
}, },
}); });
return emojis;
}; };
/** /**
@ -36,62 +39,72 @@ export const parseEmojis = async (text: string): Promise<Emoji[]> => {
* @param host Host to fetch the emoji from if remote * @param host Host to fetch the emoji from if remote
* @returns The emoji * @returns The emoji
*/ */
export const fetchEmoji = async (emoji: Lysand.Emoji, host?: string) => { export const fetchEmoji = async (
const existingEmoji = await client.emoji.findFirst({ emojiToFetch: Lysand.Emoji,
where: { host?: string,
shortcode: emoji.name, ): Promise<EmojiWithInstance> => {
instance: host const existingEmoji = await db
? { .select()
base_url: host, .from(emoji)
} .innerJoin(instance, eq(emoji.instanceId, instance.id))
: null, .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({ const result = (
data: { await db
shortcode: emoji.name, .insert(emoji)
url: Object.entries(emoji.url)[0][1].content, .values({
shortcode: emojiToFetch.name,
url: Object.entries(emojiToFetch.url)[0][1].content,
alt: alt:
emoji.alt || emojiToFetch.alt ||
Object.entries(emoji.url)[0][1].description || Object.entries(emojiToFetch.url)[0][1].description ||
undefined, undefined,
content_type: Object.keys(emoji.url)[0], contentType: Object.keys(emojiToFetch.url)[0],
visible_in_picker: true, visibleInPicker: true,
instance: host instanceId: foundInstance?.id,
? { })
connect: { .returning()
id: instance?.id, )[0];
},
} return {
: undefined, ...result,
}, instance: foundInstance,
}); };
}; };
/** /**
* Converts the emoji to an APIEmoji object. * Converts the emoji to an APIEmoji object.
* @returns The APIEmoji object. * @returns The APIEmoji object.
*/ */
export const emojiToAPI = (emoji: Emoji): APIEmoji => { export const emojiToAPI = (emoji: EmojiWithInstance): APIEmoji => {
return { return {
shortcode: emoji.shortcode, shortcode: emoji.shortcode,
static_url: emoji.url, // TODO: Add static version static_url: emoji.url, // TODO: Add static version
url: emoji.url, url: emoji.url,
visible_in_picker: emoji.visible_in_picker, visible_in_picker: emoji.visibleInPicker,
category: undefined, category: undefined,
}; };
}; };
export const emojiToLysand = (emoji: Emoji): Lysand.Emoji => { export const emojiToLysand = (emoji: EmojiWithInstance): Lysand.Emoji => {
return { return {
name: emoji.shortcode, name: emoji.shortcode,
url: { url: {
[emoji.content_type]: { [emoji.contentType]: {
content: emoji.url, content: emoji.url,
description: emoji.alt || undefined, description: emoji.alt || undefined,
}, },

View file

@ -1,14 +1,13 @@
import type { User } from "@prisma/client";
import type * as Lysand from "lysand-types"; import type * as Lysand from "lysand-types";
import { config } from "config-manager"; import { config } from "config-manager";
import { getUserUri } from "./User"; import { getUserUri, type User } from "./User";
export const objectToInboxRequest = async ( export const objectToInboxRequest = async (
object: Lysand.Entity, object: Lysand.Entity,
author: User, author: User,
userToSendTo: User, userToSendTo: User,
): Promise<Request> => { ): Promise<Request> => {
if (!userToSendTo.instanceId || !userToSendTo.endpoints.inbox) { if (!userToSendTo.instanceId || !userToSendTo.endpoints?.inbox) {
throw new Error("UserToSendTo has no inbox or is a local user"); throw new Error("UserToSendTo has no inbox or is a local user");
} }

View file

@ -1,6 +1,6 @@
import type { Instance } from "@prisma/client"; import { db } from "~drizzle/db";
import { client } from "~database/datasource";
import type * as Lysand from "lysand-types"; import type * as Lysand from "lysand-types";
import { instance } from "~drizzle/schema";
/** /**
* Represents an instance in the database. * Represents an instance in the database.
@ -11,16 +11,12 @@ import type * as Lysand from "lysand-types";
* @param url * @param url
* @returns Either the database instance if it already exists, or a newly created instance. * @returns Either the database instance if it already exists, or a newly created instance.
*/ */
export const addInstanceIfNotExists = async ( export const addInstanceIfNotExists = async (url: string) => {
url: string,
): Promise<Instance> => {
const origin = new URL(url).origin; const origin = new URL(url).origin;
const host = new URL(url).host; const host = new URL(url).host;
const found = await client.instance.findFirst({ const found = await db.query.instance.findFirst({
where: { where: (instance, { eq }) => eq(instance.baseUrl, host),
base_url: host,
},
}); });
if (found) return found; if (found) return found;
@ -40,12 +36,15 @@ export const addInstanceIfNotExists = async (
throw new Error("Invalid instance metadata (missing name or version)"); throw new Error("Invalid instance metadata (missing name or version)");
} }
return await client.instance.create({ return (
data: { await db
base_url: host, .insert(instance)
.values({
baseUrl: host,
name: metadata.name, name: metadata.name,
version: metadata.version, version: metadata.version,
logo: metadata.logo, logo: metadata.logo,
}, })
}); .returning()
)[0];
}; };

View file

@ -1,9 +1,12 @@
import type { Like } from "@prisma/client";
import { config } from "config-manager"; import { config } from "config-manager";
import { client } from "~database/datasource";
import type { StatusWithRelations } from "./Status"; import type { StatusWithRelations } from "./Status";
import type { UserWithRelations } from "./User"; import type { UserWithRelations } from "./User";
import type * as Lysand from "lysand-types"; 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<typeof like>;
/** /**
* Represents a Like entity in the database. * Represents a Like entity in the database.
@ -33,22 +36,18 @@ export const createLike = async (
user: UserWithRelations, user: UserWithRelations,
status: StatusWithRelations, status: StatusWithRelations,
) => { ) => {
await client.like.create({ await db.insert(like).values({
data: {
likedId: status.id, likedId: status.id,
likerId: user.id, likerId: user.id,
},
}); });
if (status.author.instanceId === user.instanceId) { if (status.author.instanceId === user.instanceId) {
// Notify the user that their post has been favourited // Notify the user that their post has been favourited
await client.notification.create({ await db.insert(notification).values({
data: {
accountId: user.id, accountId: user.id,
type: "favourite", type: "favourite",
notifiedId: status.authorId, notifiedId: status.authorId,
statusId: status.id, statusId: status.id,
},
}); });
} else { } else {
// TODO: Add database jobs for federating this // TODO: Add database jobs for federating this
@ -64,22 +63,21 @@ export const deleteLike = async (
user: UserWithRelations, user: UserWithRelations,
status: StatusWithRelations, status: StatusWithRelations,
) => { ) => {
await client.like.deleteMany({ await db
where: { .delete(like)
likedId: status.id, .where(and(eq(like.likedId, status.id), eq(like.likerId, user.id)));
likerId: user.id,
},
});
// Notify the user that their post has been favourited // Notify the user that their post has been favourited
await client.notification.deleteMany({ await db
where: { .delete(notification)
accountId: user.id, .where(
type: "favourite", and(
notifiedId: status.authorId, eq(notification.accountId, user.id),
statusId: status.id, eq(notification.type, "favourite"),
}, eq(notification.notifiedId, status.authorId),
}); eq(notification.statusId, status.id),
),
);
if (user.instanceId === null && status.author.instanceId !== null) { if (user.instanceId === null && status.author.instanceId !== null) {
// User is local, federate the delete // User is local, federate the delete

View file

@ -1,7 +1,10 @@
import type { Notification } from "@prisma/client";
import type { APINotification } from "~types/entities/notification"; import type { APINotification } from "~types/entities/notification";
import { type StatusWithRelations, statusToAPI } from "./Status"; import { type StatusWithRelations, statusToAPI } from "./Status";
import { type UserWithRelations, userToAPI } from "./User"; import { type UserWithRelations, userToAPI } from "./User";
import type { InferSelectModel } from "drizzle-orm";
import type { notification } from "~drizzle/schema";
export type Notification = InferSelectModel<typeof notification>;
export type NotificationWithRelations = Notification & { export type NotificationWithRelations = Notification & {
status: StatusWithRelations | null; status: StatusWithRelations | null;

View file

@ -1,15 +1,22 @@
import type { LysandObject } from "@prisma/client"; import type { InferSelectModel } from "drizzle-orm";
import { client } from "~database/datasource"; import { db } from "~drizzle/db";
import type { LysandObjectType } from "~types/lysand/Object"; import { lysandObject } from "~drizzle/schema";
import { findFirstUser } from "./User";
import type * as Lysand from "lysand-types";
export type LysandObject = InferSelectModel<typeof lysandObject>;
/** /**
* Represents a Lysand object in the database. * Represents a Lysand object in the database.
*/ */
export const createFromObject = async (object: LysandObjectType) => { export const createFromObject = async (
const foundObject = await client.lysandObject.findFirst({ object: Lysand.Entity,
where: { remote_id: object.id }, authorUri: string,
include: { ) => {
const foundObject = await db.query.lysandObject.findFirst({
where: (o, { eq }) => eq(o.remoteId, object.id),
with: {
author: true, author: true,
}, },
}); });
@ -18,21 +25,19 @@ export const createFromObject = async (object: LysandObjectType) => {
return foundObject; return foundObject;
} }
const author = await client.lysandObject.findFirst({ const author = await findFirstUser({
// biome-ignore lint/suspicious/noExplicitAny: <explanation> where: (user, { eq }) => eq(user.uri, authorUri),
where: { uri: (object as any).author },
}); });
return await client.lysandObject.create({ return await db.insert(lysandObject).values({
data: {
authorId: author?.id, authorId: author?.id,
created_at: new Date(object.created_at).toISOString(), createdAt: new Date(object.created_at).toISOString(),
extensions: object.extensions || {}, extensions: object.extensions,
remote_id: object.id, remoteId: object.id,
type: object.type, type: object.type,
uri: object.uri, uri: object.uri,
// Rest of data (remove id, author, created_at, extensions, type, uri) // Rest of data (remove id, author, created_at, extensions, type, uri)
extra_data: Object.fromEntries( extraData: Object.fromEntries(
Object.entries(object).filter( Object.entries(object).filter(
([key]) => ([key]) =>
![ ![
@ -45,18 +50,18 @@ export const createFromObject = async (object: LysandObjectType) => {
].includes(key), ].includes(key),
), ),
), ),
},
}); });
}; };
export const toLysand = (lyObject: LysandObject): LysandObjectType => { export const toLysand = (lyObject: LysandObject): Lysand.Entity => {
return { return {
id: lyObject.remote_id || lyObject.id, id: lyObject.remoteId || lyObject.id,
created_at: new Date(lyObject.created_at).toISOString(), created_at: new Date(lyObject.createdAt).toISOString(),
type: lyObject.type, type: lyObject.type,
uri: lyObject.uri, uri: lyObject.uri,
...lyObject.extra_data, ...(lyObject.extraData as object),
extensions: lyObject.extensions, // @ts-expect-error Assume stored JSON is valid
extensions: lyObject.extensions as object,
}; };
}; };

View file

@ -1,4 +1,3 @@
import type { User } from "@prisma/client";
import { config } from "config-manager"; import { config } from "config-manager";
// import { Worker } from "bullmq"; // import { Worker } from "bullmq";
import { type StatusWithRelations, statusToLysand } from "./Status"; 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) => { export const addStatusFederationJob = async (statusId: string) => {
/* await federationQueue.add("federation", { /* await federationQueue.add("federation", {
id: statusId, id: statusId,

View file

@ -1,10 +1,11 @@
import type { Relationship, User } from "@prisma/client";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import type { APIRelationship } from "~types/entities/relationship"; 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";
/** export type Relationship = InferSelectModel<typeof relationship>;
* Stores Mastodon API relationships
*/
/** /**
* Creates a new relationship between two users. * Creates a new relationship between two users.
@ -16,8 +17,10 @@ export const createNewRelationship = async (
owner: User, owner: User,
other: User, other: User,
): Promise<Relationship> => { ): Promise<Relationship> => {
return await client.relationship.create({ return (
data: { await db
.insert(relationship)
.values({
ownerId: owner.id, ownerId: owner.id,
subjectId: other.id, subjectId: other.id,
languages: [], languages: [],
@ -33,8 +36,10 @@ export const createNewRelationship = async (
domainBlocking: false, domainBlocking: false,
endorsed: false, endorsed: false,
note: "", note: "",
}, updatedAt: new Date().toISOString(),
}); })
.returning()
)[0];
}; };
export const checkForBidirectionalRelationships = async ( export const checkForBidirectionalRelationships = async (
@ -42,18 +47,14 @@ export const checkForBidirectionalRelationships = async (
user2: User, user2: User,
createIfNotExists = true, createIfNotExists = true,
): Promise<boolean> => { ): Promise<boolean> => {
const relationship1 = await client.relationship.findFirst({ const relationship1 = await db.query.relationship.findFirst({
where: { where: (rel, { and, eq }) =>
ownerId: user1.id, and(eq(rel.ownerId, user1.id), eq(rel.subjectId, user2.id)),
subjectId: user2.id,
},
}); });
const relationship2 = await client.relationship.findFirst({ const relationship2 = await db.query.relationship.findFirst({
where: { where: (rel, { and, eq }) =>
ownerId: user2.id, and(eq(rel.ownerId, user2.id), eq(rel.subjectId, user1.id)),
subjectId: user1.id,
},
}); });
if (!relationship1 && !relationship2 && createIfNotExists) { if (!relationship1 && !relationship2 && createIfNotExists) {
@ -82,7 +83,7 @@ export const relationshipToAPI = (rel: Relationship): APIRelationship => {
notifying: rel.notifying, notifying: rel.notifying,
requested: rel.requested, requested: rel.requested,
showing_reblogs: rel.showingReblogs, showing_reblogs: rel.showingReblogs,
languages: rel.languages, languages: rel.languages ?? [],
note: rel.note, note: rel.note,
}; };
}; };

File diff suppressed because it is too large Load diff

View file

@ -1,35 +1,128 @@
import { addUserToMeilisearch } from "@meilisearch"; import { addUserToMeilisearch } from "@meilisearch";
import type { User } from "@prisma/client";
import { Prisma } from "@prisma/client";
import { type Config, config } from "config-manager"; import { type Config, config } from "config-manager";
import { htmlToText } from "html-to-text"; import { htmlToText } from "html-to-text";
import { client } from "~database/datasource";
import type { APIAccount } from "~types/entities/account"; import type { APIAccount } from "~types/entities/account";
import type { APISource } from "~types/entities/source"; import type { APISource } from "~types/entities/source";
import type * as Lysand from "lysand-types"; 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 { addInstanceIfNotExists } from "./Instance";
import { userRelations } from "./relations";
import { createNewRelationship } from "./Relationship"; import { createNewRelationship } from "./Relationship";
import { getBestContentType, urlToContentFormat } from "@content_types"; import { getBestContentType, urlToContentFormat } from "@content_types";
import { objectToInboxRequest } from "./Federation"; 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<typeof user> & {
endpoints?: Partial<{
dislikes: string;
featured: string;
likes: string;
followers: string;
following: string;
inbox: string;
outbox: string;
}>;
};
export type UserWithRelations = User & {
instance: InferSelectModel<typeof instance> | null;
emojis: EmojiWithInstance[];
followerCount: number;
followingCount: number;
statusCount: number;
};
export type UserWithRelationsAndRelationships = UserWithRelations & {
relationships: InferSelectModel<typeof relationship>[];
relationshipSubjects: InferSelectModel<typeof relationship>[];
};
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 { export interface AuthData {
user: UserWithRelations | null; user: UserWithRelations | null;
token: string; token: string;
} }
/**
* Represents a user in the database.
* Stores local and remote users
*/
const userRelations2 = Prisma.validator<Prisma.UserDefaultArgs>()({
include: userRelations,
});
export type UserWithRelations = Prisma.UserGetPayload<typeof userRelations2>;
/** /**
* Get the user's avatar in raw URL format * Get the user's avatar in raw URL format
* @param config The config to use * @param config The config to use
@ -68,19 +161,22 @@ export const followRequestUser = async (
reblogs = false, reblogs = false,
notify = false, notify = false,
languages: string[] = [], languages: string[] = [],
) => { ): Promise<InferSelectModel<typeof relationship>> => {
const isRemote = follower.instanceId !== followee.instanceId; const isRemote = follower.instanceId !== followee.instanceId;
const relationship = await client.relationship.update({ const updatedRelationship = (
where: { id: relationshipId }, await db
data: { .update(relationship)
.set({
following: isRemote ? false : !followee.isLocked, following: isRemote ? false : !followee.isLocked,
requested: isRemote ? true : followee.isLocked, requested: isRemote ? true : followee.isLocked,
showingReblogs: reblogs, showingReblogs: reblogs,
notifying: notify, notifying: notify,
languages: languages, languages: languages,
}, })
}); .where(eq(relationship.id, relationshipId))
.returning()
)[0];
if (isRemote) { if (isRemote) {
// Federate // Federate
@ -100,35 +196,26 @@ export const followRequestUser = async (
`Failed to federate follow request from ${follower.id} to ${followee.uri}`, `Failed to federate follow request from ${follower.id} to ${followee.uri}`,
); );
return await client.relationship.update({ return (
where: { id: relationshipId }, await db
data: { .update(relationship)
.set({
following: false, following: false,
requested: false, requested: false,
}, })
}); .where(eq(relationship.id, relationshipId))
.returning()
)[0];
} }
} else { } else {
if (followee.isLocked) { await db.insert(notification).values({
await client.notification.create({ accountId: followee.id,
data: { type: followee.isLocked ? "follow_request" : "follow",
accountId: follower.id, notifiedId: follower.id,
type: "follow_request",
notifiedId: followee.id,
},
}); });
} else {
await client.notification.create({
data: {
accountId: follower.id,
type: "follow",
notifiedId: followee.id,
},
});
}
} }
return relationship; return updatedRelationship;
}; };
export const sendFollowAccept = async (follower: User, followee: User) => { 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) => { export const transformOutputToUserWithRelations = (
// Check if user not already in database user: Omit<User, "endpoints"> & {
const foundUser = await client.user.findUnique({ followerCount: unknown;
where: { followingCount: unknown;
uri, statusCount: unknown;
emojis: {
a: string;
b: string;
emoji?: EmojiWithInstance;
}[];
instance: InferSelectModel<typeof instance> | null;
endpoints: unknown;
}, },
include: userRelations, ): 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<string, object>)
.emoji as EmojiWithInstance,
),
};
};
export const findManyUsers = async (
query: Parameters<typeof db.query.user.findMany>[0],
): Promise<UserWithRelations[]> => {
const output = await db.query.user.findMany({
...query,
with: {
...userRelations,
...query?.with,
},
extras: {
...userExtras,
...query?.extras,
},
});
return output.map((user) => transformOutputToUserWithRelations(user));
};
export const findFirstUser = async (
query: Parameters<typeof db.query.user.findFirst>[0],
): Promise<UserWithRelations | null> => {
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<UserWithRelations | null> => {
// Check if user not already in database
const foundUser = await findFirstUser({
where: (user, { eq }) => eq(user.uri, uri),
}); });
if (foundUser) return foundUser; if (foundUser) return foundUser;
@ -192,12 +354,12 @@ export const resolveUser = async (uri: string) => {
); );
} }
return client.user.findUnique({ const foundLocalUser = await findFirstUser({
where: { where: (user, { eq }) => eq(user.id, uuid[0]),
id: uuid[0], with: userRelations,
},
include: userRelations,
}); });
return foundLocalUser || null;
} }
if (!URL.canParse(uri)) { if (!URL.canParse(uri)) {
@ -244,11 +406,13 @@ export const resolveUser = async (uri: string) => {
emojis.push(await fetchEmoji(emoji)); emojis.push(await fetchEmoji(emoji));
} }
const user = await client.user.create({ const newUser = (
data: { await db
.insert(user)
.values({
username: data.username, username: data.username,
uri: data.uri, uri: data.uri,
createdAt: new Date(data.created_at), createdAt: new Date(data.created_at).toISOString(),
endpoints: { endpoints: {
dislikes: data.dislikes, dislikes: data.dislikes,
featured: data.featured, featured: data.featured,
@ -258,11 +422,6 @@ export const resolveUser = async (uri: string) => {
inbox: data.inbox, inbox: data.inbox,
outbox: data.outbox, outbox: data.outbox,
}, },
emojis: {
connect: emojis.map((emoji) => ({
id: emoji.id,
})),
},
instanceId: instance.id, instanceId: instance.id,
avatar: data.avatar avatar: data.avatar
? Object.entries(data.avatar)[0][1].content ? Object.entries(data.avatar)[0][1].content
@ -280,14 +439,29 @@ export const resolveUser = async (uri: string) => {
sensitive: false, sensitive: false,
fields: [], fields: [],
}, },
}, })
include: userRelations, .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 if (!finalUser) return null;
await addUserToMeilisearch(user);
return user; // Add to Meilisearch
await addUserToMeilisearch(finalUser);
return finalUser;
}; };
export const getUserUri = (user: User) => { export const getUserUri = (user: User) => {
@ -301,19 +475,25 @@ export const getUserUri = (user: User) => {
* Resolves a WebFinger identifier to a user. * Resolves a WebFinger identifier to a user.
* @param identifier Either a UUID or a username * @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<UserWithRelations | null> => {
// Check if user not already in database // Check if user not already in database
const foundUser = await client.user.findUnique({ const foundUser = await db
where: { .select()
username: identifier, .from(user)
instance: { .innerJoin(instance, eq(user.instanceId, instance.id))
base_url: host, .where(and(eq(user.username, identifier), eq(instance.baseUrl, host)))
}, .limit(1);
},
include: userRelations,
});
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}`; const hostWithProtocol = host.startsWith("http") ? host : `https://${host}`;
@ -383,11 +563,13 @@ export const createNewLocalUser = async (data: {
avatar?: string; avatar?: string;
header?: string; header?: string;
admin?: boolean; admin?: boolean;
}) => { }): Promise<UserWithRelations | null> => {
const keys = await generateUserKeys(); const keys = await generateUserKeys();
const user = await client.user.create({ const newUser = (
data: { await db
.insert(user)
.values({
username: data.username, username: data.username,
displayName: data.display_name ?? data.username, displayName: data.display_name ?? data.username,
password: await Bun.password.hash(data.password), password: await Bun.password.hash(data.password),
@ -398,6 +580,7 @@ export const createNewLocalUser = async (data: {
isAdmin: data.admin ?? false, isAdmin: data.admin ?? false,
publicKey: keys.public_key, publicKey: keys.public_key,
privateKey: keys.private_key, privateKey: keys.private_key,
updatedAt: new Date().toISOString(),
source: { source: {
language: null, language: null,
note: "", note: "",
@ -405,27 +588,32 @@ export const createNewLocalUser = async (data: {
sensitive: false, sensitive: false,
fields: [], fields: [],
}, },
}, })
include: userRelations, .returning()
)[0];
const finalUser = await findFirstUser({
where: (user, { eq }) => eq(user.id, newUser.id),
with: userRelations,
}); });
// Add to Meilisearch if (!finalUser) return null;
await addUserToMeilisearch(user);
return user; // Add to Meilisearch
await addUserToMeilisearch(finalUser);
return finalUser;
}; };
/** /**
* Parses mentions from a list of URIs * Parses mentions from a list of URIs
*/ */
export const parseMentionsUris = async (mentions: string[]) => { export const parseMentionsUris = async (
return await client.user.findMany({ mentions: string[],
where: { ): Promise<UserWithRelations[]> => {
uri: { return await findManyUsers({
in: mentions, where: (user, { inArray }) => inArray(user.uri, mentions),
}, with: userRelations,
},
include: userRelations,
}); });
}; };
@ -434,23 +622,22 @@ export const parseMentionsUris = async (mentions: string[]) => {
* @param access_token The access token to retrieve the user from. * @param access_token The access token to retrieve the user from.
* @returns The user associated with the given access token. * @returns The user associated with the given access token.
*/ */
export const retrieveUserFromToken = async (access_token: string) => { export const retrieveUserFromToken = async (
access_token: string,
): Promise<UserWithRelations | null> => {
if (!access_token) return null; if (!access_token) return null;
const token = await client.token.findFirst({ const token = await db.query.token.findFirst({
where: { where: (tokens, { eq }) => eq(tokens.accessToken, access_token),
access_token,
},
include: {
user: {
include: userRelations,
},
},
}); });
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 ( export const getRelationshipToOtherUser = async (
user: UserWithRelations, user: UserWithRelations,
other: User, other: User,
) => { ): Promise<InferSelectModel<typeof relationship>> => {
const relationship = await client.relationship.findFirst({ const foundRelationship = await db.query.relationship.findFirst({
where: { where: (relationship, { and, eq }) =>
ownerId: user.id, and(
subjectId: other.id, eq(relationship.ownerId, user.id),
}, eq(relationship.subjectId, other.id),
),
}); });
if (!relationship) { if (!foundRelationship) {
// Create new relationship // Create new relationship
const newRelationship = await createNewRelationship(user, other); const newRelationship = await createNewRelationship(user, other);
await client.user.update({
where: { id: user.id },
data: {
relationships: {
connect: {
id: newRelationship.id,
},
},
},
});
return newRelationship; return newRelationship;
} }
return relationship; return foundRelationship;
}; };
/** /**
@ -526,40 +703,42 @@ export const generateUserKeys = async () => {
}; };
export const userToAPI = ( export const userToAPI = (
user: UserWithRelations, userToConvert: UserWithRelations,
isOwnAccount = false, isOwnAccount = false,
): APIAccount => { ): APIAccount => {
return { return {
id: user.id, id: userToConvert.id,
username: user.username, username: userToConvert.username,
display_name: user.displayName, display_name: userToConvert.displayName,
note: user.note, note: userToConvert.note,
url: url:
user.uri || userToConvert.uri ||
new URL(`/@${user.username}`, config.http.base_url).toString(), new URL(
avatar: getAvatarUrl(user, config), `/@${userToConvert.username}`,
header: getHeaderUrl(user, config), config.http.base_url,
locked: user.isLocked, ).toString(),
created_at: new Date(user.createdAt).toISOString(), avatar: getAvatarUrl(userToConvert, config),
followers_count: user.relationshipSubjects.filter((r) => r.following) header: getHeaderUrl(userToConvert, config),
.length, locked: userToConvert.isLocked,
following_count: user.relationships.filter((r) => r.following).length, created_at: new Date(userToConvert.createdAt).toISOString(),
statuses_count: user._count.statuses, followers_count: userToConvert.followerCount,
emojis: user.emojis.map((emoji) => emojiToAPI(emoji)), following_count: userToConvert.followingCount,
statuses_count: userToConvert.statusCount,
emojis: userToConvert.emojis.map((emoji) => emojiToAPI(emoji)),
// TODO: Add fields // TODO: Add fields
fields: [], fields: [],
bot: user.isBot, bot: userToConvert.isBot,
source: source:
isOwnAccount && user.source isOwnAccount && userToConvert.source
? (user.source as APISource) ? (userToConvert.source as APISource)
: undefined, : undefined,
// TODO: Add static avatar and header // TODO: Add static avatar and header
avatar_static: "", avatar_static: "",
header_static: "", header_static: "",
acct: acct:
user.instance === null userToConvert.instance === null
? user.username ? userToConvert.username
: `${user.username}@${user.instance.base_url}`, : `${userToConvert.username}@${userToConvert.instance.baseUrl}`,
// TODO: Add these fields // TODO: Add these fields
limited: false, limited: false,
moved: null, moved: null,
@ -569,8 +748,8 @@ export const userToAPI = (
mute_expires_at: undefined, mute_expires_at: undefined,
group: false, group: false,
pleroma: { pleroma: {
is_admin: user.isAdmin, is_admin: userToConvert.isAdmin,
is_moderator: user.isAdmin, is_moderator: userToConvert.isAdmin,
}, },
}; };
}; };

View file

@ -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 $$;
*/

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,13 @@
{
"version": "5",
"dialect": "pg",
"entries": [
{
"idx": 0,
"version": "5",
"when": 1712812153499,
"tag": "0000_third_misty_knight",
"breakpoints": true
}
]
}

292
drizzle-scanned/schema.ts Normal file
View file

@ -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(),
});

19
drizzle.config.ts Normal file
View file

@ -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;

View file

@ -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 $$;
*/

14
drizzle/db.ts Normal file
View file

@ -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 });

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,13 @@
{
"version": "5",
"dialect": "pg",
"entries": [
{
"idx": 0,
"version": "5",
"when": 1712805159664,
"tag": "0000_illegal_living_lightning",
"breakpoints": true
}
]
}

677
drizzle/schema.ts Normal file
View file

@ -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],
}),
}));

View file

@ -2,13 +2,15 @@ import { exists, mkdir, writeFile } from "node:fs/promises";
import { dirname } from "node:path"; import { dirname } from "node:path";
import { connectMeili } from "@meilisearch"; import { connectMeili } from "@meilisearch";
import { moduleIsEntry } from "@module"; import { moduleIsEntry } from "@module";
import type { PrismaClientInitializationError } from "@prisma/client/runtime/library";
import { initializeRedisCache } from "@redis"; import { initializeRedisCache } from "@redis";
import { config } from "config-manager"; import { config } from "config-manager";
import { LogLevel, LogManager, MultiLogManager } from "log-manager"; import { LogLevel, LogManager, MultiLogManager } from "log-manager";
import { client } from "~database/datasource";
import { createServer } from "~server"; 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(); const timeAtStart = performance.now();
// Create requests file if it doesnt exist // Create requests file if it doesnt exist
@ -43,16 +45,18 @@ if (config.meilisearch.enabled) {
await connectMeili(dualLogger); await connectMeili(dualLogger);
} }
if (redisCache) {
client.$use(redisCache);
}
// Check if database is reachable // Check if database is reachable
let postCount = 0; let postCount = 0;
try { try {
postCount = await client.status.count(); postCount = (
await db
.select({
count: count(),
})
.from(status)
)[0].count;
} catch (e) { } catch (e) {
const error = e as PrismaClientInitializationError; const error = e as Error;
await logger.logError(LogLevel.CRITICAL, "Database", error); await logger.logError(LogLevel.CRITICAL, "Database", error);
await consoleLogger.logError(LogLevel.CRITICAL, "Database", error); await consoleLogger.logError(LogLevel.CRITICAL, "Database", error);
process.exit(1); process.exit(1);

View file

@ -53,6 +53,7 @@
"@fortawesome/free-solid-svg-icons", "@fortawesome/free-solid-svg-icons",
"@prisma/client", "@prisma/client",
"@prisma/engines", "@prisma/engines",
"es5-ext",
"esbuild", "esbuild",
"json-editor-vue", "json-editor-vue",
"msgpackr-extract", "msgpackr-extract",
@ -72,6 +73,7 @@
"@types/ioredis": "^5.0.0", "@types/ioredis": "^5.0.0",
"@types/jsonld": "^1.5.13", "@types/jsonld": "^1.5.13",
"@types/mime-types": "^2.1.4", "@types/mime-types": "^2.1.4",
"@types/pg": "^8.11.5",
"@typescript-eslint/eslint-plugin": "latest", "@typescript-eslint/eslint-plugin": "latest",
"@unocss/cli": "latest", "@unocss/cli": "latest",
"@unocss/transformer-directives": "^0.59.0", "@unocss/transformer-directives": "^0.59.0",
@ -79,6 +81,7 @@
"@vueuse/head": "^2.0.0", "@vueuse/head": "^2.0.0",
"activitypub-types": "^1.0.3", "activitypub-types": "^1.0.3",
"bun-types": "latest", "bun-types": "latest",
"drizzle-kit": "^0.20.14",
"shiki": "^1.2.4", "shiki": "^1.2.4",
"typescript": "latest", "typescript": "latest",
"unocss": "latest", "unocss": "latest",
@ -103,6 +106,7 @@
"cli-parser": "workspace:*", "cli-parser": "workspace:*",
"cli-table": "^0.3.11", "cli-table": "^0.3.11",
"config-manager": "workspace:*", "config-manager": "workspace:*",
"drizzle-orm": "^0.30.7",
"eventemitter3": "^5.0.1", "eventemitter3": "^5.0.1",
"extract-zip": "^2.0.1", "extract-zip": "^2.0.1",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
@ -123,6 +127,7 @@
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"next-route-matcher": "^1.0.1", "next-route-matcher": "^1.0.1",
"oauth4webapi": "^2.4.0", "oauth4webapi": "^2.4.0",
"pg": "^8.11.5",
"prisma": "^5.6.0", "prisma": "^5.6.0",
"prisma-json-types-generator": "^3.0.4", "prisma-json-types-generator": "^3.0.4",
"prisma-redis-middleware": "^4.8.0", "prisma-redis-middleware": "^4.8.0",

View file

@ -1,11 +1,14 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines"; import { fetchTimeline } from "@timelines";
import { sql } from "drizzle-orm";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { import {
findManyStatuses,
statusToAPI, statusToAPI,
type StatusWithRelations, type StatusWithRelations,
} from "~database/entities/Status"; } from "~database/entities/Status";
import { findFirstUser } from "~database/entities/User";
import { import {
statusAndUserRelations, statusAndUserRelations,
userRelations, userRelations,
@ -52,38 +55,30 @@ export default apiRoute<{
pinned, pinned,
} = extraData.parsedRequest; } = extraData.parsedRequest;
const user = await client.user.findUnique({ const user = await findFirstUser({
where: { id }, where: (user, { eq }) => eq(user.id, id),
include: userRelations,
}); });
if (!user) return errorResponse("User not found", 404); if (!user) return errorResponse("User not found", 404);
if (pinned) { if (pinned) {
const { objects, link } = await fetchTimeline<StatusWithRelations>( const { objects, link } = await fetchTimeline<StatusWithRelations>(
client.status, findManyStatuses,
{ {
where: { // @ts-ignore
authorId: id, where: (status, { and, lt, gt, gte, eq, sql }) =>
reblogId: null, and(
pinnedBy: { max_id ? lt(status.id, max_id) : undefined,
some: { since_id ? gte(status.id, since_id) : undefined,
id: user.id, 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})`,
// If only_media is true, only return statuses with attachments only_media
attachments: only_media ? { some: {} } : undefined, ? sql`EXISTS (SELECT 1 FROM "Attachment" WHERE "Attachment"."statusId" = ${status.id})`
id: { : undefined,
lt: max_id, ),
gt: min_id, // @ts-expect-error Yes I KNOW the types are wrong
gte: since_id, orderBy: (status, { desc }) => desc(status.id),
},
},
include: statusAndUserRelations,
take: Number(limit),
orderBy: {
id: "desc",
},
}, },
req, req,
); );
@ -100,22 +95,22 @@ export default apiRoute<{
} }
const { objects, link } = await fetchTimeline<StatusWithRelations>( const { objects, link } = await fetchTimeline<StatusWithRelations>(
client.status, findManyStatuses,
{ {
where: { // @ts-ignore
authorId: id, where: (status, { and, lt, gt, gte, eq, sql }) =>
reblogId: exclude_reblogs ? null : undefined, and(
id: { max_id ? lt(status.id, max_id) : undefined,
lt: max_id, since_id ? gte(status.id, since_id) : undefined,
gt: min_id, min_id ? gt(status.id, min_id) : undefined,
gte: since_id, eq(status.authorId, id),
}, only_media
}, ? sql`EXISTS (SELECT 1 FROM "Attachment" WHERE "Attachment"."statusId" = ${status.id})`
include: statusAndUserRelations, : undefined,
take: Number(limit), exclude_reblogs ? eq(status.reblogId, null) : undefined,
orderBy: { ),
id: "desc", // @ts-expect-error Yes I KNOW the types are wrong
}, orderBy: (status, { desc }) => desc(status.id),
}, },
req, req,
); );

View file

@ -27,8 +27,8 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
return jsonResponse({ return jsonResponse({
name: application.name, name: application.name,
website: application.website, website: application.website,
vapid_key: application.vapid_key, vapid_key: application.vapidKey,
redirect_uris: application.redirect_uris, redirect_uris: application.redirectUris,
scopes: application.scopes, scopes: application.scopes,
}); });
}); });

View file

@ -1,9 +1,11 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines"; import { fetchTimeline } from "@timelines";
import { client } from "~database/datasource"; import {
import { userToAPI, type UserWithRelations } from "~database/entities/User"; findManyUsers,
import { userRelations } from "~database/entities/relations"; userToAPI,
type UserWithRelations,
} from "~database/entities/User";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -28,25 +30,22 @@ export default apiRoute<{
if (!user) return errorResponse("Unauthorized", 401); 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<UserWithRelations>( const { objects: blocks, link } = await fetchTimeline<UserWithRelations>(
client.user, findManyUsers,
{ {
where: { // @ts-expect-error Yes I KNOW the types are wrong
relationshipSubjects: { where: (subject, { lt, gte, gt, and, sql }) =>
some: { and(
ownerId: user.id, max_id ? lt(subject.id, max_id) : undefined,
blocking: true, 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)`,
id: { ),
lt: max_id, limit: Number(limit),
gte: since_id, // @ts-expect-error Yes I KNOW the types are wrong
}, orderBy: (subject, { desc }) => desc(subject.id),
},
include: userRelations,
take: Number(limit),
}, },
req, req,
); );

View file

@ -2,6 +2,7 @@ import { apiRoute, applyConfig } from "@api";
import { jsonResponse } from "@response"; import { jsonResponse } from "@response";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { emojiToAPI } from "~database/entities/Emoji"; import { emojiToAPI } from "~database/entities/Emoji";
import { db } from "~drizzle/db";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -16,9 +17,10 @@ export const meta = applyConfig({
}); });
export default apiRoute(async () => { export default apiRoute(async () => {
const emojis = await client.emoji.findMany({ const emojis = await db.query.emoji.findMany({
where: { where: (emoji, { isNull }) => isNull(emoji.instanceId),
instanceId: null, with: {
instance: true,
}, },
}); });

View file

@ -1,8 +1,11 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { jsonResponse } from "@response"; import { jsonResponse } from "@response";
import { count, isNull } from "drizzle-orm";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { userToAPI } from "~database/entities/User"; import { userToAPI } from "~database/entities/User";
import { userRelations } from "~database/entities/relations"; import { userRelations } from "~database/entities/relations";
import { db } from "~drizzle/db";
import { status, user } from "~drizzle/schema";
import manifest from "~package.json"; import manifest from "~package.json";
import type { APIInstance } from "~types/entities/instance"; import type { APIInstance } from "~types/entities/instance";
@ -24,16 +27,23 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
// Get software version from package.json // Get software version from package.json
const version = manifest.version; const version = manifest.version;
const statusCount = await client.status.count({ const statusCount = (
where: { await db
instanceId: null, .select({
}, count: count(),
}); })
const userCount = await client.user.count({ .from(status)
where: { .where(isNull(status.instanceId))
instanceId: null, )[0].count;
},
}); const userCount = (
await db
.select({
count: count(),
})
.from(user)
.where(isNull(user.instanceId))
)[0].count;
// Get the first created admin user // Get the first created admin user
const contactAccount = await client.user.findFirst({ const contactAccount = await client.user.findFirst({
@ -47,10 +57,6 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
include: userRelations, include: userRelations,
}); });
if (!contactAccount) {
throw new Error("No admin user found");
}
// Get user that have posted once in the last 30 days // Get user that have posted once in the last 30 days
const monthlyActiveUsers = await client.user.count({ const monthlyActiveUsers = await client.user.count({
where: { where: {
@ -189,7 +195,8 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
}, },
vapid_public_key: "", vapid_public_key: "",
}, },
contact_account: userToAPI(contactAccount), // @ts-expect-error Sometimes there just isnt an admin
contact_account: contactAccount ? userToAPI(contactAccount) : undefined,
} satisfies APIInstance & { } satisfies APIInstance & {
pleroma: object; pleroma: object;
}); });

View file

@ -2,7 +2,11 @@ import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines"; import { fetchTimeline } from "@timelines";
import { client } from "~database/datasource"; 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"; import { userRelations } from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
@ -21,30 +25,28 @@ export const meta = applyConfig({
export default apiRoute<{ export default apiRoute<{
max_id?: string; max_id?: string;
since_id?: string; since_id?: string;
min_id?: string;
limit?: number; limit?: number;
}>(async (req, matchedRoute, extraData) => { }>(async (req, matchedRoute, extraData) => {
const { user } = extraData.auth; 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); if (!user) return errorResponse("Unauthorized", 401);
const { objects: blocks, link } = await fetchTimeline<UserWithRelations>( const { objects: blocks, link } = await fetchTimeline<UserWithRelations>(
client.user, findManyUsers,
{ {
where: { // @ts-expect-error Yes I KNOW the types are wrong
relationshipSubjects: { where: (subject, { lt, gte, gt, and, sql }) =>
some: { and(
ownerId: user.id, max_id ? lt(subject.id, max_id) : undefined,
muting: true, 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)`,
id: { ),
lt: max_id, limit: Number(limit),
gte: since_id, // @ts-expect-error Yes I KNOW the types are wrong
}, orderBy: (subject, { desc }) => desc(subject.id),
},
include: userRelations,
take: Number(limit),
}, },
req, req,
); );

View file

@ -1,12 +1,13 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import type { Relationship } from "~database/entities/Relationship";
import { import {
findFirstStatuses,
getAncestors, getAncestors,
getDescendants, getDescendants,
statusToAPI, statusToAPI,
} from "~database/entities/Status"; } from "~database/entities/Status";
import { statusAndUserRelations } from "~database/entities/relations"; import { db } from "~drizzle/db";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -30,16 +31,47 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
const { user } = extraData.auth; const { user } = extraData.auth;
const foundStatus = await client.status.findUnique({ const foundStatus = await findFirstStatuses({
where: { id }, where: (status, { eq }) => eq(status.id, id),
include: statusAndUserRelations,
}); });
if (!foundStatus) return errorResponse("Record not found", 404); 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 // Get all ancestors
const ancestors = await getAncestors(foundStatus, user); const ancestors = await getAncestors(
const descendants = await getDescendants(foundStatus, user); 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({ return jsonResponse({
ancestors: await Promise.all( ancestors: await Promise.all(

View file

@ -2,7 +2,11 @@ import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { createLike } from "~database/entities/Like"; 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 { statusAndUserRelations } from "~database/entities/relations";
import type { APIStatus } from "~types/entities/status"; import type { APIStatus } from "~types/entities/status";
@ -28,9 +32,8 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);
const status = await client.status.findUnique({ const status = await findFirstStatuses({
where: { id }, where: (status, { eq }) => eq(status.id, id),
include: statusAndUserRelations,
}); });
// Check if user is authorized to view this status (if it's private) // 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({ return jsonResponse({
...(await statusToAPI(status, user)), ...(await statusToAPI(status, user)),
favourited: true, favourited: true,
favourites_count: status._count.likes + 1, favourites_count: status.likeCount + 1,
} as APIStatus); } as APIStatus);
}); });

View file

@ -1,13 +1,12 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines"; import { fetchTimeline } from "@timelines";
import { client } from "~database/datasource"; import { findFirstStatuses, isViewableByUser } from "~database/entities/Status";
import { isViewableByUser } from "~database/entities/Status";
import { userToAPI, type UserWithRelations } from "~database/entities/User";
import { import {
statusAndUserRelations, findManyUsers,
userRelations, userToAPI,
} from "~database/entities/relations"; type UserWithRelations,
} from "~database/entities/User";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -34,9 +33,8 @@ export default apiRoute<{
const { user } = extraData.auth; const { user } = extraData.auth;
const status = await client.status.findUnique({ const status = await findFirstStatuses({
where: { id }, where: (status, { eq }) => eq(status.id, id),
include: statusAndUserRelations,
}); });
// Check if user is authorized to view this status (if it's private) // 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); if (limit < 1) return errorResponse("Invalid limit", 400);
const { objects, link } = await fetchTimeline<UserWithRelations>( const { objects, link } = await fetchTimeline<UserWithRelations>(
client.user, findManyUsers,
{ {
where: { // @ts-ignore
likes: { where: (liker, { and, lt, gt, gte, eq, sql }) =>
some: { and(
likedId: status.id, max_id ? lt(liker.id, max_id) : undefined,
}, since_id ? gte(liker.id, since_id) : undefined,
}, min_id ? gt(liker.id, min_id) : undefined,
id: { sql`EXISTS (SELECT 1 FROM "Like" WHERE "Like"."likedId" = ${status.id} AND "Like"."likerId" = ${liker.id})`,
lt: max_id, ),
gte: since_id, // @ts-expect-error Yes I KNOW the types are wrong
gt: min_id, orderBy: (liker, { desc }) => desc(liker.id),
},
},
include: {
...userRelations,
likes: {
where: {
likedId: status.id,
},
},
},
take: Number(limit),
orderBy: {
id: "desc",
},
}, },
req, req,
); );

View file

@ -1,14 +1,17 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { sanitizeHtml } from "@sanitization"; import { sanitizeHtml } from "@sanitization";
import { eq } from "drizzle-orm";
import { parse } from "marked"; import { parse } from "marked";
import { client } from "~database/datasource";
import { import {
editStatus, editStatus,
findFirstStatuses,
isViewableByUser, isViewableByUser,
statusToAPI, statusToAPI,
} from "~database/entities/Status"; } from "~database/entities/Status";
import { statusAndUserRelations } from "~database/entities/relations"; import { statusAndUserRelations } from "~database/entities/relations";
import { db } from "~drizzle/db";
import { status } from "~drizzle/schema";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET", "DELETE", "PUT"], allowedMethods: ["GET", "DELETE", "PUT"],
@ -42,37 +45,32 @@ export default apiRoute<{
const { user } = extraData.auth; const { user } = extraData.auth;
const status = await client.status.findUnique({ const foundStatus = await findFirstStatuses({
where: { id }, where: (status, { eq }) => eq(status.id, id),
include: statusAndUserRelations,
}); });
const config = await extraData.configManager.getConfig(); const config = await extraData.configManager.getConfig();
// Check if user is authorized to view this status (if it's private) // 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); return errorResponse("Record not found", 404);
if (req.method === "GET") { if (req.method === "GET") {
return jsonResponse(await statusToAPI(status)); return jsonResponse(await statusToAPI(foundStatus));
} }
if (req.method === "DELETE") { if (req.method === "DELETE") {
if (status.authorId !== user?.id) { if (foundStatus.authorId !== user?.id) {
return errorResponse("Unauthorized", 401); return errorResponse("Unauthorized", 401);
} }
// TODO: Implement delete and redraft functionality // TODO: Implement delete and redraft functionality
// Get associated Status object
// Delete status and all associated objects // Delete status and all associated objects
await client.status.delete({ await db.delete(status).where(eq(status.id, id));
where: { id },
});
return jsonResponse( return jsonResponse(
{ {
...(await statusToAPI(status, user)), ...(await statusToAPI(foundStatus, user)),
// TODO: Add // TODO: Add
// text: Add source text // text: Add source text
// poll: Add source poll // poll: Add source poll
@ -82,7 +80,7 @@ export default apiRoute<{
); );
} }
if (req.method === "PUT") { if (req.method === "PUT") {
if (status.authorId !== user?.id) { if (foundStatus.authorId !== user?.id) {
return errorResponse("Unauthorized", 401); return errorResponse("Unauthorized", 401);
} }
@ -191,21 +189,19 @@ export default apiRoute<{
} }
// Check if media attachments are all valid // Check if media attachments are all valid
if (media_ids && media_ids.length > 0) {
const foundAttachments = await client.attachment.findMany({ const foundAttachments = await db.query.attachment.findMany({
where: { where: (attachment, { inArray }) =>
id: { inArray(attachment.id, media_ids),
in: media_ids ?? [],
},
},
}); });
if (foundAttachments.length !== (media_ids ?? []).length) { if (foundAttachments.length !== (media_ids ?? []).length) {
return errorResponse("Invalid media IDs", 422); return errorResponse("Invalid media IDs", 422);
} }
}
// Update status // Update status
const newStatus = await editStatus(status, { const newStatus = await editStatus(foundStatus, {
content: sanitizedStatus, content: sanitizedStatus,
content_type, content_type,
media_attachments: media_ids, media_attachments: media_ids,
@ -213,6 +209,10 @@ export default apiRoute<{
sensitive: sensitive ?? false, sensitive: sensitive ?? false,
}); });
if (!newStatus) {
return errorResponse("Failed to update status", 500);
}
return jsonResponse(await statusToAPI(newStatus, user)); return jsonResponse(await statusToAPI(newStatus, user));
} }

View file

@ -1,8 +1,10 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; 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 { statusAndUserRelations } from "~database/entities/relations";
import { db } from "~drizzle/db";
import { statusToUser } from "~drizzle/schema";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -26,34 +28,34 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);
let status = await client.status.findUnique({ const foundStatus = await findFirstStatuses({
where: { id }, where: (status, { eq }) => eq(status.id, id),
include: statusAndUserRelations,
}); });
// Check if status exists // 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 // 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({ // Check if post is already pinned
where: { id: user.id }, if (
data: { await db.query.statusToUser.findFirst({
pinnedNotes: { where: (statusToUser, { and, eq }) =>
connect: { and(
id: status.id, 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({ return jsonResponse(statusToAPI(foundStatus, user));
where: { id },
include: statusAndUserRelations,
});
if (!status) return errorResponse("Record not found", 404);
return jsonResponse(statusToAPI(status, user));
}); });

View file

@ -1,9 +1,13 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import {
import { isViewableByUser, statusToAPI } from "~database/entities/Status"; findFirstStatuses,
import type { UserWithRelations } from "~database/entities/User"; isViewableByUser,
statusToAPI,
} from "~database/entities/Status";
import { statusAndUserRelations } from "~database/entities/relations"; import { statusAndUserRelations } from "~database/entities/relations";
import { db } from "~drizzle/db";
import { notification, status } from "~drizzle/schema";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -31,47 +35,57 @@ export default apiRoute<{
const { visibility = "public" } = extraData.parsedRequest; const { visibility = "public" } = extraData.parsedRequest;
const status = await client.status.findUnique({ const foundStatus = await findFirstStatuses({
where: { id }, where: (status, { eq }) => eq(status.id, id),
include: statusAndUserRelations,
}); });
// Check if user is authorized to view this status (if it's private) // 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); return errorResponse("Record not found", 404);
const existingReblog = await client.status.findFirst({ const existingReblog = await db.query.status.findFirst({
where: { where: (status, { and, eq }) =>
authorId: user.id, and(eq(status.authorId, user.id), eq(status.reblogId, status.id)),
reblogId: status.id,
},
}); });
if (existingReblog) { if (existingReblog) {
return errorResponse("Already reblogged", 422); return errorResponse("Already reblogged", 422);
} }
const newReblog = await client.status.create({ const newReblog = (
data: { await db
.insert(status)
.values({
authorId: user.id, authorId: user.id,
reblogId: status.id, reblogId: foundStatus.id,
visibility, visibility,
sensitive: false, sensitive: false,
}, updatedAt: new Date().toISOString(),
include: statusAndUserRelations, })
.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 // Create notification for reblog if reblogged user is on the same instance
if (status.author.instanceId === user.instanceId) { if (foundStatus.author.instanceId === user.instanceId) {
await client.notification.create({ await db.insert(notification).values({
data: {
accountId: user.id, accountId: user.id,
notifiedId: status.authorId, notifiedId: foundStatus.authorId,
type: "reblog", type: "reblog",
statusId: status.reblogId, statusId: foundStatus.reblogId,
},
}); });
} }
return jsonResponse(await statusToAPI(newReblog, user)); return jsonResponse(await statusToAPI(finalNewReblog, user));
}); });

View file

@ -1,13 +1,12 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines"; import { fetchTimeline } from "@timelines";
import { client } from "~database/datasource"; import { findFirstStatuses, isViewableByUser } from "~database/entities/Status";
import { isViewableByUser } from "~database/entities/Status";
import { type UserWithRelations, userToAPI } from "~database/entities/User";
import { import {
statusAndUserRelations, type UserWithRelations,
userRelations, userToAPI,
} from "~database/entities/relations"; findManyUsers,
} from "~database/entities/User";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -34,9 +33,8 @@ export default apiRoute<{
const { user } = extraData.auth; const { user } = extraData.auth;
const status = await client.status.findUnique({ const status = await findFirstStatuses({
where: { id }, where: (status, { eq }) => eq(status.id, id),
include: statusAndUserRelations,
}); });
// Check if user is authorized to view this status (if it's private) // 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); if (limit < 1) return errorResponse("Invalid limit", 400);
const { objects, link } = await fetchTimeline<UserWithRelations>( const { objects, link } = await fetchTimeline<UserWithRelations>(
client.user, findManyUsers,
{ {
where: { // @ts-ignore
statuses: { where: (reblogger, { and, lt, gt, gte, eq, sql }) =>
some: { and(
reblogId: status.id, max_id ? lt(reblogger.id, max_id) : undefined,
}, since_id ? gte(reblogger.id, since_id) : undefined,
}, min_id ? gt(reblogger.id, min_id) : undefined,
id: { sql`EXISTS (SELECT 1 FROM "Status" WHERE "Status"."reblogId" = ${status.id} AND "Status"."authorId" = ${reblogger.id})`,
lt: max_id ?? undefined, ),
gte: since_id ?? undefined, // @ts-expect-error Yes I KNOW the types are wrong
gt: min_id ?? undefined, orderBy: (liker, { desc }) => desc(liker.id),
},
},
include: {
...userRelations,
statuses: {
where: {
reblogId: status.id,
},
include: statusAndUserRelations,
},
},
take: Number(limit),
orderBy: {
id: "desc",
},
}, },
req, req,
); );

View file

@ -2,7 +2,11 @@ import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { deleteLike } from "~database/entities/Like"; 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 { statusAndUserRelations } from "~database/entities/relations";
import type { APIStatus } from "~types/entities/status"; import type { APIStatus } from "~types/entities/status";
@ -28,20 +32,19 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);
const status = await client.status.findUnique({ const foundStatus = await findFirstStatuses({
where: { id }, where: (status, { eq }) => eq(status.id, id),
include: statusAndUserRelations,
}); });
// Check if user is authorized to view this status (if it's private) // 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); return errorResponse("Record not found", 404);
await deleteLike(user, status); await deleteLike(user, foundStatus);
return jsonResponse({ return jsonResponse({
...(await statusToAPI(status, user)), ...(await statusToAPI(foundStatus, user)),
favourited: false, favourited: false,
favourites_count: status._count.likes - 1, favourites_count: foundStatus.likeCount - 1,
} as APIStatus); } as APIStatus);
}); });

View file

@ -1,8 +1,13 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { eq } from "drizzle-orm";
import { isViewableByUser, statusToAPI } from "~database/entities/Status"; import {
import { statusAndUserRelations } from "~database/entities/relations"; findFirstStatuses,
isViewableByUser,
statusToAPI,
} from "~database/entities/Status";
import { db } from "~drizzle/db";
import { status } from "~drizzle/schema";
import type { APIStatus } from "~types/entities/status"; import type { APIStatus } from "~types/entities/status";
export const meta = applyConfig({ export const meta = applyConfig({
@ -27,33 +32,28 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);
const status = await client.status.findUnique({ const foundStatus = await findFirstStatuses({
where: { id }, where: (status, { eq }) => eq(status.id, id),
include: statusAndUserRelations,
}); });
// Check if user is authorized to view this status (if it's private) // 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); return errorResponse("Record not found", 404);
const existingReblog = await client.status.findFirst({ const existingReblog = await findFirstStatuses({
where: { where: (status, { eq }) =>
authorId: user.id, eq(status.authorId, user.id) && eq(status.reblogId, foundStatus.id),
reblogId: status.id,
},
}); });
if (!existingReblog) { if (!existingReblog) {
return errorResponse("Not already reblogged", 422); return errorResponse("Not already reblogged", 422);
} }
await client.status.delete({ await db.delete(status).where(eq(status.id, existingReblog.id));
where: { id: existingReblog.id },
});
return jsonResponse({ return jsonResponse({
...(await statusToAPI(status, user)), ...(await statusToAPI(foundStatus, user)),
reblogged: false, reblogged: false,
reblogs_count: status._count.reblogs - 1, reblogs_count: foundStatus.reblogCount - 1,
} as APIStatus); } as APIStatus);
}); });

View file

@ -8,6 +8,7 @@ import type { StatusWithRelations } from "~database/entities/Status";
import { import {
createNewStatus, createNewStatus,
federateStatus, federateStatus,
findFirstStatuses,
parseTextMentions, parseTextMentions,
statusToAPI, statusToAPI,
} from "~database/entities/Status"; } from "~database/entities/Status";
@ -46,9 +47,9 @@ export default apiRoute<{
scheduled_at?: string; scheduled_at?: string;
local_only?: boolean; local_only?: boolean;
content_type?: string; content_type?: string;
federate?: boolean;
}>(async (req, matchedRoute, extraData) => { }>(async (req, matchedRoute, extraData) => {
const { user, token } = extraData.auth; const { user, token } = extraData.auth;
const application = await getFromToken(token);
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);
@ -69,6 +70,7 @@ export default apiRoute<{
spoiler_text, spoiler_text,
visibility, visibility,
content_type, content_type,
federate = true,
} = extraData.parsedRequest; } = extraData.parsedRequest;
// Validate status // Validate status
@ -173,9 +175,8 @@ export default apiRoute<{
let quote: StatusWithRelations | null = null; let quote: StatusWithRelations | null = null;
if (in_reply_to_id) { if (in_reply_to_id) {
replyStatus = await client.status.findUnique({ replyStatus = await findFirstStatuses({
where: { id: in_reply_to_id }, where: (status, { eq }) => eq(status.id, in_reply_to_id),
include: statusAndUserRelations,
}); });
if (!replyStatus) { if (!replyStatus) {
@ -184,9 +185,8 @@ export default apiRoute<{
} }
if (quote_id) { if (quote_id) {
quote = await client.status.findUnique({ quote = await findFirstStatuses({
where: { id: quote_id }, where: (status, { eq }) => eq(status.id, quote_id),
include: statusAndUserRelations,
}); });
if (!quote) { if (!quote) {
@ -233,7 +233,13 @@ export default apiRoute<{
quote ?? undefined, quote ?? undefined,
); );
if (!newStatus) {
return errorResponse("Failed to create status", 500);
}
if (federate) {
await federateStatus(newStatus); await federateStatus(newStatus);
}
return jsonResponse(await statusToAPI(newStatus, user)); return jsonResponse(await statusToAPI(newStatus, user));
}); });

View file

@ -5,8 +5,10 @@ import { client } from "~database/datasource";
import { import {
type StatusWithRelations, type StatusWithRelations,
statusToAPI, statusToAPI,
findManyStatuses,
} from "~database/entities/Status"; } from "~database/entities/Status";
import { statusAndUserRelations } from "~database/entities/relations"; import { statusAndUserRelations } from "~database/entities/relations";
import { db } from "~drizzle/db";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -39,48 +41,40 @@ export default apiRoute<{
if (!user) return errorResponse("Unauthorized", 401); 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<StatusWithRelations>( const { objects, link } = await fetchTimeline<StatusWithRelations>(
client.status, findManyStatuses,
{ {
where: { // @ts-expect-error Yes I KNOW the types are wrong
id: { where: (status, { lt, gte, gt, and, or, eq, inArray, sql }) =>
lt: max_id ?? undefined, or(
gte: since_id ?? undefined, and(
gt: min_id ?? undefined, max_id ? lt(status.id, max_id) : undefined,
}, since_id ? gte(status.id, since_id) : undefined,
OR: [ min_id ? gt(status.id, min_id) : undefined,
{ ),
author: { eq(status.authorId, user.id),
OR: [ /* inArray(
{ status.authorId,
relationshipSubjects: { followers.map((f) => f.ownerId),
some: { ), */
ownerId: user.id, // All statuses where the user is mentioned, using table StatusToUser which has a: status.id and b: user.id
following: true, // 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)`,
id: user.id, ),
}, limit: Number(limit),
], // @ts-expect-error Yes I KNOW the types are wrong
}, orderBy: (status, { desc }) => desc(status.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",
},
}, },
req, req,
); );

View file

@ -3,6 +3,7 @@ import { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines"; import { fetchTimeline } from "@timelines";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { import {
findManyStatuses,
statusToAPI, statusToAPI,
type StatusWithRelations, type StatusWithRelations,
} from "~database/entities/Status"; } from "~database/entities/Status";
@ -49,27 +50,23 @@ export default apiRoute<{
} }
const { objects, link } = await fetchTimeline<StatusWithRelations>( const { objects, link } = await fetchTimeline<StatusWithRelations>(
client.status, findManyStatuses,
{ {
where: { // @ts-expect-error Yes I KNOW the types are wrong
id: { where: (status, { lt, gte, gt, and, isNull, isNotNull }) =>
lt: max_id ?? undefined, and(
gte: since_id ?? undefined, max_id ? lt(status.id, max_id) : undefined,
gt: min_id ?? undefined, since_id ? gte(status.id, since_id) : undefined,
}, min_id ? gt(status.id, min_id) : undefined,
instanceId: remote remote
? { ? isNotNull(status.instanceId)
not: null,
}
: local : local
? null ? isNull(status.instanceId)
: undefined, : undefined,
}, ),
include: statusAndUserRelations, limit: Number(limit),
take: Number(limit), // @ts-expect-error Yes I KNOW the types are wrong
orderBy: { orderBy: (status, { desc }) => desc(status.id),
id: "desc",
},
}, },
req, req,
); );

View file

@ -4,8 +4,9 @@ import { encode } from "blurhash";
import type { MediaBackend } from "media-manager"; import type { MediaBackend } from "media-manager";
import { MediaBackendType } from "media-manager"; import { MediaBackendType } from "media-manager";
import sharp from "sharp"; import sharp from "sharp";
import { client } from "~database/datasource";
import { attachmentToAPI, getUrl } from "~database/entities/Attachment"; import { attachmentToAPI, getUrl } from "~database/entities/Attachment";
import { db } from "~drizzle/db";
import { attachment } from "~drizzle/schema";
import { LocalMediaBackend, S3MediaBackend } from "~packages/media-manager"; import { LocalMediaBackend, S3MediaBackend } from "~packages/media-manager";
export const meta = applyConfig({ export const meta = applyConfig({
@ -134,19 +135,22 @@ export default apiRoute<{
thumbnailUrl = getUrl(path, config); thumbnailUrl = getUrl(path, config);
} }
const newAttachment = await client.attachment.create({ const newAttachment = (
data: { await db
.insert(attachment)
.values({
url, url,
thumbnail_url: thumbnailUrl, thumbnailUrl,
sha256: sha256.update(await file.arrayBuffer()).digest("hex"), sha256: sha256.update(await file.arrayBuffer()).digest("hex"),
mime_type: file.type, mimeType: file.type,
description: description ?? "", description: description ?? "",
size: file.size, size: file.size,
blurhash: blurhash ?? undefined, blurhash: blurhash ?? undefined,
width: metadata?.width ?? undefined, width: metadata?.width ?? undefined,
height: metadata?.height ?? undefined, height: metadata?.height ?? undefined,
}, })
}); .returning()
)[0];
// TODO: Add job to process videos and other media // TODO: Add job to process videos and other media

View file

@ -10,22 +10,34 @@ import {
import type { APIEmoji } from "~types/entities/emoji"; import type { APIEmoji } from "~types/entities/emoji";
import type { APIInstance } from "~types/entities/instance"; import type { APIInstance } from "~types/entities/instance";
import { sendTestRequest, wrapRelativeUrl } from "./utils"; 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; const base_url = config.http.base_url;
let token: Token; let token: Token;
let user: UserWithRelations; let dummyUser: UserWithRelations;
describe("API Tests", () => { describe("API Tests", () => {
beforeAll(async () => { beforeAll(async () => {
await db.delete(user).where(inArray(user.username, ["test", "test2"]));
await db
.delete(application)
.where(inArray(application.clientId, ["test"]));
// Initialize test user // Initialize test user
user = await createNewLocalUser({ dummyUser = await createNewLocalUser({
email: "test@test.com", email: "test@test.com",
username: "test", username: "test",
password: "test", password: "test",
display_name: "", display_name: "",
}); });
if (!dummyUser) {
throw new Error("Failed to create test user");
}
token = await client.token.create({ token = await client.token.create({
data: { data: {
access_token: "test", access_token: "test",
@ -45,7 +57,7 @@ describe("API Tests", () => {
token_type: TokenType.BEARER, token_type: TokenType.BEARER,
user: { user: {
connect: { connect: {
id: user.id, id: dummyUser.id,
}, },
}, },
}, },
@ -53,19 +65,10 @@ describe("API Tests", () => {
}); });
afterAll(async () => { afterAll(async () => {
await client.user.deleteMany({ await db.delete(user).where(inArray(user.username, ["test", "test2"]));
where: { await db
username: { .delete(application)
in: ["test", "test2"], .where(inArray(application.clientId, ["test"]));
},
},
});
await client.application.deleteMany({
where: {
client_id: "test",
},
});
}); });
describe("GET /api/v1/instance", () => { describe("GET /api/v1/instance", () => {
@ -89,7 +92,7 @@ describe("API Tests", () => {
const instance = (await response.json()) as APIInstance; 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.title).toBeDefined();
expect(instance.description).toBeDefined(); expect(instance.description).toBeDefined();
expect(instance.email).toBeDefined(); expect(instance.email).toBeDefined();

View file

@ -126,6 +126,7 @@ describe("API Tests", () => {
status: "Hello, world!", status: "Hello, world!",
visibility: "public", visibility: "public",
media_ids: [media1?.id], media_ids: [media1?.id],
federate: false,
}), }),
}, },
), ),
@ -173,6 +174,7 @@ describe("API Tests", () => {
status: "This is a reply!", status: "This is a reply!",
visibility: "public", visibility: "public",
in_reply_to_id: status?.id, in_reply_to_id: status?.id,
federate: false,
}), }),
}, },
), ),

View file

@ -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 * 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 * @returns Response from the server
*/ */
export async function sendTestRequest(req: Request) { 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) { export function wrapRelativeUrl(url: string, base_url: string) {

View file

@ -1,9 +1,10 @@
import type { Status, User } from "@prisma/client";
import chalk from "chalk"; import chalk from "chalk";
import { config } from "config-manager"; import { config } from "config-manager";
import { LogLevel, type LogManager, type MultiLogManager } from "log-manager"; import { LogLevel, type LogManager, type MultiLogManager } from "log-manager";
import { Meilisearch } from "meilisearch"; import { Meilisearch } from "meilisearch";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import type { Status } from "~database/entities/Status";
import type { User } from "~database/entities/User";
export const meilisearch = new Meilisearch({ export const meilisearch = new Meilisearch({
host: `${config.meilisearch.host}:${config.meilisearch.port}`, host: `${config.meilisearch.host}:${config.meilisearch.port}`,

View file

@ -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<T extends User | Status | Notification>( export async function fetchTimeline<T extends User | Status | Notification>(
model: model:
| Prisma.StatusDelegate | typeof findManyStatuses
| Prisma.UserDelegate | typeof findManyUsers
| Prisma.NotificationDelegate, | typeof db.query.notification.findMany,
args: args:
| Prisma.StatusFindManyArgs | Parameters<typeof findManyStatuses>[0]
| Prisma.UserFindManyArgs | Parameters<typeof findManyUsers>[0]
| Prisma.NotificationFindManyArgs, | Parameters<typeof db.query.notification.findMany>[0],
req: Request, req: Request,
) { ) {
// BEFORE: Before in a top-to-bottom order, so the most recent posts // 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 // 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 // @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 // Constuct HTTP Link header (next and prev) only if there are more statuses
const linkHeader = []; const linkHeader = [];
@ -22,15 +25,11 @@ export async function fetchTimeline<T extends User | Status | Notification>(
if (objects.length > 0) { if (objects.length > 0) {
// Check if there are statuses before the first one // 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 // @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, ...args,
where: { // @ts-expect-error this hack breaks typing :(
...args.where, where: (object, { gt }) => gt(object.id, objects[0].id),
id: { limit: 1,
gt: objects[0].id,
},
},
take: 1,
}); });
if (objectsBefore.length > 0) { if (objectsBefore.length > 0) {
@ -41,18 +40,16 @@ export async function fetchTimeline<T extends User | Status | Notification>(
); );
} }
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 // 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 // @ts-expect-error hack again
const objectsAfter = await model.findMany({ const objectsAfter = await model({
...args, ...args,
where: { // @ts-expect-error this hack breaks typing :(
...args.where, where: (object, { lt }) => lt(object.id, objects.at(-1).id),
id: { limit: 1,
lt: objects.at(-1)?.id,
},
},
take: 1,
}); });
if (objectsAfter.length > 0) { if (objectsAfter.length > 0) {