More work on converting old Prisma calls to Drizzle

This commit is contained in:
Jesse Wierzbinski 2024-04-13 02:20:12 -10:00
parent 66922faa51
commit ad0bf1a350
No known key found for this signature in database
82 changed files with 2580 additions and 5631 deletions

View file

@ -2,19 +2,34 @@
"$schema": "https://biomejs.dev/schemas/1.6.4/schema.json", "$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
"organizeImports": { "organizeImports": {
"enabled": true, "enabled": true,
"ignore": ["node_modules/**/*", "dist/**/*"] "ignore": [
"node_modules/**/*",
"dist/**/*",
"packages/frontend/.output",
"packages/frontend/.nuxt"
]
}, },
"linter": { "linter": {
"enabled": true, "enabled": true,
"rules": { "rules": {
"recommended": true "recommended": true
}, },
"ignore": ["node_modules/**/*", "dist/**/*"] "ignore": [
"node_modules/**/*",
"dist/**/*",
"packages/frontend/.output",
"packages/frontend/.nuxt"
]
}, },
"formatter": { "formatter": {
"enabled": true, "enabled": true,
"indentStyle": "space", "indentStyle": "space",
"indentWidth": 4, "indentWidth": 4,
"ignore": ["node_modules/**/*", "dist/**/*"] "ignore": [
"node_modules/**/*",
"dist/**/*",
"packages/frontend/.output",
"packages/frontend/.nuxt"
]
} }
} }

2
cli.ts
View file

@ -9,12 +9,12 @@ import { CliBuilder, CliCommand } from "cli-parser";
import Table from "cli-table"; import Table from "cli-table";
import extract from "extract-zip"; import extract from "extract-zip";
import { MediaBackend } from "media-manager"; import { MediaBackend } from "media-manager";
import { lookup } from "mime-types";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { getUrl } from "~database/entities/Attachment"; import { getUrl } from "~database/entities/Attachment";
import { createNewLocalUser } from "~database/entities/User"; import { createNewLocalUser } from "~database/entities/User";
import { CliParameterType } from "~packages/cli-parser/cli-builder.type"; import { CliParameterType } from "~packages/cli-parser/cli-builder.type";
import { config } from "~packages/config-manager"; import { config } from "~packages/config-manager";
import { lookup } from "mime-types";
const args = process.argv; const args = process.argv;

View file

@ -1,11 +1,11 @@
import type { Config } from "config-manager"; import type { Config } from "config-manager";
import { MediaBackendType } from "media-manager"; import type { InferSelectModel } from "drizzle-orm";
import type { APIAsyncAttachment } from "~types/entities/async_attachment";
import type { APIAttachment } from "~types/entities/attachment";
import type * as Lysand from "lysand-types"; import type * as Lysand from "lysand-types";
import { MediaBackendType } from "media-manager";
import { db } from "~drizzle/db"; import { db } from "~drizzle/db";
import { attachment } from "~drizzle/schema"; import { attachment } from "~drizzle/schema";
import type { InferSelectModel } from "drizzle-orm"; import type { APIAsyncAttachment } from "~types/entities/async_attachment";
import type { APIAttachment } from "~types/entities/attachment";
export type Attachment = InferSelectModel<typeof attachment>; export type Attachment = InferSelectModel<typeof attachment>;

View file

@ -1,9 +1,9 @@
import type { APIEmoji } from "~types/entities/emoji"; import { type InferSelectModel, and, eq } from "drizzle-orm";
import type * as Lysand from "lysand-types"; import type * as Lysand from "lysand-types";
import { addInstanceIfNotExists } from "./Instance";
import { db } from "~drizzle/db"; import { db } from "~drizzle/db";
import { emoji, instance } from "~drizzle/schema"; import { emoji, instance } from "~drizzle/schema";
import { and, eq, type InferSelectModel } from "drizzle-orm"; import type { APIEmoji } from "~types/entities/emoji";
import { addInstanceIfNotExists } from "./Instance";
export type EmojiWithInstance = InferSelectModel<typeof emoji> & { export type EmojiWithInstance = InferSelectModel<typeof emoji> & {
instance: InferSelectModel<typeof instance> | null; instance: InferSelectModel<typeof instance> | null;

View file

@ -1,6 +1,6 @@
import type * as Lysand from "lysand-types";
import { config } from "config-manager"; import { config } from "config-manager";
import { getUserUri, type User } from "./User"; import type * as Lysand from "lysand-types";
import { type User, getUserUri } from "./User";
export const objectToInboxRequest = async ( export const objectToInboxRequest = async (
object: Lysand.Entity, object: Lysand.Entity,

View file

@ -1,5 +1,5 @@
import { db } from "~drizzle/db";
import type * as Lysand from "lysand-types"; import type * as Lysand from "lysand-types";
import { db } from "~drizzle/db";
import { instance } from "~drizzle/schema"; import { instance } from "~drizzle/schema";
/** /**

View file

@ -1,10 +1,10 @@
import { config } from "config-manager"; import { config } from "config-manager";
import { type InferSelectModel, and, eq } from "drizzle-orm";
import type * as Lysand from "lysand-types";
import { db } from "~drizzle/db";
import { like, notification } from "~drizzle/schema";
import type { StatusWithRelations } from "./Status"; import type { StatusWithRelations } from "./Status";
import type { UserWithRelations } from "./User"; import type { UserWithRelations } from "./User";
import type * as Lysand from "lysand-types";
import { and, eq, type InferSelectModel } from "drizzle-orm";
import { notification, like } from "~drizzle/schema";
import { db } from "~drizzle/db";
export type Like = InferSelectModel<typeof like>; export type Like = InferSelectModel<typeof like>;

View file

@ -1,19 +1,19 @@
import type { InferSelectModel } from "drizzle-orm";
import { db } from "~drizzle/db";
import type { notification } from "~drizzle/schema";
import type { APINotification } from "~types/entities/notification"; import type { APINotification } from "~types/entities/notification";
import { import {
type StatusWithRelations, type StatusWithRelations,
statusToAPI,
findFirstStatuses, findFirstStatuses,
statusToAPI,
} from "./Status"; } from "./Status";
import { import {
type UserWithRelations, type UserWithRelations,
userToAPI,
userRelations,
userExtrasTemplate,
transformOutputToUserWithRelations, transformOutputToUserWithRelations,
userExtrasTemplate,
userRelations,
userToAPI,
} from "./User"; } from "./User";
import type { InferSelectModel } from "drizzle-orm";
import type { notification } from "~drizzle/schema";
import { db } from "~drizzle/db";
export type Notification = InferSelectModel<typeof notification>; export type Notification = InferSelectModel<typeof notification>;

View file

@ -1,8 +1,8 @@
import type { InferSelectModel } from "drizzle-orm"; import type { InferSelectModel } from "drizzle-orm";
import type * as Lysand from "lysand-types";
import { db } from "~drizzle/db"; import { db } from "~drizzle/db";
import { lysandObject } from "~drizzle/schema"; import { lysandObject } from "~drizzle/schema";
import { findFirstUser } from "./User"; import { findFirstUser } from "./User";
import type * as Lysand from "lysand-types";
export type LysandObject = InferSelectModel<typeof lysandObject>; export type LysandObject = InferSelectModel<typeof lysandObject>;

View file

@ -1,9 +1,8 @@
import { client } from "~database/datasource"; import type { InferSelectModel } from "drizzle-orm";
import { db } from "~drizzle/db";
import { relationship } from "~drizzle/schema";
import type { APIRelationship } from "~types/entities/relationship"; import type { APIRelationship } from "~types/entities/relationship";
import type { User } from "./User"; import type { User } from "./User";
import type { InferSelectModel } from "drizzle-orm";
import { relationship } from "~drizzle/schema";
import { db } from "~drizzle/db";
export type Relationship = InferSelectModel<typeof relationship>; export type Relationship = InferSelectModel<typeof relationship>;

View file

@ -1,13 +1,34 @@
import { sanitizeHtml } from "@sanitization"; import { sanitizeHtml } from "@sanitization";
import { config } from "config-manager"; import { config } from "config-manager";
import {
type InferSelectModel,
and,
eq,
inArray,
isNotNull,
isNull,
or,
sql,
} from "drizzle-orm";
import { htmlToText } from "html-to-text"; import { htmlToText } from "html-to-text";
import linkifyHtml from "linkify-html"; import linkifyHtml from "linkify-html";
import linkifyStr from "linkify-string"; import linkifyStr from "linkify-string";
import type * as Lysand from "lysand-types";
import { parse } from "marked"; import { parse } from "marked";
import { db } from "~drizzle/db";
import {
type application,
attachment,
emojiToStatus,
instance,
type like,
status,
statusToUser,
user,
} from "~drizzle/schema";
import type { APIAttachment } from "~types/entities/attachment"; import type { APIAttachment } from "~types/entities/attachment";
import type { APIStatus } from "~types/entities/status"; import type { APIStatus } from "~types/entities/status";
import type { Note } from "~types/lysand/Object"; import type { Note } from "~types/lysand/Object";
import type * as Lysand from "lysand-types";
import { applicationToAPI } from "./Application"; import { applicationToAPI } from "./Application";
import { import {
attachmentFromLysand, attachmentFromLysand,
@ -15,48 +36,27 @@ import {
attachmentToLysand, attachmentToLysand,
} from "./Attachment"; } from "./Attachment";
import { import {
type EmojiWithInstance,
emojiToAPI, emojiToAPI,
emojiToLysand, emojiToLysand,
fetchEmoji, fetchEmoji,
parseEmojis, parseEmojis,
type EmojiWithInstance,
} from "./Emoji"; } from "./Emoji";
import { objectToInboxRequest } from "./Federation";
import { import {
getUserUri,
resolveUser,
resolveWebFinger,
userToAPI,
userExtras,
userRelations,
userExtrasTemplate,
type User, type User,
type UserWithRelations, type UserWithRelations,
type UserWithRelationsAndRelationships, type UserWithRelationsAndRelationships,
transformOutputToUserWithRelations,
findManyUsers, findManyUsers,
getUserUri,
resolveUser,
resolveWebFinger,
transformOutputToUserWithRelations,
userExtras,
userExtrasTemplate,
userRelations,
userToAPI,
} from "./User"; } from "./User";
import { objectToInboxRequest } from "./Federation";
import {
and,
eq,
or,
type InferSelectModel,
sql,
isNotNull,
inArray,
isNull,
} from "drizzle-orm";
import {
status,
type application,
attachment,
type like,
user,
statusToUser,
emojiToStatus,
instance,
} from "~drizzle/schema";
import { db } from "~drizzle/db";
export type Status = InferSelectModel<typeof status>; export type Status = InferSelectModel<typeof status>;

View file

@ -1,20 +1,10 @@
import { getBestContentType, urlToContentFormat } from "@content_types";
import { addUserToMeilisearch } from "@meilisearch"; import { addUserToMeilisearch } from "@meilisearch";
import { type Config, config } from "config-manager"; import { type Config, config } from "config-manager";
import { type InferSelectModel, and, eq, sql } from "drizzle-orm";
import { htmlToText } from "html-to-text"; import { htmlToText } from "html-to-text";
import type { APIAccount } from "~types/entities/account";
import type { APISource } from "~types/entities/source";
import type * as Lysand from "lysand-types"; import type * as Lysand from "lysand-types";
import { import { db } from "~drizzle/db";
fetchEmoji,
emojiToAPI,
emojiToLysand,
type EmojiWithInstance,
} from "./Emoji";
import { addInstanceIfNotExists } from "./Instance";
import { createNewRelationship } from "./Relationship";
import { getBestContentType, urlToContentFormat } from "@content_types";
import { objectToInboxRequest } from "./Federation";
import { and, eq, sql, type InferSelectModel } from "drizzle-orm";
import { import {
emojiToUser, emojiToUser,
instance, instance,
@ -22,7 +12,17 @@ import {
relationship, relationship,
user, user,
} from "~drizzle/schema"; } from "~drizzle/schema";
import { db } from "~drizzle/db"; import type { APIAccount } from "~types/entities/account";
import type { APISource } from "~types/entities/source";
import {
type EmojiWithInstance,
emojiToAPI,
emojiToLysand,
fetchEmoji,
} from "./Emoji";
import { objectToInboxRequest } from "./Federation";
import { addInstanceIfNotExists } from "./Instance";
import { createNewRelationship } from "./Relationship";
export type User = InferSelectModel<typeof user> & { export type User = InferSelectModel<typeof user> & {
endpoints?: Partial<{ endpoints?: Partial<{

View file

@ -1,462 +0,0 @@
-- 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

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

View file

@ -1,292 +0,0 @@
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(),
});

View file

@ -1,5 +1,5 @@
import type { Config } from "drizzle-kit";
import { config } from "config-manager"; import { config } from "config-manager";
import type { Config } from "drizzle-kit";
export default { export default {
driver: "pg", driver: "pg",

View file

@ -1,6 +1,6 @@
import { Client } from "pg";
import { config } from "config-manager"; import { config } from "config-manager";
import { drizzle } from "drizzle-orm/node-postgres"; import { drizzle } from "drizzle-orm/node-postgres";
import { Client } from "pg";
import * as schema from "./schema"; import * as schema from "./schema";
export const client = new Client({ export const client = new Client({

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,16 +1,16 @@
import {
pgTable,
timestamp,
text,
integer,
foreignKey,
uuid,
boolean,
uniqueIndex,
jsonb,
index,
} from "drizzle-orm/pg-core";
import { relations, sql } from "drizzle-orm"; import { relations, sql } from "drizzle-orm";
import {
boolean,
foreignKey,
index,
integer,
jsonb,
pgTable,
text,
timestamp,
uniqueIndex,
uuid,
} from "drizzle-orm/pg-core";
export const emoji = pgTable("Emoji", { export const emoji = pgTable("Emoji", {
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),

View file

@ -4,11 +4,11 @@ import { connectMeili } from "@meilisearch";
import { moduleIsEntry } from "@module"; import { moduleIsEntry } from "@module";
import { initializeRedisCache } from "@redis"; import { initializeRedisCache } from "@redis";
import { config } from "config-manager"; import { config } from "config-manager";
import { count, sql } from "drizzle-orm";
import { LogLevel, LogManager, MultiLogManager } from "log-manager"; import { LogLevel, LogManager, MultiLogManager } from "log-manager";
import { createServer } from "~server";
import { db, client as pgClient } from "~drizzle/db"; import { db, client as pgClient } from "~drizzle/db";
import { status } from "~drizzle/schema"; import { status } from "~drizzle/schema";
import { count, sql } from "drizzle-orm"; import { createServer } from "~server";
await pgClient.connect(); await pgClient.connect();
const timeAtStart = performance.now(); const timeAtStart = performance.now();

View file

@ -1,139 +1,139 @@
{ {
"name": "lysand", "name": "lysand",
"module": "index.ts", "module": "index.ts",
"type": "module", "type": "module",
"version": "0.4.0", "version": "0.4.0",
"description": "A project to build a federated social network", "description": "A project to build a federated social network",
"author": { "author": {
"email": "contact@cpluspatch.com", "email": "contact@cpluspatch.com",
"name": "CPlusPatch", "name": "CPlusPatch",
"url": "https://cpluspatch.com" "url": "https://cpluspatch.com"
}, },
"bugs": { "bugs": {
"url": "https://github.com/lysand-org/lysand/issues" "url": "https://github.com/lysand-org/lysand/issues"
}, },
"icon": "https://github.com/lysand-org/lysand", "icon": "https://github.com/lysand-org/lysand",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"keywords": ["federated", "activitypub", "bun"], "keywords": ["federated", "activitypub", "bun"],
"workspaces": ["packages/*"], "workspaces": ["packages/*"],
"maintainers": [ "maintainers": [
{ {
"email": "contact@cpluspatch.com", "email": "contact@cpluspatch.com",
"name": "CPlusPatch", "name": "CPlusPatch",
"url": "https://cpluspatch.com" "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"
} }
],
"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,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ETailwind, EButton } from "vue-email"; import { EButton, ETailwind } from "vue-email";
import tailwindConfig from "~/tailwind.config"; import tailwindConfig from "~/tailwind.config";
defineProps<{ defineProps<{

View file

@ -1,22 +1,22 @@
{ {
"name": "nuxt-app", "name": "nuxt-app",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "nuxt build", "build": "nuxt build",
"dev": "nuxt dev", "dev": "nuxt dev",
"generate": "nuxt generate", "generate": "nuxt generate",
"preview": "nuxt preview", "preview": "nuxt preview",
"postinstall": "nuxt prepare" "postinstall": "nuxt prepare"
}, },
"dependencies": { "dependencies": {
"c12": "^1.10.0", "c12": "^1.10.0",
"nuxt": "^3.11.2", "nuxt": "^3.11.2",
"vue": "^3.4.21", "vue": "^3.4.21",
"vue-router": "^4.3.0" "vue-router": "^4.3.0"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/forms": "^0.5.7", "@tailwindcss/forms": "^0.5.7",
"@vue-email/nuxt": "^0.8.19" "@vue-email/nuxt": "^0.8.19"
} }
} }

View file

@ -10,9 +10,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useRoute } from "vue-router";
import { getHighlighterCore } from "shiki/core"; import { getHighlighterCore } from "shiki/core";
import getWasm from "shiki/wasm"; import getWasm from "shiki/wasm";
import { useRoute } from "vue-router";
const config = (await useFetch("/api/config")).data.value; const config = (await useFetch("/api/config")).data.value;

View file

@ -10,9 +10,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useRoute } from "vue-router";
import { getHighlighterCore } from "shiki/core"; import { getHighlighterCore } from "shiki/core";
import getWasm from "shiki/wasm"; import getWasm from "shiki/wasm";
import { useRoute } from "vue-router";
const config = (await useFetch("/api/config")).data.value; const config = (await useFetch("/api/config")).data.value;

View file

@ -1,5 +1,5 @@
import type { Config } from "tailwindcss";
import forms from "@tailwindcss/forms"; import forms from "@tailwindcss/forms";
import type { Config } from "tailwindcss";
// Default are on https://tailwindcss.nuxtjs.org/tailwind/config#default-configuration // Default are on https://tailwindcss.nuxtjs.org/tailwind/config#default-configuration
export default (<Partial<Config>>{ export default (<Partial<Config>>{

View file

@ -1,4 +1,4 @@
import { appendFile, mkdir, exists } from "node:fs/promises"; import { appendFile, exists, mkdir } from "node:fs/promises";
import { dirname } from "node:path"; import { dirname } from "node:path";
import type { BunFile } from "bun"; import type { BunFile } from "bun";

View file

@ -1,5 +1,5 @@
import type { Config } from "config-manager";
import { S3Client } from "@jsr/bradenmacdonald__s3-lite-client"; import { S3Client } from "@jsr/bradenmacdonald__s3-lite-client";
import type { Config } from "config-manager";
import type { ConvertableMediaFormats } from "./media-converter"; import type { ConvertableMediaFormats } from "./media-converter";
import { MediaConverter } from "./media-converter"; import { MediaConverter } from "./media-converter";

View file

@ -3,10 +3,10 @@ import type { S3Client } from "@jsr/bradenmacdonald__s3-lite-client";
import type { Config } from "config-manager"; import type { Config } from "config-manager";
// FILEPATH: /home/jessew/Dev/lysand/packages/media-manager/backends/s3.test.ts // FILEPATH: /home/jessew/Dev/lysand/packages/media-manager/backends/s3.test.ts
import { import {
LocalMediaBackend,
MediaBackend, MediaBackend,
MediaBackendType, MediaBackendType,
MediaHasher, MediaHasher,
LocalMediaBackend,
S3MediaBackend, S3MediaBackend,
} from ".."; } from "..";
import { ConvertableMediaFormats, MediaConverter } from "../media-converter"; import { ConvertableMediaFormats, MediaConverter } from "../media-converter";

View file

@ -1,8 +1,9 @@
import { randomBytes } from "node:crypto"; import { randomBytes } from "node:crypto";
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { client } from "~database/datasource";
import { TokenType } from "~database/entities/Token"; import { TokenType } from "~database/entities/Token";
import { userRelations } from "~database/entities/relations"; import { findFirstUser } from "~database/entities/User";
import { db } from "~drizzle/db";
import { token } from "~drizzle/schema";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -47,45 +48,28 @@ export default apiRoute<{
if (!email || !password) if (!email || !password)
return redirectToLogin("Invalid username or password"); return redirectToLogin("Invalid username or password");
// Get user const user = await findFirstUser({
const user = await client.user.findFirst({ where: (user, { eq }) => eq(user.email, email),
where: {
email,
},
include: userRelations,
}); });
if (!user || !(await Bun.password.verify(password, user.password || ""))) if (!user || !(await Bun.password.verify(password, user.password || "")))
return redirectToLogin("Invalid username or password"); return redirectToLogin("Invalid username or password");
// Get application const application = await db.query.application.findFirst({
const application = await client.application.findFirst({ where: (app, { eq }) => eq(app.clientId, client_id),
where: {
client_id,
},
}); });
if (!application) return redirectToLogin("Invalid client_id"); if (!application) return redirectToLogin("Invalid client_id");
const code = randomBytes(32).toString("hex"); const code = randomBytes(32).toString("hex");
await client.application.update({ await db.insert(token).values({
where: { id: application.id }, accessToken: randomBytes(64).toString("base64url"),
data: { code: code,
tokens: { scope: scopes.join(" "),
create: { tokenType: TokenType.BEARER,
access_token: randomBytes(64).toString("base64url"), applicationId: application.id,
code: code, userId: user.id,
scope: scopes.join(" "),
token_type: TokenType.BEARER,
user: {
connect: {
id: user.id,
},
},
},
},
},
}); });
// Redirect to OAuth confirmation screen // Redirect to OAuth confirmation screen

View file

@ -1,6 +1,7 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { client } from "~database/datasource"; import { and, eq } from "drizzle-orm";
import { userRelations } from "~database/entities/relations"; import { db } from "~drizzle/db";
import { application, token } from "~drizzle/schema";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -34,23 +35,15 @@ export default apiRoute<{
302, 302,
); );
// Get token const foundToken = await db
const token = await client.token.findFirst({ .select()
where: { .from(token)
code, .leftJoin(application, eq(token.applicationId, application.id))
application: { .where(and(eq(token.code, code), eq(application.clientId, client_id)))
client_id, .limit(1);
},
},
include: {
user: {
include: userRelations,
},
application: true,
},
});
if (!token) return redirectToLogin("Invalid code"); if (!foundToken || foundToken.length <= 0)
return redirectToLogin("Invalid code");
// Redirect back to application // Redirect back to application
return Response.redirect(`${redirect_uri}?code=${code}`, 302); return Response.redirect(`${redirect_uri}?code=${code}`, 302);

View file

@ -1,11 +1,13 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { eq } from "drizzle-orm";
import { relationshipToAPI } from "~database/entities/Relationship";
import { import {
createNewRelationship, findFirstUser,
relationshipToAPI, getRelationshipToOtherUser,
} from "~database/entities/Relationship"; } from "~database/entities/User";
import { getRelationshipToOtherUser } from "~database/entities/User"; import { db } from "~drizzle/db";
import { relationship } from "~drizzle/schema";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -30,52 +32,24 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
const user = await client.user.findUnique({ const otherUser = await findFirstUser({
where: { id }, where: (user, { eq }) => eq(user.id, id),
include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
},
}); });
if (!user) return errorResponse("User not found", 404); if (!otherUser) return errorResponse("User not found", 404);
// Check if already following const foundRelationship = await getRelationshipToOtherUser(self, otherUser);
let relationship = await getRelationshipToOtherUser(self, user);
if (!relationship) { if (!foundRelationship.blocking) {
// Create new relationship foundRelationship.blocking = true;
const newRelationship = await createNewRelationship(self, user);
await client.user.update({
where: { id: self.id },
data: {
relationships: {
connect: {
id: newRelationship.id,
},
},
},
});
relationship = newRelationship;
} }
if (!relationship.blocking) { await db
relationship.blocking = true; .update(relationship)
} .set({
await client.relationship.update({
where: { id: relationship.id },
data: {
blocking: true, blocking: true,
}, })
}); .where(eq(relationship.id, foundRelationship.id));
return jsonResponse(relationshipToAPI(relationship)); return jsonResponse(relationshipToAPI(foundRelationship));
}); });

View file

@ -1,11 +1,8 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { relationshipToAPI } from "~database/entities/Relationship";
import {
createNewRelationship,
relationshipToAPI,
} from "~database/entities/Relationship";
import { import {
findFirstUser,
followRequestUser, followRequestUser,
getRelationshipToOtherUser, getRelationshipToOtherUser,
} from "~database/entities/User"; } from "~database/entities/User";
@ -39,27 +36,19 @@ export default apiRoute<{
const { languages, notify, reblogs } = extraData.parsedRequest; const { languages, notify, reblogs } = extraData.parsedRequest;
const user = await client.user.findUnique({ const otherUser = await findFirstUser({
where: { id }, where: (user, { eq }) => eq(user.id, id),
include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
},
}); });
if (!user) return errorResponse("User not found", 404); if (!otherUser) return errorResponse("User not found", 404);
// Check if already following // Check if already following
let relationship = await getRelationshipToOtherUser(self, user); let relationship = await getRelationshipToOtherUser(self, otherUser);
if (!relationship.following) { if (!relationship.following) {
relationship = await followRequestUser( relationship = await followRequestUser(
self, self,
user, otherUser,
relationship.id, relationship.id,
reblogs, reblogs,
notify, notify,

View file

@ -1,9 +1,12 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines"; import { fetchTimeline } from "@timelines";
import { client } from "~database/datasource"; import {
import { type UserWithRelations, userToAPI } from "~database/entities/User"; type UserWithRelations,
import { userRelations } from "~database/entities/relations"; findFirstUser,
findManyUsers,
userToAPI,
} from "~database/entities/User";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -32,36 +35,27 @@ export default apiRoute<{
// TODO: Add pinned // TODO: Add pinned
const { max_id, min_id, since_id, limit = 20 } = extraData.parsedRequest; const { max_id, min_id, since_id, limit = 20 } = extraData.parsedRequest;
const user = await client.user.findUnique({ const otherUser = await findFirstUser({
where: { id }, where: (user, { eq }) => eq(user.id, id),
include: userRelations,
}); });
if (limit < 1 || limit > 40) return errorResponse("Invalid limit", 400); if (limit < 1 || limit > 40) return errorResponse("Invalid limit", 400);
if (!user) return errorResponse("User not found", 404); if (!otherUser) return errorResponse("User not found", 404);
const { objects, link } = await fetchTimeline<UserWithRelations>( const { objects, link } = await fetchTimeline<UserWithRelations>(
client.user, findManyUsers,
{ {
where: { // @ts-ignore
relationships: { where: (follower, { and, lt, gt, gte, eq, sql }) =>
some: { and(
subjectId: user.id, max_id ? lt(follower.id, max_id) : undefined,
following: true, since_id ? gte(follower.id, since_id) : undefined,
}, min_id ? gt(follower.id, min_id) : undefined,
}, sql`EXISTS (SELECT 1 FROM "Relationship" WHERE "Relationship"."subjectId" = ${otherUser.id} AND "Relationship"."objectId" = ${follower.id} AND "Relationship"."following" = true)`,
id: { ),
lt: max_id, // @ts-expect-error Yes I KNOW the types are wrong
gt: min_id, orderBy: (liker, { desc }) => desc(liker.id),
gte: since_id,
},
},
include: userRelations,
take: Number(limit),
orderBy: {
id: "desc",
},
}, },
req, req,
); );

View file

@ -1,9 +1,12 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines"; import { fetchTimeline } from "@timelines";
import { client } from "~database/datasource"; import {
import { userToAPI, type UserWithRelations } from "~database/entities/User"; type UserWithRelations,
import { userRelations } from "~database/entities/relations"; findFirstUser,
findManyUsers,
userToAPI,
} from "~database/entities/User";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -32,36 +35,27 @@ export default apiRoute<{
// TODO: Add pinned // TODO: Add pinned
const { max_id, min_id, since_id, limit = 20 } = extraData.parsedRequest; const { max_id, min_id, since_id, limit = 20 } = extraData.parsedRequest;
const user = await client.user.findUnique({ const otherUser = await findFirstUser({
where: { id }, where: (user, { eq }) => eq(user.id, id),
include: userRelations,
}); });
if (limit < 1 || limit > 40) return errorResponse("Invalid limit", 400); if (limit < 1 || limit > 40) return errorResponse("Invalid limit", 400);
if (!user) return errorResponse("User not found", 404); if (!otherUser) return errorResponse("User not found", 404);
const { objects, link } = await fetchTimeline<UserWithRelations>( const { objects, link } = await fetchTimeline<UserWithRelations>(
client.user, findManyUsers,
{ {
where: { // @ts-ignore
relationshipSubjects: { where: (following, { and, lt, gt, gte, eq, sql }) =>
some: { and(
ownerId: user.id, max_id ? lt(following.id, max_id) : undefined,
following: true, since_id ? gte(following.id, since_id) : undefined,
}, min_id ? gt(following.id, min_id) : undefined,
}, sql`EXISTS (SELECT 1 FROM "Relationship" WHERE "Relationship"."subjectId" = ${following.id} AND "Relationship"."objectId" = ${otherUser.id} AND "Relationship"."following" = true)`,
id: { ),
lt: max_id, // @ts-expect-error Yes I KNOW the types are wrong
gt: min_id, orderBy: (liker, { desc }) => desc(liker.id),
gte: since_id,
},
},
include: userRelations,
take: Number(limit),
orderBy: {
id: "desc",
},
}, },
req, req,
); );

View file

@ -1,13 +1,13 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { config } from "~index";
import { import {
deleteOldTestUsers, deleteOldTestUsers,
getTestStatuses, getTestStatuses,
getTestUsers, getTestUsers,
sendTestRequest, sendTestRequest,
} from "~tests/utils"; } from "~tests/utils";
import { config } from "~index";
import { meta } from "./index";
import type { APIAccount } from "~types/entities/account"; import type { APIAccount } from "~types/entities/account";
import { meta } from "./index";
await deleteOldTestUsers(); await deleteOldTestUsers();

View file

@ -1,11 +1,13 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { eq } from "drizzle-orm";
import { relationshipToAPI } from "~database/entities/Relationship";
import { import {
createNewRelationship, findFirstUser,
relationshipToAPI, getRelationshipToOtherUser,
} from "~database/entities/Relationship"; } from "~database/entities/User";
import { getRelationshipToOtherUser } from "~database/entities/User"; import { db } from "~drizzle/db";
import { relationship } from "~drizzle/schema";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -35,58 +37,31 @@ export default apiRoute<{
const { notifications, duration } = extraData.parsedRequest; const { notifications, duration } = extraData.parsedRequest;
const user = await client.user.findUnique({ const user = await findFirstUser({
where: { id }, where: (user, { eq }) => eq(user.id, id),
include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
},
}); });
if (!user) return errorResponse("User not found", 404); if (!user) return errorResponse("User not found", 404);
// Check if already following // Check if already following
let relationship = await getRelationshipToOtherUser(self, user); const foundRelationship = await getRelationshipToOtherUser(self, user);
if (!relationship) { if (!foundRelationship.muting) {
// Create new relationship foundRelationship.muting = true;
const newRelationship = await createNewRelationship(self, user);
await client.user.update({
where: { id: self.id },
data: {
relationships: {
connect: {
id: newRelationship.id,
},
},
},
});
relationship = newRelationship;
}
if (!relationship.muting) {
relationship.muting = true;
} }
if (notifications ?? true) { if (notifications ?? true) {
relationship.mutingNotifications = true; foundRelationship.mutingNotifications = true;
} }
await client.relationship.update({ await db
where: { id: relationship.id }, .update(relationship)
data: { .set({
muting: true, muting: true,
mutingNotifications: notifications ?? true, mutingNotifications: notifications ?? true,
}, })
}); .where(eq(relationship.id, foundRelationship.id));
// TODO: Implement duration // TODO: Implement duration
return jsonResponse(relationshipToAPI(relationship)); return jsonResponse(relationshipToAPI(foundRelationship));
}); });

View file

@ -1,11 +1,13 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { eq } from "drizzle-orm";
import { relationshipToAPI } from "~database/entities/Relationship";
import { import {
createNewRelationship, findFirstUser,
relationshipToAPI, getRelationshipToOtherUser,
} from "~database/entities/Relationship"; } from "~database/entities/User";
import { getRelationshipToOtherUser } from "~database/entities/User"; import { db } from "~drizzle/db";
import { relationship } from "~drizzle/schema";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -34,50 +36,23 @@ export default apiRoute<{
const { comment } = extraData.parsedRequest; const { comment } = extraData.parsedRequest;
const user = await client.user.findUnique({ const otherUser = await findFirstUser({
where: { id }, where: (user, { eq }) => eq(user.id, id),
include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
},
}); });
if (!user) return errorResponse("User not found", 404); if (!otherUser) return errorResponse("User not found", 404);
// Check if already following // Check if already following
let relationship = await getRelationshipToOtherUser(self, user); const foundRelationship = await getRelationshipToOtherUser(self, otherUser);
if (!relationship) { foundRelationship.note = comment ?? "";
// Create new relationship
const newRelationship = await createNewRelationship(self, user); await db
.update(relationship)
.set({
note: foundRelationship.note,
})
.where(eq(relationship.id, foundRelationship.id));
await client.user.update({ return jsonResponse(relationshipToAPI(foundRelationship));
where: { id: self.id },
data: {
relationships: {
connect: {
id: newRelationship.id,
},
},
},
});
relationship = newRelationship;
}
relationship.note = comment ?? "";
await client.relationship.update({
where: { id: relationship.id },
data: {
note: relationship.note,
},
});
return jsonResponse(relationshipToAPI(relationship));
}); });

View file

@ -1,11 +1,13 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { eq } from "drizzle-orm";
import { relationshipToAPI } from "~database/entities/Relationship";
import { import {
createNewRelationship, findFirstUser,
relationshipToAPI, getRelationshipToOtherUser,
} from "~database/entities/Relationship"; } from "~database/entities/User";
import { getRelationshipToOtherUser } from "~database/entities/User"; import { db } from "~drizzle/db";
import { relationship } from "~drizzle/schema";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -30,52 +32,25 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
const user = await client.user.findUnique({ const otherUser = await findFirstUser({
where: { id }, where: (user, { eq }) => eq(user.id, id),
include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
},
}); });
if (!user) return errorResponse("User not found", 404); if (!otherUser) return errorResponse("User not found", 404);
// Check if already following // Check if already following
let relationship = await getRelationshipToOtherUser(self, user); const foundRelationship = await getRelationshipToOtherUser(self, otherUser);
if (!relationship) { if (!foundRelationship.endorsed) {
// Create new relationship foundRelationship.endorsed = true;
const newRelationship = await createNewRelationship(self, user); await db
.update(relationship)
await client.user.update({ .set({
where: { id: self.id }, endorsed: true,
data: { })
relationships: { .where(eq(relationship.id, foundRelationship.id));
connect: {
id: newRelationship.id,
},
},
},
});
relationship = newRelationship;
} }
if (!relationship.endorsed) { return jsonResponse(relationshipToAPI(foundRelationship));
relationship.endorsed = true;
}
await client.relationship.update({
where: { id: relationship.id },
data: {
endorsed: true,
},
});
return jsonResponse(relationshipToAPI(relationship));
}); });

View file

@ -1,11 +1,13 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { and, eq } from "drizzle-orm";
import { relationshipToAPI } from "~database/entities/Relationship";
import { import {
createNewRelationship, findFirstUser,
relationshipToAPI, getRelationshipToOtherUser,
} from "~database/entities/Relationship"; } from "~database/entities/User";
import { getRelationshipToOtherUser } from "~database/entities/User"; import { db } from "~drizzle/db";
import { relationship } from "~drizzle/schema";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -30,66 +32,40 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
const user = await client.user.findUnique({ const otherUser = await findFirstUser({
where: { id }, where: (user, { eq }) => eq(user.id, id),
include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
},
}); });
if (!user) return errorResponse("User not found", 404); if (!otherUser) return errorResponse("User not found", 404);
// Check if already following // Check if already following
let relationship = await getRelationshipToOtherUser(self, user); const foundRelationship = await getRelationshipToOtherUser(self, otherUser);
if (!relationship) { if (foundRelationship.followedBy) {
// Create new relationship foundRelationship.followedBy = false;
const newRelationship = await createNewRelationship(self, user); await db
.update(relationship)
.set({
followedBy: false,
})
.where(eq(relationship.id, foundRelationship.id));
await client.user.update({ if (otherUser.instanceId === null) {
where: { id: self.id }, // Also remove from followers list
data: { await db
relationships: { .update(relationship)
connect: { .set({
id: newRelationship.id, following: false,
}, })
}, .where(
}, and(
}); eq(relationship.ownerId, otherUser.id),
eq(relationship.subjectId, self.id),
relationship = newRelationship; ),
);
}
} }
if (relationship.followedBy) { return jsonResponse(relationshipToAPI(foundRelationship));
relationship.followedBy = false;
}
await client.relationship.update({
where: { id: relationship.id },
data: {
followedBy: false,
},
});
if (user.instanceId === null) {
// Also remove from followers list
await client.relationship.updateMany({
where: {
ownerId: user.id,
subjectId: self.id,
following: true,
},
data: {
following: false,
},
});
}
return jsonResponse(relationshipToAPI(relationship));
}); });

View file

@ -1,18 +1,12 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines"; import { fetchTimeline } from "@timelines";
import { sql } from "drizzle-orm";
import { client } from "~database/datasource";
import { import {
type StatusWithRelations,
findManyStatuses, findManyStatuses,
statusToAPI, statusToAPI,
type StatusWithRelations,
} from "~database/entities/Status"; } from "~database/entities/Status";
import { findFirstUser } from "~database/entities/User"; import { findFirstUser } from "~database/entities/User";
import {
statusAndUserRelations,
userRelations,
} from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],

View file

@ -1,11 +1,13 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { eq } from "drizzle-orm";
import { relationshipToAPI } from "~database/entities/Relationship";
import { import {
createNewRelationship, findFirstUser,
relationshipToAPI, getRelationshipToOtherUser,
} from "~database/entities/Relationship"; } from "~database/entities/User";
import { getRelationshipToOtherUser } from "~database/entities/User"; import { db } from "~drizzle/db";
import { relationship } from "~drizzle/schema";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -20,9 +22,6 @@ export const meta = applyConfig({
}, },
}); });
/**
* Blocks a user
*/
export default apiRoute(async (req, matchedRoute, extraData) => { export default apiRoute(async (req, matchedRoute, extraData) => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
@ -30,52 +29,24 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
const user = await client.user.findUnique({ const otherUser = await findFirstUser({
where: { id }, where: (user, { eq }) => eq(user.id, id),
include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
},
}); });
if (!user) return errorResponse("User not found", 404); if (!otherUser) return errorResponse("User not found", 404);
// Check if already following const foundRelationship = await getRelationshipToOtherUser(self, otherUser);
let relationship = await getRelationshipToOtherUser(self, user);
if (!relationship) { if (foundRelationship.blocking) {
// Create new relationship foundRelationship.blocking = false;
const newRelationship = await createNewRelationship(self, user); await db
.update(relationship)
await client.user.update({ .set({
where: { id: self.id }, blocking: false,
data: { })
relationships: { .where(eq(relationship.id, foundRelationship.id));
connect: {
id: newRelationship.id,
},
},
},
});
relationship = newRelationship;
} }
if (relationship.blocking) { return jsonResponse(relationshipToAPI(foundRelationship));
relationship.blocking = false;
}
await client.relationship.update({
where: { id: relationship.id },
data: {
blocking: false,
},
});
return jsonResponse(relationshipToAPI(relationship));
}); });

View file

@ -1,11 +1,13 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { eq } from "drizzle-orm";
import { relationshipToAPI } from "~database/entities/Relationship";
import { import {
createNewRelationship, findFirstUser,
relationshipToAPI, getRelationshipToOtherUser,
} from "~database/entities/Relationship"; } from "~database/entities/User";
import { getRelationshipToOtherUser } from "~database/entities/User"; import { db } from "~drizzle/db";
import { relationship } from "~drizzle/schema";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -30,53 +32,26 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
const user = await client.user.findUnique({ const otherUser = await findFirstUser({
where: { id }, where: (user, { eq }) => eq(user.id, id),
include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
},
}); });
if (!user) return errorResponse("User not found", 404); if (!otherUser) return errorResponse("User not found", 404);
// Check if already following // Check if already following
let relationship = await getRelationshipToOtherUser(self, user); const foundRelationship = await getRelationshipToOtherUser(self, otherUser);
if (!relationship) { if (foundRelationship.following) {
// Create new relationship foundRelationship.following = false;
const newRelationship = await createNewRelationship(self, user); await db
.update(relationship)
await client.user.update({ .set({
where: { id: self.id }, following: false,
data: { requested: false,
relationships: { })
connect: { .where(eq(relationship.id, foundRelationship.id));
id: newRelationship.id,
},
},
},
});
relationship = newRelationship;
} }
if (relationship.following) { return jsonResponse(relationshipToAPI(foundRelationship));
relationship.following = false;
}
await client.relationship.update({
where: { id: relationship.id },
data: {
following: false,
requested: false,
},
});
return jsonResponse(relationshipToAPI(relationship));
}); });

View file

@ -1,11 +1,13 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { eq } from "drizzle-orm";
import { relationshipToAPI } from "~database/entities/Relationship";
import { import {
createNewRelationship, findFirstUser,
relationshipToAPI, getRelationshipToOtherUser,
} from "~database/entities/Relationship"; } from "~database/entities/User";
import { getRelationshipToOtherUser } from "~database/entities/User"; import { db } from "~drizzle/db";
import { relationship } from "~drizzle/schema";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -30,54 +32,27 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
const user = await client.user.findUnique({ const user = await findFirstUser({
where: { id }, where: (user, { eq }) => eq(user.id, id),
include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
},
}); });
if (!user) return errorResponse("User not found", 404); if (!user) return errorResponse("User not found", 404);
// Check if already following // Check if already following
let relationship = await getRelationshipToOtherUser(self, user); const foundRelationship = await getRelationshipToOtherUser(self, user);
if (!relationship) { if (foundRelationship.muting) {
// Create new relationship foundRelationship.muting = false;
foundRelationship.mutingNotifications = false;
const newRelationship = await createNewRelationship(self, user); await db
.update(relationship)
await client.user.update({ .set({
where: { id: self.id }, muting: false,
data: { mutingNotifications: false,
relationships: { })
connect: { .where(eq(relationship.id, foundRelationship.id));
id: newRelationship.id,
},
},
},
});
relationship = newRelationship;
} }
if (relationship.muting) { return jsonResponse(relationshipToAPI(foundRelationship));
relationship.muting = false;
}
// TODO: Implement duration
await client.relationship.update({
where: { id: relationship.id },
data: {
muting: false,
},
});
return jsonResponse(relationshipToAPI(relationship));
}); });

View file

@ -1,11 +1,13 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { eq } from "drizzle-orm";
import { relationshipToAPI } from "~database/entities/Relationship";
import { import {
createNewRelationship, findFirstUser,
relationshipToAPI, getRelationshipToOtherUser,
} from "~database/entities/Relationship"; } from "~database/entities/User";
import { getRelationshipToOtherUser } from "~database/entities/User"; import { db } from "~drizzle/db";
import { relationship } from "~drizzle/schema";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -30,52 +32,25 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
const user = await client.user.findUnique({ const otherUser = await findFirstUser({
where: { id }, where: (user, { eq }) => eq(user.id, id),
include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
},
}); });
if (!user) return errorResponse("User not found", 404); if (!otherUser) return errorResponse("User not found", 404);
// Check if already following // Check if already following
let relationship = await getRelationshipToOtherUser(self, user); const foundRelationship = await getRelationshipToOtherUser(self, otherUser);
if (!relationship) { if (foundRelationship.endorsed) {
// Create new relationship foundRelationship.endorsed = false;
const newRelationship = await createNewRelationship(self, user); await db
.update(relationship)
await client.user.update({ .set({
where: { id: self.id }, endorsed: false,
data: { })
relationships: { .where(eq(relationship.id, foundRelationship.id));
connect: {
id: newRelationship.id,
},
},
},
});
relationship = newRelationship;
} }
if (relationship.endorsed) { return jsonResponse(relationshipToAPI(foundRelationship));
relationship.endorsed = false;
}
await client.relationship.update({
where: { id: relationship.id },
data: {
endorsed: false,
},
});
return jsonResponse(relationshipToAPI(relationship));
}); });

View file

@ -1,8 +1,7 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { findManyUsers, userToAPI } from "~database/entities/User";
import { userToAPI } from "~database/entities/User"; import { db } from "~drizzle/db";
import { userRelations } from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -34,34 +33,52 @@ export default apiRoute<{
return errorResponse("Number of ids must be between 1 and 10", 422); return errorResponse("Number of ids must be between 1 and 10", 422);
} }
const followersOfIds = await client.user.findMany({ const idFollowerRelationships = await db.query.relationship.findMany({
where: { columns: {
relationships: { ownerId: true,
some: {
subjectId: {
in: ids,
},
following: true,
},
},
}, },
where: (relationship, { inArray, and, eq }) =>
and(
inArray(relationship.subjectId, ids),
eq(relationship.following, true),
),
}); });
// Find users that you follow in followersOfIds if (idFollowerRelationships.length === 0) {
const output = await client.user.findMany({ return jsonResponse([]);
where: { }
relationships: {
some: { // Find users that you follow in idFollowerRelationships
ownerId: self.id, const relevantRelationships = await db.query.relationship.findMany({
subjectId: { columns: {
in: followersOfIds.map((f) => f.id), subjectId: true,
},
following: true,
},
},
}, },
include: userRelations, where: (relationship, { inArray, and, eq }) =>
and(
eq(relationship.ownerId, self.id),
inArray(
relationship.subjectId,
idFollowerRelationships.map((f) => f.ownerId),
),
eq(relationship.following, true),
),
}); });
return jsonResponse(output.map((o) => userToAPI(o))); if (relevantRelationships.length === 0) {
return jsonResponse([]);
}
const finalUsers = await findManyUsers({
where: (user, { inArray }) =>
inArray(
user.id,
relevantRelationships.map((r) => r.subjectId),
),
});
if (finalUsers.length === 0) {
return jsonResponse([]);
}
return jsonResponse(finalUsers.map((o) => userToAPI(o)));
}); });

View file

@ -2,8 +2,7 @@ import { apiRoute, applyConfig } from "@api";
import { jsonResponse, response } from "@response"; import { jsonResponse, response } from "@response";
import { tempmailDomains } from "@tempmail"; import { tempmailDomains } from "@tempmail";
import ISO6391 from "iso-639-1"; import ISO6391 from "iso-639-1";
import { client } from "~database/datasource"; import { createNewLocalUser, findFirstUser } from "~database/entities/User";
import { createNewLocalUser } from "~database/entities/User";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -127,11 +126,16 @@ export default apiRoute<{
}); });
// Check if username is taken // Check if username is taken
if (await client.user.findFirst({ where: { username: body.username } })) if (
await findFirstUser({
where: (user, { eq }) => eq(user.username, body.username ?? ""),
})
) {
errors.details.username.push({ errors.details.username.push({
error: "ERR_TAKEN", error: "ERR_TAKEN",
description: "is already taken", description: "is already taken",
}); });
}
// Check if email is valid // Check if email is valid
if ( if (

View file

@ -1,15 +1,15 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { getUserUri } from "~database/entities/User";
import { config } from "~index";
import { import {
deleteOldTestUsers, deleteOldTestUsers,
getTestStatuses, getTestStatuses,
getTestUsers, getTestUsers,
sendTestRequest, sendTestRequest,
} from "~tests/utils"; } from "~tests/utils";
import { config } from "~index";
import { meta } from "./index";
import type { APIStatus } from "~types/entities/status";
import type { APIAccount } from "~types/entities/account"; import type { APIAccount } from "~types/entities/account";
import { getUserUri } from "~database/entities/User"; import type { APIStatus } from "~types/entities/status";
import { meta } from "./index";
await deleteOldTestUsers(); await deleteOldTestUsers();

View file

@ -1,11 +1,11 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import type { User } from "@prisma/client";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource";
import { import {
createNewRelationship, createNewRelationship,
relationshipToAPI, relationshipToAPI,
} from "~database/entities/Relationship"; } from "~database/entities/Relationship";
import type { User } from "~database/entities/User";
import { db } from "~drizzle/db";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -37,13 +37,12 @@ export default apiRoute<{
return errorResponse("Number of ids must be between 1 and 10", 422); return errorResponse("Number of ids must be between 1 and 10", 422);
} }
const relationships = await client.relationship.findMany({ const relationships = await db.query.relationship.findMany({
where: { where: (relationship, { inArray, and, eq }) =>
ownerId: self.id, and(
subjectId: { inArray(relationship.subjectId, ids),
in: ids, eq(relationship.ownerId, self.id),
}, ),
},
}); });
// Find IDs that dont have a relationship // Find IDs that dont have a relationship

View file

@ -1,15 +1,15 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { getUserUri } from "~database/entities/User";
import { config } from "~index";
import { import {
deleteOldTestUsers, deleteOldTestUsers,
getTestStatuses, getTestStatuses,
getTestUsers, getTestUsers,
sendTestRequest, sendTestRequest,
} from "~tests/utils"; } from "~tests/utils";
import { config } from "~index";
import { meta } from "./index";
import type { APIStatus } from "~types/entities/status";
import type { APIAccount } from "~types/entities/account"; import type { APIAccount } from "~types/entities/account";
import { getUserUri } from "~database/entities/User"; import type { APIStatus } from "~types/entities/status";
import { meta } from "./index";
await deleteOldTestUsers(); await deleteOldTestUsers();

View file

@ -2,10 +2,10 @@ import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { sql } from "drizzle-orm"; import { sql } from "drizzle-orm";
import { import {
type UserWithRelations,
findManyUsers, findManyUsers,
resolveWebFinger, resolveWebFinger,
userToAPI, userToAPI,
type UserWithRelations,
} from "~database/entities/User"; } from "~database/entities/User";
import { user } from "~drizzle/schema"; import { user } from "~drizzle/schema";

View file

@ -2,15 +2,16 @@ import { apiRoute, applyConfig } from "@api";
import { convertTextToHtml } from "@formatting"; import { convertTextToHtml } from "@formatting";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { sanitizeHtml } from "@sanitization"; import { sanitizeHtml } from "@sanitization";
import { and, eq } from "drizzle-orm";
import ISO6391 from "iso-639-1"; import ISO6391 from "iso-639-1";
import { MediaBackendType } from "media-manager"; import { MediaBackendType } from "media-manager";
import type { MediaBackend } from "media-manager"; import type { MediaBackend } from "media-manager";
import { client } from "~database/datasource"; import { LocalMediaBackend, S3MediaBackend } from "media-manager";
import { getUrl } from "~database/entities/Attachment"; import { getUrl } from "~database/entities/Attachment";
import { parseEmojis } from "~database/entities/Emoji"; import { parseEmojis } from "~database/entities/Emoji";
import { userToAPI } from "~database/entities/User"; import { findFirstUser, userToAPI } from "~database/entities/User";
import { userRelations } from "~database/entities/relations"; import { db } from "~drizzle/db";
import { S3MediaBackend, LocalMediaBackend } from "media-manager"; import { emojiToUser, user } from "~drizzle/schema";
import type { APISource } from "~types/entities/source"; import type { APISource } from "~types/entities/source";
export const meta = applyConfig({ export const meta = applyConfig({
@ -38,9 +39,9 @@ export default apiRoute<{
"source[sensitive]": string; "source[sensitive]": string;
"source[language]": string; "source[language]": string;
}>(async (req, matchedRoute, extraData) => { }>(async (req, matchedRoute, extraData) => {
const { user } = extraData.auth; const { user: self } = extraData.auth;
if (!user) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
const config = await extraData.configManager.getConfig(); const config = await extraData.configManager.getConfig();
@ -109,12 +110,12 @@ export default apiRoute<{
} }
// Remove emojis // Remove emojis
user.emojis = []; self.emojis = [];
user.displayName = sanitizedDisplayName; self.displayName = sanitizedDisplayName;
} }
if (note && user.source) { if (note && self.source) {
// Check if within allowed note length // Check if within allowed note length
if (sanitizedNote.length > config.validation.max_note_size) { if (sanitizedNote.length > config.validation.max_note_size) {
return errorResponse( return errorResponse(
@ -128,12 +129,12 @@ export default apiRoute<{
return errorResponse("Bio contains blocked words", 422); return errorResponse("Bio contains blocked words", 422);
} }
(user.source as APISource).note = sanitizedNote; (self.source as APISource).note = sanitizedNote;
// TODO: Convert note to HTML // TODO: Convert note to HTML
user.note = await convertTextToHtml(sanitizedNote); self.note = await convertTextToHtml(sanitizedNote);
} }
if (source_privacy && user.source) { if (source_privacy && self.source) {
// Check if within allowed privacy values // Check if within allowed privacy values
if ( if (
!["public", "unlisted", "private", "direct"].includes( !["public", "unlisted", "private", "direct"].includes(
@ -146,19 +147,19 @@ export default apiRoute<{
); );
} }
(user.source as APISource).privacy = source_privacy; (self.source as APISource).privacy = source_privacy;
} }
if (source_sensitive && user.source) { if (source_sensitive && self.source) {
// Check if within allowed sensitive values // Check if within allowed sensitive values
if (source_sensitive !== "true" && source_sensitive !== "false") { if (source_sensitive !== "true" && source_sensitive !== "false") {
return errorResponse("Sensitive must be a boolean", 422); return errorResponse("Sensitive must be a boolean", 422);
} }
(user.source as APISource).sensitive = source_sensitive === "true"; (self.source as APISource).sensitive = source_sensitive === "true";
} }
if (source_language && user.source) { if (source_language && self.source) {
if (!ISO6391.validate(source_language)) { if (!ISO6391.validate(source_language)) {
return errorResponse( return errorResponse(
"Language must be a valid ISO 639-1 code", "Language must be a valid ISO 639-1 code",
@ -166,7 +167,7 @@ export default apiRoute<{
); );
} }
(user.source as APISource).language = source_language; (self.source as APISource).language = source_language;
} }
if (avatar) { if (avatar) {
@ -180,7 +181,7 @@ export default apiRoute<{
const { path } = await mediaManager.addFile(avatar); const { path } = await mediaManager.addFile(avatar);
user.avatar = getUrl(path, config); self.avatar = getUrl(path, config);
} }
if (header) { if (header) {
@ -194,7 +195,7 @@ export default apiRoute<{
const { path } = await mediaManager.addFile(header); const { path } = await mediaManager.addFile(header);
user.header = getUrl(path, config); self.header = getUrl(path, config);
} }
if (locked) { if (locked) {
@ -203,7 +204,7 @@ export default apiRoute<{
return errorResponse("Locked must be a boolean", 422); return errorResponse("Locked must be a boolean", 422);
} }
user.isLocked = locked === "true"; self.isLocked = locked === "true";
} }
if (bot) { if (bot) {
@ -212,7 +213,7 @@ export default apiRoute<{
return errorResponse("Bot must be a boolean", 422); return errorResponse("Bot must be a boolean", 422);
} }
user.isBot = bot === "true"; self.isBot = bot === "true";
} }
if (discoverable) { if (discoverable) {
@ -221,7 +222,7 @@ export default apiRoute<{
return errorResponse("Discoverable must be a boolean", 422); return errorResponse("Discoverable must be a boolean", 422);
} }
user.isDiscoverable = discoverable === "true"; self.isDiscoverable = discoverable === "true";
} }
// Parse emojis // Parse emojis
@ -229,36 +230,49 @@ export default apiRoute<{
const displaynameEmojis = await parseEmojis(sanitizedDisplayName); const displaynameEmojis = await parseEmojis(sanitizedDisplayName);
const noteEmojis = await parseEmojis(sanitizedNote); const noteEmojis = await parseEmojis(sanitizedNote);
user.emojis = [...displaynameEmojis, ...noteEmojis]; self.emojis = [...displaynameEmojis, ...noteEmojis];
// Deduplicate emojis // Deduplicate emojis
user.emojis = user.emojis.filter( self.emojis = self.emojis.filter(
(emoji, index, self) => (emoji, index, self) =>
self.findIndex((e) => e.id === emoji.id) === index, self.findIndex((e) => e.id === emoji.id) === index,
); );
const output = await client.user.update({ await db
where: { id: user.id }, .update(user)
data: { .set({
displayName: user.displayName, displayName: self.displayName,
note: user.note, note: self.note,
avatar: user.avatar, avatar: self.avatar,
header: user.header, header: self.header,
isLocked: user.isLocked, isLocked: self.isLocked,
isBot: user.isBot, isBot: self.isBot,
isDiscoverable: user.isDiscoverable, isDiscoverable: self.isDiscoverable,
emojis: { source: self.source || undefined,
disconnect: user.emojis.map((e) => ({ })
id: e.id, .where(eq(user.id, self.id));
})),
connect: user.emojis.map((e) => ({ // Connect emojis, if any
id: e.id, for (const emoji of self.emojis) {
})), await db
}, .delete(emojiToUser)
source: user.source || undefined, .where(and(eq(emojiToUser.a, emoji.id), eq(emojiToUser.b, self.id)))
}, .execute();
include: userRelations,
await db
.insert(emojiToUser)
.values({
a: emoji.id,
b: self.id,
})
.execute();
}
const output = await findFirstUser({
where: (user, { eq }) => eq(user.id, self.id),
}); });
if (!output) return errorResponse("Couldn't edit user", 500);
return jsonResponse(userToAPI(output)); return jsonResponse(userToAPI(output));
}); });

View file

@ -1,7 +1,8 @@
import { randomBytes } from "node:crypto"; import { randomBytes } from "node:crypto";
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { db } from "~drizzle/db";
import { application } from "~drizzle/schema";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -33,24 +34,28 @@ export default apiRoute<{
return errorResponse("Redirect URI must be a valid URI", 422); return errorResponse("Redirect URI must be a valid URI", 422);
} }
} }
const application = await client.application.create({
data: { const app = (
name: client_name || "", await db
redirect_uris: redirect_uris || "", .insert(application)
scopes: scopes || "read", .values({
website: website || null, name: client_name || "",
client_id: randomBytes(32).toString("base64url"), redirectUris: redirect_uris || "",
secret: randomBytes(64).toString("base64url"), scopes: scopes || "read",
}, website: website || null,
}); clientId: randomBytes(32).toString("base64url"),
secret: randomBytes(64).toString("base64url"),
})
.returning()
)[0];
return jsonResponse({ return jsonResponse({
id: application.id, id: app.id,
name: application.name, name: app.name,
website: application.website, website: app.website,
client_id: application.client_id, client_id: app.clientId,
client_secret: application.secret, client_secret: app.secret,
redirect_uri: application.redirect_uris, redirect_uri: app.redirectUris,
vapid_link: application.vapid_key, vapid_link: app.vapidKey,
}); });
}); });

View file

@ -2,9 +2,9 @@ import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines"; import { fetchTimeline } from "@timelines";
import { import {
type UserWithRelations,
findManyUsers, findManyUsers,
userToAPI, userToAPI,
type UserWithRelations,
} from "~database/entities/User"; } from "~database/entities/User";
export const meta = applyConfig({ export const meta = applyConfig({

View file

@ -1,6 +1,5 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { jsonResponse } from "@response"; import { jsonResponse } from "@response";
import { client } from "~database/datasource";
import { emojiToAPI } from "~database/entities/Emoji"; import { emojiToAPI } from "~database/entities/Emoji";
import { db } from "~drizzle/db"; import { db } from "~drizzle/db";

View file

@ -1,12 +1,11 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines"; import { fetchTimeline } from "@timelines";
import { client } from "~database/datasource";
import { import {
statusToAPI,
type StatusWithRelations, type StatusWithRelations,
findManyStatuses,
statusToAPI,
} from "~database/entities/Status"; } from "~database/entities/Status";
import { statusAndUserRelations } from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -37,25 +36,18 @@ export default apiRoute<{
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);
const { objects, link } = await fetchTimeline<StatusWithRelations>( const { objects, link } = await fetchTimeline<StatusWithRelations>(
client.status, findManyStatuses,
{ {
where: { // @ts-ignore
id: { where: (status, { and, lt, gt, gte, eq, sql }) =>
lt: max_id ?? undefined, and(
gte: since_id ?? undefined, max_id ? lt(status.id, max_id) : undefined,
gt: min_id ?? undefined, since_id ? gte(status.id, since_id) : undefined,
}, min_id ? gt(status.id, min_id) : undefined,
likes: { sql`EXISTS (SELECT 1 FROM "Like" WHERE "Like"."likedId" = ${status.id} AND "Like"."likerId" = ${user.id})`,
some: { ),
likerId: user.id, // @ts-expect-error Yes I KNOW the types are wrong
}, orderBy: (status, { desc }) => desc(status.id),
},
},
include: statusAndUserRelations,
take: Number(limit),
orderBy: {
id: "desc",
},
}, },
req, req,
); );

View file

@ -1,12 +1,17 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { and, eq } from "drizzle-orm";
import { import {
checkForBidirectionalRelationships, checkForBidirectionalRelationships,
relationshipToAPI, relationshipToAPI,
} from "~database/entities/Relationship"; } from "~database/entities/Relationship";
import { sendFollowAccept } from "~database/entities/User"; import {
import { userRelations } from "~database/entities/relations"; findFirstUser,
getRelationshipToOtherUser,
sendFollowAccept,
} from "~database/entities/User";
import { db } from "~drizzle/db";
import { relationship } from "~drizzle/schema";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -27,11 +32,8 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
const { account_id } = matchedRoute.params; const { account_id } = matchedRoute.params;
const account = await client.user.findUnique({ const account = await findFirstUser({
where: { where: (user, { eq }) => eq(user.id, account_id),
id: account_id,
},
include: userRelations,
}); });
if (!account) return errorResponse("Account not found", 404); if (!account) return errorResponse("Account not found", 404);
@ -40,37 +42,35 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
await checkForBidirectionalRelationships(user, account); await checkForBidirectionalRelationships(user, account);
// Authorize follow request // Authorize follow request
await client.relationship.updateMany({ await db
where: { .update(relationship)
subjectId: user.id, .set({
ownerId: account.id,
requested: true,
},
data: {
requested: false, requested: false,
following: true, following: true,
}, })
}); .where(
and(
eq(relationship.subjectId, user.id),
eq(relationship.ownerId, account.id),
),
);
// Update followedBy for other user // Update followedBy for other user
await client.relationship.updateMany({ await db
where: { .update(relationship)
subjectId: account.id, .set({
ownerId: user.id,
},
data: {
followedBy: true, followedBy: true,
}, })
}); .where(
and(
eq(relationship.subjectId, account.id),
eq(relationship.ownerId, user.id),
),
);
const relationship = await client.relationship.findFirst({ const foundRelationship = await getRelationshipToOtherUser(user, account);
where: {
subjectId: account.id,
ownerId: user.id,
},
});
if (!relationship) return errorResponse("Relationship not found", 404); if (!foundRelationship) return errorResponse("Relationship not found", 404);
// Check if accepting remote follow // Check if accepting remote follow
if (account.instanceId) { if (account.instanceId) {
@ -78,5 +78,5 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
await sendFollowAccept(account, user); await sendFollowAccept(account, user);
} }
return jsonResponse(relationshipToAPI(relationship)); return jsonResponse(relationshipToAPI(foundRelationship));
}); });

View file

@ -1,12 +1,17 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { and, eq } from "drizzle-orm";
import { import {
checkForBidirectionalRelationships, checkForBidirectionalRelationships,
relationshipToAPI, relationshipToAPI,
} from "~database/entities/Relationship"; } from "~database/entities/Relationship";
import { sendFollowReject } from "~database/entities/User"; import {
import { userRelations } from "~database/entities/relations"; findFirstUser,
getRelationshipToOtherUser,
sendFollowReject,
} from "~database/entities/User";
import { db } from "~drizzle/db";
import { relationship } from "~drizzle/schema";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -27,11 +32,8 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
const { account_id } = matchedRoute.params; const { account_id } = matchedRoute.params;
const account = await client.user.findUnique({ const account = await findFirstUser({
where: { where: (user, { eq }) => eq(user.id, account_id),
id: account_id,
},
include: userRelations,
}); });
if (!account) return errorResponse("Account not found", 404); if (!account) return errorResponse("Account not found", 404);
@ -40,25 +42,35 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
await checkForBidirectionalRelationships(user, account); await checkForBidirectionalRelationships(user, account);
// Reject follow request // Reject follow request
await client.relationship.updateMany({ await db
where: { .update(relationship)
subjectId: user.id, .set({
ownerId: account.id,
requested: true,
},
data: {
requested: false, requested: false,
}, following: false,
}); })
.where(
and(
eq(relationship.subjectId, user.id),
eq(relationship.ownerId, account.id),
),
);
const relationship = await client.relationship.findFirst({ // Update followedBy for other user
where: { await db
subjectId: account.id, .update(relationship)
ownerId: user.id, .set({
}, followedBy: false,
}); })
.where(
and(
eq(relationship.subjectId, account.id),
eq(relationship.ownerId, user.id),
),
);
if (!relationship) return errorResponse("Relationship not found", 404); const foundRelationship = await getRelationshipToOtherUser(user, account);
if (!foundRelationship) return errorResponse("Relationship not found", 404);
// Check if rejecting remote follow // Check if rejecting remote follow
if (account.instanceId) { if (account.instanceId) {
@ -66,5 +78,5 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
await sendFollowReject(account, user); await sendFollowReject(account, user);
} }
return jsonResponse(relationshipToAPI(relationship)); return jsonResponse(relationshipToAPI(foundRelationship));
}); });

View file

@ -2,9 +2,9 @@ import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines"; import { fetchTimeline } from "@timelines";
import { import {
type UserWithRelations,
findManyUsers, findManyUsers,
userToAPI, userToAPI,
type UserWithRelations,
} from "~database/entities/User"; } from "~database/entities/User";
export const meta = applyConfig({ export const meta = applyConfig({

View file

@ -1,11 +1,9 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { jsonResponse } from "@response"; import { jsonResponse } from "@response";
import { count, isNull } from "drizzle-orm"; import { and, count, countDistinct, eq, gte, isNull } from "drizzle-orm";
import { client } from "~database/datasource"; import { findFirstUser, userToAPI } from "~database/entities/User";
import { userToAPI } from "~database/entities/User";
import { userRelations } from "~database/entities/relations";
import { db } from "~drizzle/db"; import { db } from "~drizzle/db";
import { status, user } from "~drizzle/schema"; import { instance, status, user } from "~drizzle/schema";
import manifest from "~package.json"; import manifest from "~package.json";
import type { APIInstance } from "~types/entities/instance"; import type { APIInstance } from "~types/entities/instance";
@ -45,33 +43,39 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
.where(isNull(user.instanceId)) .where(isNull(user.instanceId))
)[0].count; )[0].count;
// Get the first created admin user const contactAccount = await findFirstUser({
const contactAccount = await client.user.findFirst({ where: (user, { isNull, eq, and }) =>
where: { and(isNull(user.instanceId), eq(user.isAdmin, true)),
instanceId: null, orderBy: (user, { asc }) => asc(user.id),
isAdmin: true,
},
orderBy: {
id: "asc",
},
include: userRelations,
}); });
// Get user that have posted once in the last 30 days const monthlyActiveUsers = (
const monthlyActiveUsers = await client.user.count({ await db
where: { .select({
instanceId: null, count: countDistinct(user),
statuses: { })
some: { .from(user)
createdAt: { .leftJoin(status, eq(user.id, status.authorId))
gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), .where(
}, and(
}, isNull(user.instanceId),
}, gte(
}, status.createdAt,
}); new Date(
Date.now() - 30 * 24 * 60 * 60 * 1000,
).toISOString(),
),
),
)
)[0].count;
const knownDomainsCount = await client.instance.count(); const knownDomainsCount = (
await db
.select({
count: count(),
})
.from(instance)
)[0].count;
// TODO: fill in more values // TODO: fill in more values
return jsonResponse({ return jsonResponse({

View file

@ -1,9 +1,11 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse, response } from "@response"; import { errorResponse, jsonResponse, response } from "@response";
import { eq } from "drizzle-orm";
import type { MediaBackend } from "media-manager"; import type { MediaBackend } from "media-manager";
import { MediaBackendType } from "media-manager"; import { MediaBackendType } from "media-manager";
import { client } from "~database/datasource";
import { attachmentToAPI, getUrl } from "~database/entities/Attachment"; import { attachmentToAPI, getUrl } from "~database/entities/Attachment";
import { db } from "~drizzle/db";
import { attachment } from "~drizzle/schema";
import { LocalMediaBackend, S3MediaBackend } from "~packages/media-manager"; import { LocalMediaBackend, S3MediaBackend } from "~packages/media-manager";
export const meta = applyConfig({ export const meta = applyConfig({
@ -35,13 +37,11 @@ export default apiRoute<{
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const attachment = await client.attachment.findUnique({ const foundAttachment = await db.query.attachment.findFirst({
where: { where: (attachment, { eq }) => eq(attachment.id, id),
id,
},
}); });
if (!attachment) { if (!foundAttachment) {
return errorResponse("Media not found", 404); return errorResponse("Media not found", 404);
} }
@ -49,15 +49,15 @@ export default apiRoute<{
switch (req.method) { switch (req.method) {
case "GET": { case "GET": {
if (attachment.url) { if (foundAttachment.url) {
return jsonResponse(attachmentToAPI(attachment)); return jsonResponse(attachmentToAPI(foundAttachment));
} }
return response(null, 206); return response(null, 206);
} }
case "PUT": { case "PUT": {
const { description, thumbnail } = extraData.parsedRequest; const { description, thumbnail } = extraData.parsedRequest;
let thumbnailUrl = attachment.thumbnail_url; let thumbnailUrl = foundAttachment.thumbnailUrl;
let mediaManager: MediaBackend; let mediaManager: MediaBackend;
@ -78,26 +78,27 @@ export default apiRoute<{
thumbnailUrl = getUrl(path, config); thumbnailUrl = getUrl(path, config);
} }
const descriptionText = description || attachment.description; const descriptionText = description || foundAttachment.description;
if ( if (
descriptionText !== attachment.description || descriptionText !== foundAttachment.description ||
thumbnailUrl !== attachment.thumbnail_url thumbnailUrl !== foundAttachment.thumbnailUrl
) { ) {
const newAttachment = await client.attachment.update({ const newAttachment = (
where: { await db
id, .update(attachment)
}, .set({
data: { description: descriptionText,
description: descriptionText, thumbnailUrl,
thumbnail_url: thumbnailUrl, })
}, .where(eq(attachment.id, id))
}); .returning()
)[0];
return jsonResponse(attachmentToAPI(newAttachment)); return jsonResponse(attachmentToAPI(newAttachment));
} }
return jsonResponse(attachmentToAPI(attachment)); return jsonResponse(attachmentToAPI(foundAttachment));
} }
} }

View file

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

View file

@ -1,13 +1,11 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines"; import { fetchTimeline } from "@timelines";
import { client } from "~database/datasource";
import { import {
type UserWithRelations,
findManyUsers, findManyUsers,
userToAPI, userToAPI,
type UserWithRelations,
} from "~database/entities/User"; } from "~database/entities/User";
import { userRelations } from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],

View file

@ -1,20 +1,11 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines"; import { fetchTimeline } from "@timelines";
import { client } from "~database/datasource";
import { import {
findManyNotifications, findManyNotifications,
notificationToAPI, notificationToAPI,
} from "~database/entities/Notification"; } from "~database/entities/Notification";
import { import type { NotificationWithRelations } from "~database/entities/Notification";
statusAndUserRelations,
userRelations,
} from "~database/entities/relations";
import type {
Notification,
NotificationWithRelations,
} from "~database/entities/Notification";
import { db } from "~drizzle/db";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],

View file

@ -1,13 +1,12 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource";
import { createLike } from "~database/entities/Like"; import { createLike } from "~database/entities/Like";
import { import {
findFirstStatuses, findFirstStatuses,
isViewableByUser, isViewableByUser,
statusToAPI, statusToAPI,
} from "~database/entities/Status"; } from "~database/entities/Status";
import { statusAndUserRelations } from "~database/entities/relations"; import { db } from "~drizzle/db";
import type { APIStatus } from "~types/entities/status"; import type { APIStatus } from "~types/entities/status";
export const meta = applyConfig({ export const meta = applyConfig({
@ -40,11 +39,9 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
if (!status || !isViewableByUser(status, user)) if (!status || !isViewableByUser(status, user))
return errorResponse("Record not found", 404); return errorResponse("Record not found", 404);
const existingLike = await client.like.findFirst({ const existingLike = await db.query.like.findFirst({
where: { where: (like, { and, eq }) =>
likedId: status.id, and(eq(like.likedId, status.id), eq(like.likerId, user.id)),
likerId: user.id,
},
}); });
if (!existingLike) { if (!existingLike) {

View file

@ -1,14 +1,13 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { config } from "~index";
import { import {
deleteOldTestUsers, deleteOldTestUsers,
getTestStatuses, getTestStatuses,
getTestUsers, getTestUsers,
sendTestRequest, sendTestRequest,
} from "~tests/utils"; } from "~tests/utils";
import { config } from "~index";
import { meta } from "./favourited_by";
import type { APIStatus } from "~types/entities/status";
import type { APIAccount } from "~types/entities/account"; import type { APIAccount } from "~types/entities/account";
import { meta } from "./favourited_by";
await deleteOldTestUsers(); await deleteOldTestUsers();
@ -21,7 +20,7 @@ afterAll(async () => {
beforeAll(async () => { beforeAll(async () => {
for (const status of timeline) { for (const status of timeline) {
await fetch( const res = await fetch(
new URL( new URL(
`/api/v1/statuses/${status.id}/favourite`, `/api/v1/statuses/${status.id}/favourite`,
config.http.base_url, config.http.base_url,
@ -29,6 +28,7 @@ beforeAll(async () => {
{ {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokens[1].accessToken}`, Authorization: `Bearer ${tokens[1].accessToken}`,
}, },
}, },

View file

@ -3,9 +3,9 @@ import { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines"; import { fetchTimeline } from "@timelines";
import { findFirstStatuses, isViewableByUser } from "~database/entities/Status"; import { findFirstStatuses, isViewableByUser } from "~database/entities/Status";
import { import {
type UserWithRelations,
findManyUsers, findManyUsers,
userToAPI, userToAPI,
type UserWithRelations,
} from "~database/entities/User"; } from "~database/entities/User";
export const meta = applyConfig({ export const meta = applyConfig({

View file

@ -1,14 +1,14 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { config } from "~index";
import { import {
deleteOldTestUsers, deleteOldTestUsers,
getTestStatuses, getTestStatuses,
getTestUsers, getTestUsers,
sendTestRequest, sendTestRequest,
} from "~tests/utils"; } from "~tests/utils";
import { config } from "~index";
import { meta } from "./reblogged_by";
import type { APIStatus } from "~types/entities/status";
import type { APIAccount } from "~types/entities/account"; import type { APIAccount } from "~types/entities/account";
import type { APIStatus } from "~types/entities/status";
import { meta } from "./reblogged_by";
await deleteOldTestUsers(); await deleteOldTestUsers();
@ -29,6 +29,7 @@ beforeAll(async () => {
{ {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokens[1].accessToken}`, Authorization: `Bearer ${tokens[1].accessToken}`,
}, },
}, },

View file

@ -4,8 +4,8 @@ import { fetchTimeline } from "@timelines";
import { findFirstStatuses, isViewableByUser } from "~database/entities/Status"; import { findFirstStatuses, isViewableByUser } from "~database/entities/Status";
import { import {
type UserWithRelations, type UserWithRelations,
userToAPI,
findManyUsers, findManyUsers,
userToAPI,
} from "~database/entities/User"; } from "~database/entities/User";
export const meta = applyConfig({ export const meta = applyConfig({

View file

@ -1,12 +1,12 @@
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import { config } from "~index";
import { import {
deleteOldTestUsers, deleteOldTestUsers,
getTestUsers, getTestUsers,
sendTestRequest, sendTestRequest,
} from "~tests/utils"; } from "~tests/utils";
import { config } from "~index";
import { meta } from "./index";
import type { APIStatus } from "~types/entities/status"; import type { APIStatus } from "~types/entities/status";
import { meta } from "./index";
await deleteOldTestUsers(); await deleteOldTestUsers();

View file

@ -2,8 +2,6 @@ import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { sanitizeHtml } from "@sanitization"; import { sanitizeHtml } from "@sanitization";
import { parse } from "marked"; import { parse } from "marked";
import { client } from "~database/datasource";
import { getFromToken } from "~database/entities/Application";
import type { StatusWithRelations } from "~database/entities/Status"; import type { StatusWithRelations } from "~database/entities/Status";
import { import {
createNewStatus, createNewStatus,
@ -12,10 +10,7 @@ import {
parseTextMentions, parseTextMentions,
statusToAPI, statusToAPI,
} from "~database/entities/Status"; } from "~database/entities/Status";
import type { UserWithRelations } from "~database/entities/User";
import { statusAndUserRelations } from "~database/entities/relations";
import { db } from "~drizzle/db"; import { db } from "~drizzle/db";
import type { APIStatus } from "~types/entities/status";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],

View file

@ -1,13 +1,13 @@
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import { config } from "~index";
import { import {
deleteOldTestUsers, deleteOldTestUsers,
getTestStatuses, getTestStatuses,
getTestUsers, getTestUsers,
sendTestRequest, sendTestRequest,
} from "~tests/utils"; } from "~tests/utils";
import { config } from "~index";
import { meta } from "./home";
import type { APIStatus } from "~types/entities/status"; import type { APIStatus } from "~types/entities/status";
import { meta } from "./home";
await deleteOldTestUsers(); await deleteOldTestUsers();

View file

@ -3,8 +3,8 @@ import { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines"; import { fetchTimeline } from "@timelines";
import { import {
type StatusWithRelations, type StatusWithRelations,
statusToAPI,
findManyStatuses, findManyStatuses,
statusToAPI,
} from "~database/entities/Status"; } from "~database/entities/Status";
import { db } from "~drizzle/db"; import { db } from "~drizzle/db";

View file

@ -1,13 +1,13 @@
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import { config } from "~index";
import { import {
deleteOldTestUsers, deleteOldTestUsers,
getTestStatuses, getTestStatuses,
getTestUsers, getTestUsers,
sendTestRequest, sendTestRequest,
} from "~tests/utils"; } from "~tests/utils";
import { config } from "~index";
import { meta } from "./public";
import type { APIStatus } from "~types/entities/status"; import type { APIStatus } from "~types/entities/status";
import { meta } from "./public";
await deleteOldTestUsers(); await deleteOldTestUsers();

View file

@ -3,9 +3,9 @@ import { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines"; import { fetchTimeline } from "@timelines";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { import {
type StatusWithRelations,
findManyStatuses, findManyStatuses,
statusToAPI, statusToAPI,
type StatusWithRelations,
} from "~database/entities/Status"; } from "~database/entities/Status";
import { statusAndUserRelations } from "~database/entities/relations"; import { statusAndUserRelations } from "~database/entities/relations";

View file

@ -1,10 +1,10 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import type * as Lysand from "lysand-types";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { statusToLysand } from "~database/entities/Status";
import { userToLysand } from "~database/entities/User"; import { userToLysand } from "~database/entities/User";
import { statusAndUserRelations } from "~database/entities/relations"; import { statusAndUserRelations } from "~database/entities/relations";
import type * as Lysand from "lysand-types";
import { statusToLysand } from "~database/entities/Status";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],

View file

@ -1,17 +1,17 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, response } from "@response"; import { errorResponse, response } from "@response";
import { client } from "~database/datasource";
import { userRelations } from "~database/entities/relations";
import type * as Lysand from "lysand-types"; import type * as Lysand from "lysand-types";
import { client } from "~database/datasource";
import { objectToInboxRequest } from "~database/entities/Federation";
import { createNewStatus, resolveStatus } from "~database/entities/Status"; import { createNewStatus, resolveStatus } from "~database/entities/Status";
import type { APIStatus } from "~types/entities/status";
import { import {
followAcceptToLysand, followAcceptToLysand,
getRelationshipToOtherUser, getRelationshipToOtherUser,
resolveUser, resolveUser,
sendFollowAccept, sendFollowAccept,
} from "~database/entities/User"; } from "~database/entities/User";
import { objectToInboxRequest } from "~database/entities/Federation"; import { userRelations } from "~database/entities/relations";
import type { APIStatus } from "~types/entities/status";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],

View file

@ -1,18 +1,18 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import type { Token } from "@prisma/client"; import type { Token } from "@prisma/client";
import { config } from "config-manager"; import { config } from "config-manager";
import { inArray } from "drizzle-orm";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { TokenType } from "~database/entities/Token"; import { TokenType } from "~database/entities/Token";
import { import {
type UserWithRelations, type UserWithRelations,
createNewLocalUser, createNewLocalUser,
} from "~database/entities/User"; } from "~database/entities/User";
import { db } from "~drizzle/db";
import { application, user } from "~drizzle/schema";
import type { APIEmoji } from "~types/entities/emoji"; import type { APIEmoji } from "~types/entities/emoji";
import type { APIInstance } from "~types/entities/instance"; import type { APIInstance } from "~types/entities/instance";
import { sendTestRequest, wrapRelativeUrl } from "./utils"; import { sendTestRequest, wrapRelativeUrl } from "./utils";
import { db } from "~drizzle/db";
import { inArray } from "drizzle-orm";
import { application, user } from "~drizzle/schema";
const base_url = config.http.base_url; const base_url = config.http.base_url;

View file

@ -1,14 +1,14 @@
import {
createNewLocalUser,
type User,
type UserWithRelations,
} from "~database/entities/User";
import { randomBytes } from "node:crypto"; import { randomBytes } from "node:crypto";
import { server } from "~index";
import { db } from "~drizzle/db";
import { status, token, user } from "~drizzle/schema";
import { inArray, like } from "drizzle-orm"; import { inArray, like } from "drizzle-orm";
import type { Status } from "~database/entities/Status"; import type { Status } from "~database/entities/Status";
import {
type User,
type UserWithRelations,
createNewLocalUser,
} from "~database/entities/User";
import { db } from "~drizzle/db";
import { status, token, user } from "~drizzle/schema";
import { server } from "~index";
/** /**
* This allows us to send a test request to the server even when it isnt running * This allows us to send a test request to the server even when it isnt running

View file

@ -1,11 +1,11 @@
import type { findManyStatuses, Status } from "~database/entities/Status";
import type { findManyUsers, User } from "~database/entities/User";
import type {
findManyNotifications,
Notification,
} from "~database/entities/Notification";
import type { db } from "~drizzle/db";
import { config } from "config-manager"; import { config } from "config-manager";
import type {
Notification,
findManyNotifications,
} from "~database/entities/Notification";
import type { Status, findManyStatuses } from "~database/entities/Status";
import type { User, findManyUsers } from "~database/entities/User";
import type { db } from "~drizzle/db";
export async function fetchTimeline<T extends User | Status | Notification>( export async function fetchTimeline<T extends User | Status | Notification>(
model: model: