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

View file

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

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

View file

@ -1,14 +1,13 @@
import type { User } from "@prisma/client";
import type * as Lysand from "lysand-types";
import { config } from "config-manager";
import { getUserUri } from "./User";
import { getUserUri, type User } from "./User";
export const objectToInboxRequest = async (
object: Lysand.Entity,
author: User,
userToSendTo: User,
): Promise<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");
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,3 @@
import type { User } from "@prisma/client";
import { config } from "config-manager";
// import { Worker } from "bullmq";
import { type StatusWithRelations, statusToLysand } from "./Status";
@ -118,70 +117,6 @@ import { type StatusWithRelations, statusToLysand } from "./Status";
}
); */
/**
* Convert a string into an ArrayBuffer
* from https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
*/
export const str2ab = (str: string) => {
const buf = new ArrayBuffer(str.length);
const bufView = new Uint8Array(buf);
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
};
export const federateStatusTo = async (
status: StatusWithRelations,
sender: User,
user: User,
) => {
const privateKey = await crypto.subtle.importKey(
"pkcs8",
str2ab(atob(user.privateKey ?? "")),
"Ed25519",
false,
["sign"],
);
const digest = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode("request_body"),
);
const userInbox = new URL(user.endpoints.inbox);
const date = new Date();
const signature = await crypto.subtle.sign(
"Ed25519",
privateKey,
new TextEncoder().encode(
`(request-target): post ${userInbox.pathname}\n` +
`host: ${userInbox.host}\n` +
`date: ${date.toUTCString()}\n` +
`digest: SHA-256=${btoa(
String.fromCharCode(...new Uint8Array(digest)),
)}\n`,
),
);
const signatureBase64 = btoa(
String.fromCharCode(...new Uint8Array(signature)),
);
return fetch(userInbox, {
method: "POST",
headers: {
"Content-Type": "application/json",
Date: date.toUTCString(),
Origin: config.http.base_url,
Signature: `keyId="${sender.uri}",algorithm="ed25519",headers="(request-target) host date digest",signature="${signatureBase64}"`,
},
body: JSON.stringify(statusToLysand(status)),
});
};
export const addStatusFederationJob = async (statusId: string) => {
/* await federationQueue.add("federation", {
id: statusId,

View file

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

File diff suppressed because it is too large Load diff

View file

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

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

View file

@ -1,134 +1,139 @@
{
"name": "lysand",
"module": "index.ts",
"type": "module",
"version": "0.4.0",
"description": "A project to build a federated social network",
"author": {
"email": "contact@cpluspatch.com",
"name": "CPlusPatch",
"url": "https://cpluspatch.com"
},
"bugs": {
"url": "https://github.com/lysand-org/lysand/issues"
},
"icon": "https://github.com/lysand-org/lysand",
"license": "AGPL-3.0",
"keywords": ["federated", "activitypub", "bun"],
"workspaces": ["packages/*"],
"maintainers": [
{
"email": "contact@cpluspatch.com",
"name": "CPlusPatch",
"url": "https://cpluspatch.com"
}
],
"repository": {
"type": "git",
"url": "git+https://github.com/lysand-org/lysand.git"
},
"private": true,
"scripts": {
"dev": "bun run --watch index.ts",
"vite:dev": "bunx --bun vite pages",
"vite:build": "bunx --bun vite build pages",
"fe:dev": "bun --bun nuxt dev packages/frontend",
"fe:build": "bun --bun nuxt build packages/frontend",
"fe:analyze": "bun --bun nuxt analyze packages/frontend",
"start": "NITRO_PORT=5173 bun run dist/frontend/server/index.mjs & NODE_ENV=production bun run dist/index.js --prod",
"migrate-dev": "bun prisma migrate dev",
"migrate": "bun prisma migrate deploy",
"lint": "bunx --bun eslint --config .eslintrc.cjs --ext .ts .",
"prod-build": "bun run build.ts",
"prisma": "DATABASE_URL=$(bun run prisma.ts) bunx prisma",
"generate": "bun prisma generate",
"benchmark:timeline": "bun run benchmarks/timelines.ts",
"cloc": "cloc . --exclude-dir node_modules,dist",
"cli": "bun run cli.ts"
},
"trustedDependencies": [
"@biomejs/biome",
"@fortawesome/fontawesome-common-types",
"@fortawesome/free-regular-svg-icons",
"@fortawesome/free-solid-svg-icons",
"@prisma/client",
"@prisma/engines",
"esbuild",
"json-editor-vue",
"msgpackr-extract",
"nuxt-app",
"prisma",
"sharp",
"vue-demi"
],
"devDependencies": {
"@biomejs/biome": "1.6.4",
"@img/sharp-wasm32": "^0.33.3",
"@julr/unocss-preset-forms": "^0.1.0",
"@nuxtjs/seo": "^2.0.0-rc.10",
"@nuxtjs/tailwindcss": "^6.11.4",
"@types/cli-table": "^0.3.4",
"@types/html-to-text": "^9.0.4",
"@types/ioredis": "^5.0.0",
"@types/jsonld": "^1.5.13",
"@types/mime-types": "^2.1.4",
"@typescript-eslint/eslint-plugin": "latest",
"@unocss/cli": "latest",
"@unocss/transformer-directives": "^0.59.0",
"@vitejs/plugin-vue": "latest",
"@vueuse/head": "^2.0.0",
"activitypub-types": "^1.0.3",
"bun-types": "latest",
"shiki": "^1.2.4",
"typescript": "latest",
"unocss": "latest",
"untyped": "^1.4.2",
"vite": "^5.2.8",
"vite-ssr": "^0.17.1",
"vue": "^3.3.9",
"vue-router": "^4.2.5",
"vue-tsc": "latest"
},
"peerDependencies": {
"typescript": "^5.3.2"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.461.0",
"@iarna/toml": "^2.2.5",
"@json2csv/plainjs": "^7.0.6",
"@prisma/client": "^5.6.0",
"blurhash": "^2.0.5",
"bullmq": "latest",
"chalk": "^5.3.0",
"cli-parser": "workspace:*",
"cli-table": "^0.3.11",
"config-manager": "workspace:*",
"eventemitter3": "^5.0.1",
"extract-zip": "^2.0.1",
"html-to-text": "^9.0.5",
"ioredis": "^5.3.2",
"ip-matching": "^2.1.2",
"iso-639-1": "^3.1.0",
"isomorphic-dompurify": "latest",
"jsonld": "^8.3.1",
"linkify-html": "^4.1.3",
"linkify-string": "^4.1.3",
"linkifyjs": "^4.1.3",
"log-manager": "workspace:*",
"marked": "latest",
"media-manager": "workspace:*",
"megalodon": "^10.0.0",
"meilisearch": "latest",
"merge-deep-ts": "^1.2.6",
"mime-types": "^2.1.35",
"next-route-matcher": "^1.0.1",
"oauth4webapi": "^2.4.0",
"prisma": "^5.6.0",
"prisma-json-types-generator": "^3.0.4",
"prisma-redis-middleware": "^4.8.0",
"request-parser": "workspace:*",
"semver": "^7.5.4",
"sharp": "^0.33.3",
"strip-ansi": "^7.1.0"
"name": "lysand",
"module": "index.ts",
"type": "module",
"version": "0.4.0",
"description": "A project to build a federated social network",
"author": {
"email": "contact@cpluspatch.com",
"name": "CPlusPatch",
"url": "https://cpluspatch.com"
},
"bugs": {
"url": "https://github.com/lysand-org/lysand/issues"
},
"icon": "https://github.com/lysand-org/lysand",
"license": "AGPL-3.0",
"keywords": ["federated", "activitypub", "bun"],
"workspaces": ["packages/*"],
"maintainers": [
{
"email": "contact@cpluspatch.com",
"name": "CPlusPatch",
"url": "https://cpluspatch.com"
}
],
"repository": {
"type": "git",
"url": "git+https://github.com/lysand-org/lysand.git"
},
"private": true,
"scripts": {
"dev": "bun run --watch index.ts",
"vite:dev": "bunx --bun vite pages",
"vite:build": "bunx --bun vite build pages",
"fe:dev": "bun --bun nuxt dev packages/frontend",
"fe:build": "bun --bun nuxt build packages/frontend",
"fe:analyze": "bun --bun nuxt analyze packages/frontend",
"start": "NITRO_PORT=5173 bun run dist/frontend/server/index.mjs & NODE_ENV=production bun run dist/index.js --prod",
"migrate-dev": "bun prisma migrate dev",
"migrate": "bun prisma migrate deploy",
"lint": "bunx --bun eslint --config .eslintrc.cjs --ext .ts .",
"prod-build": "bun run build.ts",
"prisma": "DATABASE_URL=$(bun run prisma.ts) bunx prisma",
"generate": "bun prisma generate",
"benchmark:timeline": "bun run benchmarks/timelines.ts",
"cloc": "cloc . --exclude-dir node_modules,dist",
"cli": "bun run cli.ts"
},
"trustedDependencies": [
"@biomejs/biome",
"@fortawesome/fontawesome-common-types",
"@fortawesome/free-regular-svg-icons",
"@fortawesome/free-solid-svg-icons",
"@prisma/client",
"@prisma/engines",
"es5-ext",
"esbuild",
"json-editor-vue",
"msgpackr-extract",
"nuxt-app",
"prisma",
"sharp",
"vue-demi"
],
"devDependencies": {
"@biomejs/biome": "1.6.4",
"@img/sharp-wasm32": "^0.33.3",
"@julr/unocss-preset-forms": "^0.1.0",
"@nuxtjs/seo": "^2.0.0-rc.10",
"@nuxtjs/tailwindcss": "^6.11.4",
"@types/cli-table": "^0.3.4",
"@types/html-to-text": "^9.0.4",
"@types/ioredis": "^5.0.0",
"@types/jsonld": "^1.5.13",
"@types/mime-types": "^2.1.4",
"@types/pg": "^8.11.5",
"@typescript-eslint/eslint-plugin": "latest",
"@unocss/cli": "latest",
"@unocss/transformer-directives": "^0.59.0",
"@vitejs/plugin-vue": "latest",
"@vueuse/head": "^2.0.0",
"activitypub-types": "^1.0.3",
"bun-types": "latest",
"drizzle-kit": "^0.20.14",
"shiki": "^1.2.4",
"typescript": "latest",
"unocss": "latest",
"untyped": "^1.4.2",
"vite": "^5.2.8",
"vite-ssr": "^0.17.1",
"vue": "^3.3.9",
"vue-router": "^4.2.5",
"vue-tsc": "latest"
},
"peerDependencies": {
"typescript": "^5.3.2"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.461.0",
"@iarna/toml": "^2.2.5",
"@json2csv/plainjs": "^7.0.6",
"@prisma/client": "^5.6.0",
"blurhash": "^2.0.5",
"bullmq": "latest",
"chalk": "^5.3.0",
"cli-parser": "workspace:*",
"cli-table": "^0.3.11",
"config-manager": "workspace:*",
"drizzle-orm": "^0.30.7",
"eventemitter3": "^5.0.1",
"extract-zip": "^2.0.1",
"html-to-text": "^9.0.5",
"ioredis": "^5.3.2",
"ip-matching": "^2.1.2",
"iso-639-1": "^3.1.0",
"isomorphic-dompurify": "latest",
"jsonld": "^8.3.1",
"linkify-html": "^4.1.3",
"linkify-string": "^4.1.3",
"linkifyjs": "^4.1.3",
"log-manager": "workspace:*",
"marked": "latest",
"media-manager": "workspace:*",
"megalodon": "^10.0.0",
"meilisearch": "latest",
"merge-deep-ts": "^1.2.6",
"mime-types": "^2.1.35",
"next-route-matcher": "^1.0.1",
"oauth4webapi": "^2.4.0",
"pg": "^8.11.5",
"prisma": "^5.6.0",
"prisma-json-types-generator": "^3.0.4",
"prisma-redis-middleware": "^4.8.0",
"request-parser": "workspace:*",
"semver": "^7.5.4",
"sharp": "^0.33.3",
"strip-ansi": "^7.1.0"
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,8 +5,10 @@ import { client } from "~database/datasource";
import {
type StatusWithRelations,
statusToAPI,
findManyStatuses,
} from "~database/entities/Status";
import { statusAndUserRelations } from "~database/entities/relations";
import { db } from "~drizzle/db";
export const meta = applyConfig({
allowedMethods: ["GET"],
@ -39,48 +41,40 @@ export default apiRoute<{
if (!user) return errorResponse("Unauthorized", 401);
const followers = await db.query.relationship.findMany({
where: (relationship, { eq, and }) =>
and(
eq(relationship.subjectId, user.id),
eq(relationship.following, true),
),
});
const { objects, link } = await fetchTimeline<StatusWithRelations>(
client.status,
findManyStatuses,
{
where: {
id: {
lt: max_id ?? undefined,
gte: since_id ?? undefined,
gt: min_id ?? undefined,
},
OR: [
{
author: {
OR: [
{
relationshipSubjects: {
some: {
ownerId: user.id,
following: true,
},
},
},
{
id: user.id,
},
],
},
},
{
// Include posts where the user is mentioned in addition to posts by followed users
mentions: {
some: {
id: user.id,
},
},
},
],
},
include: statusAndUserRelations,
take: Number(limit),
orderBy: {
id: "desc",
},
// @ts-expect-error Yes I KNOW the types are wrong
where: (status, { lt, gte, gt, and, or, eq, inArray, sql }) =>
or(
and(
max_id ? lt(status.id, max_id) : undefined,
since_id ? gte(status.id, since_id) : undefined,
min_id ? gt(status.id, min_id) : undefined,
),
eq(status.authorId, user.id),
/* inArray(
status.authorId,
followers.map((f) => f.ownerId),
), */
// All statuses where the user is mentioned, using table StatusToUser which has a: status.id and b: user.id
// WHERE format (... = ...)
sql`EXISTS (SELECT 1 FROM "StatusToUser" WHERE "StatusToUser"."a" = ${status.id} AND "StatusToUser"."b" = ${user.id})`,
// All statuses from users that the user is following
// WHERE format (... = ...)
sql`EXISTS (SELECT 1 FROM "Relationship" WHERE "Relationship"."subjectId" = ${status.authorId} AND "Relationship"."ownerId" = ${user.id} AND "Relationship"."following" = true)`,
),
limit: Number(limit),
// @ts-expect-error Yes I KNOW the types are wrong
orderBy: (status, { desc }) => desc(status.id),
},
req,
);

View file

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

View file

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

View file

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

View file

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

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
@ -7,7 +7,9 @@ import { server } from "~index";
* @returns Response from the server
*/
export async function sendTestRequest(req: Request) {
return server.fetch(req);
console.log(req);
return fetch(req);
// return server.fetch(req);
}
export function wrapRelativeUrl(url: string, base_url: string) {

View file

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

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