From 4c3a4907b1e835510c4411cdde28861f16d2d248 Mon Sep 17 00:00:00 2001 From: aliceif <7098860+aliceif@users.noreply.github.com> Date: Sat, 18 Apr 2026 20:11:54 +0200 Subject: [PATCH 01/16] add thumbnail_cleanup page --- src/components/DashboardLayout.tsx | 16 +- src/pages/index.tsx | 2 + src/pages/thumbnail_cleanup.tsx | 745 +++++++++++++++++++++++++++++ 3 files changed, 762 insertions(+), 1 deletion(-) create mode 100644 src/pages/thumbnail_cleanup.tsx diff --git a/src/components/DashboardLayout.tsx b/src/components/DashboardLayout.tsx index b315fbd1..4aea2660 100644 --- a/src/components/DashboardLayout.tsx +++ b/src/components/DashboardLayout.tsx @@ -3,7 +3,12 @@ import type { PropsWithChildren } from "hono/jsx"; import metadata from "../../package.json"; import { Layout, type LayoutProps } from "./Layout"; -export type Menu = "accounts" | "emojis" | "federation" | "auth"; +export type Menu = + | "accounts" + | "emojis" + | "federation" + | "thumbnail_cleanup" + | "auth"; export interface DashboardLayoutProps extends LayoutProps { selectedMenu?: Menu; @@ -61,6 +66,15 @@ export function DashboardLayout( Federation )} +
  • + {props.selectedMenu === "thumbnail_cleanup" ? ( + + Thumbnail Cleanup + + ) : ( + Thumbnail Cleanup + )} +
  • {props.selectedMenu === "auth" ? ( diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 919c0e4e..a0cb32dd 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -11,6 +11,7 @@ import logout from "./logout"; import profile from "./profile"; import setup from "./setup"; import tags from "./tags"; +import thumbnail_cleanup from "./thumbnail_cleanup"; const page = new Hono(); @@ -24,6 +25,7 @@ page.route("/auth", auth); page.route("/accounts", accounts); page.route("/emojis", emojis); page.route("/federation", federation); +page.route("/thumbnail_cleanup", thumbnail_cleanup); page.route("/tags", tags); export default page; diff --git a/src/pages/thumbnail_cleanup.tsx b/src/pages/thumbnail_cleanup.tsx new file mode 100644 index 00000000..88cf1366 --- /dev/null +++ b/src/pages/thumbnail_cleanup.tsx @@ -0,0 +1,745 @@ +import { Temporal } from "@js-temporal/polyfill"; +import { getLogger } from "@logtape/logtape"; +import { and, count, eq, exists, ilike, lt, notExists } from "drizzle-orm"; +import { alias } from "drizzle-orm/pg-core"; +import { Hono } from "hono"; + +import { DashboardLayout } from "../components/DashboardLayout"; +import db from "../db"; +import { loginRequired } from "../login"; +import { + accountOwners, + accounts, + bookmarks, + likes, + media, + posts, + reactions, +} from "../schema"; +import { drive } from "../storage"; +import { STORAGE_URL_BASE } from "../storage-config"; +import type { Uuid } from "../uuid"; + +const logger = getLogger(["hollo", "pages", "thumbnail_cleanup"]); + +const data = new Hono(); + +data.use(loginRequired); + +data.get("/", async (c) => { + const done = c.req.query("done"); + const error = c.req.query("error"); + const before = c.req.query("before"); + const fileCount = c.req.query("fileCount"); + const firstFile = c.req.query("firstFile"); + const lastFile = c.req.query("lastFile"); + const todo = c.req.query("todo"); + const processed = c.req.query("processed"); + const deleted = c.req.query("deleted"); + + const suggestedCleanupCutoff = + typeof before === "string" + ? before + : new Date(new Date().getFullYear() - 1, 0, 1) + .toISOString() + .split("T")[0]; + + const sharingPosts = alias(posts, "sharingPosts"); + const quotingPosts = alias(posts, "quotingPosts"); + + let thumbnailsBeforeLastYearAndOnlyMaybeRepliedResult: { count: number }[]; + try { + thumbnailsBeforeLastYearAndOnlyMaybeRepliedResult = await db + .select({ + count: count(), + }) + .from(media) + .innerJoin(posts, eq(media.postId, posts.id)) + .innerJoin(accounts, eq(posts.accountId, accounts.id)) + .where( + and( + ilike(media.thumbnailUrl, `${STORAGE_URL_BASE}%`), + lt(media.created, new Date(new Date().getFullYear() - 1, 0, 1)), + notExists( + db + .select() + .from(accountOwners) + .where(eq(accounts.id, accountOwners.id)), + ), + notExists( + db.select().from(bookmarks).where(eq(posts.id, bookmarks.postId)), + ), + notExists( + db + .select() + .from(likes) + .where( + and( + eq(posts.id, likes.postId), + exists( + db + .select() + .from(accountOwners) + .where(eq(likes.accountId, accountOwners.id)), + ), + ), + ), + ), + notExists( + db + .select() + .from(reactions) + .where( + and( + eq(posts.id, reactions.postId), + exists( + db + .select() + .from(accountOwners) + .where(eq(reactions.accountId, accountOwners.id)), + ), + ), + ), + ), + notExists( + db + .select() + .from(sharingPosts) + .where( + and( + eq(posts.id, sharingPosts.sharingId), + exists( + db + .select() + .from(accountOwners) + .where(eq(sharingPosts.accountId, accountOwners.id)), + ), + ), + ), + ), + notExists( + db + .select() + .from(quotingPosts) + .where( + and( + eq(posts.id, quotingPosts.quoteTargetId), + exists( + db + .select() + .from(accountOwners) + .where(eq(quotingPosts.accountId, accountOwners.id)), + ), + ), + ), + ), + ), + ); + } catch { + thumbnailsBeforeLastYearAndOnlyMaybeRepliedResult = [{ count: 0 }]; + } + const thumbnailsBeforeLastYearAndOnlyMaybeRepliedCount = + thumbnailsBeforeLastYearAndOnlyMaybeRepliedResult[0].count; + + let thumbnailsBeforeLastYearResult: { count: number }[]; + try { + thumbnailsBeforeLastYearResult = await db + .select({ + count: count(), + }) + .from(media) + .innerJoin(posts, eq(media.postId, posts.id)) + .innerJoin(accounts, eq(posts.accountId, accounts.id)) + .where( + and( + ilike(media.thumbnailUrl, `${STORAGE_URL_BASE}%`), + lt(media.created, new Date(new Date().getFullYear() - 1, 0, 1)), + notExists( + db + .select() + .from(accountOwners) + .where(eq(accounts.id, accountOwners.id)), + ), + ), + ); + } catch { + thumbnailsBeforeLastYearResult = [{ count: 0 }]; + } + const thumbnailsBeforeLastYearCount = thumbnailsBeforeLastYearResult[0].count; + + const oneYearAgo = new Date( + Temporal.Now.zonedDateTimeISO().subtract(new Temporal.Duration(1)) + .epochMilliseconds, + ); + + let thumbnailsYearOldAndOnlyMaybeRepliedResult: { count: number }[]; + try { + thumbnailsYearOldAndOnlyMaybeRepliedResult = await db + .select({ + count: count(), + }) + .from(media) + .innerJoin(posts, eq(media.postId, posts.id)) + .innerJoin(accounts, eq(posts.accountId, accounts.id)) + .where( + and( + ilike(media.thumbnailUrl, `${STORAGE_URL_BASE}%`), + lt(media.created, oneYearAgo), + notExists( + db + .select() + .from(accountOwners) + .where(eq(accounts.id, accountOwners.id)), + ), + notExists( + db.select().from(bookmarks).where(eq(posts.id, bookmarks.postId)), + ), + notExists( + db + .select() + .from(likes) + .where( + and( + eq(posts.id, likes.postId), + exists( + db + .select() + .from(accountOwners) + .where(eq(likes.accountId, accountOwners.id)), + ), + ), + ), + ), + notExists( + db + .select() + .from(reactions) + .where( + and( + eq(posts.id, reactions.postId), + exists( + db + .select() + .from(accountOwners) + .where(eq(reactions.accountId, accountOwners.id)), + ), + ), + ), + ), + notExists( + db + .select() + .from(sharingPosts) + .where( + and( + eq(posts.id, sharingPosts.sharingId), + exists( + db + .select() + .from(accountOwners) + .where(eq(sharingPosts.accountId, accountOwners.id)), + ), + ), + ), + ), + notExists( + db + .select() + .from(quotingPosts) + .where( + and( + eq(posts.id, quotingPosts.quoteTargetId), + exists( + db + .select() + .from(accountOwners) + .where(eq(quotingPosts.accountId, accountOwners.id)), + ), + ), + ), + ), + ), + ); + } catch { + thumbnailsYearOldAndOnlyMaybeRepliedResult = [{ count: 0 }]; + } + const thumbnailsYearOldAndOnlyMaybeRepliedCount = + thumbnailsYearOldAndOnlyMaybeRepliedResult[0].count; + + let thumbnailsYearOldResult: { count: number }[]; + try { + thumbnailsYearOldResult = await db + .select({ + count: count(), + }) + .from(media) + .innerJoin(posts, eq(media.postId, posts.id)) + .innerJoin(accounts, eq(posts.accountId, accounts.id)) + .where( + and( + ilike(media.thumbnailUrl, `${STORAGE_URL_BASE}%`), + lt(media.created, oneYearAgo), + notExists( + db + .select() + .from(accountOwners) + .where(eq(accounts.id, accountOwners.id)), + ), + ), + ); + } catch { + thumbnailsYearOldResult = [{ count: 0 }]; + } + const thumbnailsYearOldCount = thumbnailsYearOldResult[0].count; + + let remoteThumbnailsResult: { count: number }[]; + try { + remoteThumbnailsResult = await db + .select({ + count: count(), + }) + .from(media) + .innerJoin(posts, eq(media.postId, posts.id)) + .innerJoin(accounts, eq(posts.accountId, accounts.id)) + .where( + and( + ilike(media.thumbnailUrl, `${STORAGE_URL_BASE}%`), + notExists( + db + .select() + .from(accountOwners) + .where(eq(accounts.id, accountOwners.id)), + ), + ), + ); + } catch { + remoteThumbnailsResult = [{ count: 0 }]; + } + const thumbnailsRemoteCount = remoteThumbnailsResult[0].count; + + let thumbnailsCountResult: { count: number }[]; + try { + thumbnailsCountResult = await db + .select({ + count: count(), + }) + .from(media) + .where(ilike(media.thumbnailUrl, `${STORAGE_URL_BASE}%`)); + } catch { + thumbnailsCountResult = [{ count: 0 }]; + } + const thumbnailsCount = thumbnailsCountResult[0].count; + + const thumbnailsTable: { caption: string; count: number }[] = [ + { caption: "Total, thumbnail hosted locally", count: thumbnailsCount }, + { + caption: "Remote, thumbnail hosted locally", + count: thumbnailsRemoteCount, + }, + { + caption: "From before 1 year ago, remote, thumbnail hosted locally", + count: thumbnailsYearOldCount, + }, + { + caption: + "From before 1 year ago, remote, thumbnail hosted locally, not interacted with outside of maybe replying", + count: thumbnailsYearOldAndOnlyMaybeRepliedCount, + }, + { + caption: "From before last year, remote, thumbnail hosted locally", + count: thumbnailsBeforeLastYearCount, + }, + { + caption: + "From before last year, remote, thumbnail hosted locally, not interacted with outside of maybe replying", + count: thumbnailsBeforeLastYearAndOnlyMaybeRepliedCount, + }, + ]; + + return c.html( + +
    +

    Thumbnail Cleanup

    +

    This control panel allows you to clean up thumbnails.

    +
    + +
    +
    +
    +

    Thumbnail statistics

    +

    An overview about the number of thumbnails tracked by hollo.

    +
    +
    + + + + + + + + + {thumbnailsTable.map((entry) => ( + + + + + ))} + +
    TypeNumber of thumbnails
    {entry.caption} + {entry.count.toLocaleString("en")} +
    +
    +
    +
    +
    +

    Preview cleanup

    + {done === "clean_preview" ? ( +

    Preview done.

    + ) : ( +

    Use this to preview the cleanup.

    + )} +
    +
    +
    +
    + + +
    + {error === "clean_preview" ? ( + Something went wrong while cleaning up. + ) : ( + The date before which remote thumbnails get deleted. + )} +
    + {done === "clean_preview" && ( +

    + Number of Items: {fileCount} +
    + First: {firstFile} +
    + Last: {lastFile} +
    +

    + )} +
    +
    +
    +
    +

    Clean up thumbnails

    + {done === "clean" ? ( +

    Thumbnails have been cleaned up.

    + ) : ( +

    + Use this when you want to free up storage by deleting old + thumbnails. +

    + )} +
    +
    +
    +
    + + +
    + {error === "clean" ? ( + Something went wrong while cleaning up. + ) : ( + The date before which remote thumbnails get deleted. + )} +
    + {(done === "clean" || error === "clean") && ( +

    + Number of Items in Range: {todo} +
    + Processed: {processed} +
    + Actually deleted: {deleted} +
    +

    + )} +
    +
    , + ); +}); + +function readFilesToDelete( + before: Date, + keyPrefix: string, +): Promise<{ id: Uuid; thumbnailUrl: string; created: Date }[]> { + const sharingPosts = alias(posts, "sharingPosts"); + const quotingPosts = alias(posts, "quotingPosts"); + + return db + .select({ + id: media.id, + thumbnailUrl: media.thumbnailUrl, + created: media.created, + }) + .from(media) + .innerJoin(posts, eq(media.postId, posts.id)) + .innerJoin(accounts, eq(posts.accountId, accounts.id)) + .where( + and( + ilike(media.thumbnailUrl, `${keyPrefix}%`), + lt(media.created, before), + notExists( + db + .select() + .from(accountOwners) + .where(eq(accounts.id, accountOwners.id)), + ), + notExists( + db.select().from(bookmarks).where(eq(posts.id, bookmarks.postId)), + ), + notExists( + db + .select() + .from(likes) + .where( + and( + eq(posts.id, likes.postId), + exists( + db + .select() + .from(accountOwners) + .where(eq(likes.accountId, accountOwners.id)), + ), + ), + ), + ), + notExists( + db + .select() + .from(reactions) + .where( + and( + eq(posts.id, reactions.postId), + exists( + db + .select() + .from(accountOwners) + .where(eq(reactions.accountId, accountOwners.id)), + ), + ), + ), + ), + notExists( + db + .select() + .from(sharingPosts) + .where( + and( + eq(posts.id, sharingPosts.sharingId), + exists( + db + .select() + .from(accountOwners) + .where(eq(sharingPosts.accountId, accountOwners.id)), + ), + ), + ), + ), + notExists( + db + .select() + .from(quotingPosts) + .where( + and( + eq(posts.id, quotingPosts.quoteTargetId), + exists( + db + .select() + .from(accountOwners) + .where(eq(quotingPosts.accountId, accountOwners.id)), + ), + ), + ), + ), + ), + ) + .orderBy(media.created); +} + +data.post("/clean_preview", async (c) => { + const form = await c.req.formData(); + var beforeParameter = form.get("before"); + if (typeof beforeParameter === "string") { + const before = new Date(Date.parse(beforeParameter)); + const owner = await db.query.accountOwners.findFirst({}); + if (owner != null && STORAGE_URL_BASE !== undefined) { + logger.info(`Starting cleanup preview - before: ${before.toISOString()}`); + try { + const mediaToDelete: { id: Uuid; key: string | null; created: Date }[] = + (await readFilesToDelete(before, STORAGE_URL_BASE)).map((row) => ({ + id: row.id, + key: row.thumbnailUrl.startsWith(STORAGE_URL_BASE as string) + ? row.thumbnailUrl.replace(STORAGE_URL_BASE as string, "") + : null, + created: row.created, + })); + + logger.info(`would be about to delete ${mediaToDelete.length} files!`); + const firstItem = mediaToDelete[0]; + const lastItem = mediaToDelete[mediaToDelete.length - 1]; + logger.info( + `first file would have id ${firstItem.id}, key ${firstItem.key}, created at ${firstItem.created}`, + ); + logger.info( + `last file would have id ${lastItem.id}, key ${lastItem.key}, created at ${lastItem.created}`, + ); + const doneUrl: URL = new URL( + "/thumbnail_cleanup", + new URL(c.req.url).origin, + ); + doneUrl.searchParams.set("done", "clean_preview"); + doneUrl.searchParams.set("before", beforeParameter); + doneUrl.searchParams.set( + "fileCount", + mediaToDelete.length.toLocaleString("en"), + ); + doneUrl.searchParams.set( + "firstFile", + firstItem.created.toLocaleString(), + ); + doneUrl.searchParams.set("lastFile", lastItem.created.toLocaleString()); + return c.redirect(doneUrl); + } catch (error) { + logger.error("Failed to clean up: {error}", { error }); + } + } + } + + const errorUrl: URL = new URL( + "/thumbnail_cleanup", + new URL(c.req.url).origin, + ); + errorUrl.searchParams.set("error", "clean_preview"); + if (typeof beforeParameter === "string") { + errorUrl.searchParams.set("before", beforeParameter); + } + return c.redirect(errorUrl); +}); + +data.post("/clean", async (c) => { + let todoCounter = 0; + let deletionCounter = 0; + let processCounter = 0; + + const form = await c.req.formData(); + var beforeParameter = form.get("before"); + if (typeof beforeParameter === "string") { + const before = new Date(Date.parse(beforeParameter)); + const owner = await db.query.accountOwners.findFirst({}); + if (owner != null && STORAGE_URL_BASE !== undefined) { + logger.info(`Starting cleanup - before: ${before.toISOString()}`); + + try { + const mediaToDelete: { id: Uuid; key: string | null; created: Date }[] = + (await readFilesToDelete(before, STORAGE_URL_BASE)).map((row) => ({ + id: row.id, + key: row.thumbnailUrl.startsWith(STORAGE_URL_BASE as string) + ? row.thumbnailUrl.replace(STORAGE_URL_BASE as string, "") + : null, + created: row.created, + })); + + todoCounter = mediaToDelete.length; + logger.info(`about to delete ${mediaToDelete.length} files!`); + const firstItem = mediaToDelete[0]; + const lastItem = mediaToDelete[mediaToDelete.length - 1]; + logger.info( + `first file has id ${firstItem.id}, key ${firstItem.key}, created at ${firstItem.created}`, + ); + logger.info( + `last file has id ${lastItem.id}, key ${lastItem.key}, created at ${lastItem.created}`, + ); + + const disk = drive.use(); + + // we should report every 5 percent (or at worst every item if it's that few), sounds about good. + const chunksize = Math.trunc(Math.max(1, todoCounter / 20)); + + for (const medium of mediaToDelete) { + if (medium.key != null) { + await disk.delete(medium.key); + ++deletionCounter; + } + ++processCounter; + if (processCounter % chunksize === 0) { + logger.info( + `Thumbnail cleanup ${Math.trunc((processCounter / todoCounter) * 100)}% done (${processCounter}/${todoCounter}, ${deletionCounter} deletions)`, + ); + } + } + + logger.info( + `Cleanup done, ${todoCounter} to do, ${processCounter} processed, ${deletionCounter} deleted!`, + ); + + const doneUrl: URL = new URL( + "/thumbnail_cleanup", + new URL(c.req.url).origin, + ); + doneUrl.searchParams.set("done", "clean"); + doneUrl.searchParams.set("before", beforeParameter); + doneUrl.searchParams.set("todo", todoCounter.toLocaleString("en")); + doneUrl.searchParams.set( + "processed", + processCounter.toLocaleString("en"), + ); + doneUrl.searchParams.set( + "deleted", + deletionCounter.toLocaleString("en"), + ); + return c.redirect(doneUrl); + } catch (error) { + logger.error("Failed to clean up: {error}", { error }); + logger.info( + `Cleanup unfinished, ${todoCounter} to do, ${processCounter} processed, ${deletionCounter} deleted!`, + ); + } + } + } + + const errorUrl: URL = new URL( + "/thumbnail_cleanup", + new URL(c.req.url).origin, + ); + errorUrl.searchParams.set("error", "clean"); + errorUrl.searchParams.set("todo", todoCounter.toLocaleString("en")); + errorUrl.searchParams.set("processed", processCounter.toLocaleString("en")); + errorUrl.searchParams.set("deleted", deletionCounter.toLocaleString("en")); + if (typeof beforeParameter === "string") { + errorUrl.searchParams.set("before", beforeParameter); + } + return c.redirect(errorUrl); +}); + +export default data; From 25717214a340ac267cb3173758f7060506d7bb97 Mon Sep 17 00:00:00 2001 From: aliceif <7098860+aliceif@users.noreply.github.com> Date: Sat, 18 Apr 2026 19:48:09 +0200 Subject: [PATCH 02/16] add column to media table to track thumbnail cleanups --- drizzle/0080_add_thumbnail_cleaned_flag.sql | 1 + drizzle/meta/0080_snapshot.json | 3837 +++++++++++++++++++ drizzle/meta/_journal.json | 7 + src/pages/thumbnail_cleanup.tsx | 19 +- src/schema.ts | 1 + 5 files changed, 3863 insertions(+), 2 deletions(-) create mode 100644 drizzle/0080_add_thumbnail_cleaned_flag.sql create mode 100644 drizzle/meta/0080_snapshot.json diff --git a/drizzle/0080_add_thumbnail_cleaned_flag.sql b/drizzle/0080_add_thumbnail_cleaned_flag.sql new file mode 100644 index 00000000..79b20cb9 --- /dev/null +++ b/drizzle/0080_add_thumbnail_cleaned_flag.sql @@ -0,0 +1 @@ +ALTER TABLE "media" ADD COLUMN "thumbnail_cleaned" boolean DEFAULT false NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0080_snapshot.json b/drizzle/meta/0080_snapshot.json new file mode 100644 index 00000000..0473b0ed --- /dev/null +++ b/drizzle/meta/0080_snapshot.json @@ -0,0 +1,3837 @@ +{ + "id": "326b202d-0fe6-4c19-9b7b-fe851ba533ed", + "prevId": "6bf2b035-da26-48a3-995e-07de29fc62b1", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.access_grants": { + "name": "access_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_in": { + "name": "expires_in", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "scope[]", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_owner_id": { + "name": "resource_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "revoked": { + "name": "revoked", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "access_grants_resource_owner_id_index": { + "name": "access_grants_resource_owner_id_index", + "columns": [ + { + "expression": "resource_owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "access_grants_application_id_applications_id_fk": { + "name": "access_grants_application_id_applications_id_fk", + "tableFrom": "access_grants", + "tableTo": "applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "access_grants_resource_owner_id_account_owners_id_fk": { + "name": "access_grants_resource_owner_id_account_owners_id_fk", + "tableFrom": "access_grants", + "tableTo": "account_owners", + "columnsFrom": [ + "resource_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "access_grants_code_unique": { + "name": "access_grants_code_unique", + "nullsNotDistinct": false, + "columns": [ + "code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.access_tokens": { + "name": "access_tokens", + "schema": "", + "columns": { + "code": { + "name": "code", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_owner_id": { + "name": "account_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "grant_type": { + "name": "grant_type", + "type": "grant_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'authorization_code'" + }, + "scopes": { + "name": "scopes", + "type": "scope[]", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "access_tokens_application_id_applications_id_fk": { + "name": "access_tokens_application_id_applications_id_fk", + "tableFrom": "access_tokens", + "tableTo": "applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "access_tokens_account_owner_id_account_owners_id_fk": { + "name": "access_tokens_account_owner_id_account_owners_id_fk", + "tableFrom": "access_tokens", + "tableTo": "account_owners", + "columnsFrom": [ + "account_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account_owners": { + "name": "account_owners", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rsa_private_key_jwk": { + "name": "rsa_private_key_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "rsa_public_key_jwk": { + "name": "rsa_public_key_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "ed25519_private_key_jwk": { + "name": "ed25519_private_key_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "ed25519_public_key_jwk": { + "name": "ed25519_public_key_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "fields": { + "name": "fields", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "followed_tags": { + "name": "followed_tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "visibility": { + "name": "visibility", + "type": "post_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'en'" + }, + "discoverable": { + "name": "discoverable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "expand_spoilers": { + "name": "expand_spoilers", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme_color": { + "name": "theme_color", + "type": "theme_color", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_owners_id_accounts_id_fk": { + "name": "account_owners_id_accounts_id_fk", + "tableFrom": "account_owners", + "tableTo": "accounts", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "account_owners_handle_unique": { + "name": "account_owners_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "iri": { + "name": "iri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "account_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bio_html": { + "name": "bio_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "protected": { + "name": "protected", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_url": { + "name": "cover_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_url": { + "name": "inbox_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "followers_url": { + "name": "followers_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_inbox_url": { + "name": "shared_inbox_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "featured_url": { + "name": "featured_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "following_count": { + "name": "following_count", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "followers_count": { + "name": "followers_count", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "posts_count": { + "name": "posts_count", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "field_htmls": { + "name": "field_htmls", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "emojis": { + "name": "emojis", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "sensitive": { + "name": "sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "successor_id": { + "name": "successor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "aliases": { + "name": "aliases", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "(ARRAY[]::text[])" + }, + "instance_host": { + "name": "instance_host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "published": { + "name": "published", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "fetched": { + "name": "fetched", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "accounts_successor_id_accounts_id_fk": { + "name": "accounts_successor_id_accounts_id_fk", + "tableFrom": "accounts", + "tableTo": "accounts", + "columnsFrom": [ + "successor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "accounts_instance_host_instances_host_fk": { + "name": "accounts_instance_host_instances_host_fk", + "tableFrom": "accounts", + "tableTo": "instances", + "columnsFrom": [ + "instance_host" + ], + "columnsTo": [ + "host" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "accounts_iri_unique": { + "name": "accounts_iri_unique", + "nullsNotDistinct": false, + "columns": [ + "iri" + ] + }, + "accounts_handle_unique": { + "name": "accounts_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.applications": { + "name": "applications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "scope[]", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidential": { + "name": "confidential", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "applications_client_id_unique": { + "name": "applications_client_id_unique", + "nullsNotDistinct": false, + "columns": [ + "client_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.blocks": { + "name": "blocks", + "schema": "", + "columns": { + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "blocked_account_id": { + "name": "blocked_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "blocks_account_id_index": { + "name": "blocks_account_id_index", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "blocks_blocked_account_id_index": { + "name": "blocks_blocked_account_id_index", + "columns": [ + { + "expression": "blocked_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "blocks_account_id_accounts_id_fk": { + "name": "blocks_account_id_accounts_id_fk", + "tableFrom": "blocks", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "blocks_blocked_account_id_accounts_id_fk": { + "name": "blocks_blocked_account_id_accounts_id_fk", + "tableFrom": "blocks", + "tableTo": "accounts", + "columnsFrom": [ + "blocked_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "blocks_account_id_blocked_account_id_pk": { + "name": "blocks_account_id_blocked_account_id_pk", + "columns": [ + "account_id", + "blocked_account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bookmarks": { + "name": "bookmarks", + "schema": "", + "columns": { + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_owner_id": { + "name": "account_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "bookmarks_post_id_account_owner_id_index": { + "name": "bookmarks_post_id_account_owner_id_index", + "columns": [ + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bookmarks_post_id_posts_id_fk": { + "name": "bookmarks_post_id_posts_id_fk", + "tableFrom": "bookmarks", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bookmarks_account_owner_id_account_owners_id_fk": { + "name": "bookmarks_account_owner_id_account_owners_id_fk", + "tableFrom": "bookmarks", + "tableTo": "account_owners", + "columnsFrom": [ + "account_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "bookmarks_post_id_account_owner_id_pk": { + "name": "bookmarks_post_id_account_owner_id_pk", + "columns": [ + "post_id", + "account_owner_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentials": { + "name": "credentials", + "schema": "", + "columns": { + "email": { + "name": "email", + "type": "varchar(254)", + "primaryKey": true, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_emojis": { + "name": "custom_emojis", + "schema": "", + "columns": { + "shortcode": { + "name": "shortcode", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.featured_tags": { + "name": "featured_tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "account_owner_id": { + "name": "account_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "featured_tags_account_owner_id_account_owners_id_fk": { + "name": "featured_tags_account_owner_id_account_owners_id_fk", + "tableFrom": "featured_tags", + "tableTo": "account_owners", + "columnsFrom": [ + "account_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "featured_tags_account_owner_id_name_unique": { + "name": "featured_tags_account_owner_id_name_unique", + "nullsNotDistinct": false, + "columns": [ + "account_owner_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.follows": { + "name": "follows", + "schema": "", + "columns": { + "iri": { + "name": "iri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "following_id": { + "name": "following_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "follower_id": { + "name": "follower_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "shares": { + "name": "shares", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notify": { + "name": "notify", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "languages": { + "name": "languages", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "approved": { + "name": "approved", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "follows_following_id_approved_index": { + "name": "follows_following_id_approved_index", + "columns": [ + { + "expression": "following_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "approved", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"follows\".\"approved\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "follows_following_id_created_index": { + "name": "follows_following_id_created_index", + "columns": [ + { + "expression": "following_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "follows_following_id_accounts_id_fk": { + "name": "follows_following_id_accounts_id_fk", + "tableFrom": "follows", + "tableTo": "accounts", + "columnsFrom": [ + "following_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "follows_follower_id_accounts_id_fk": { + "name": "follows_follower_id_accounts_id_fk", + "tableFrom": "follows", + "tableTo": "accounts", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "follows_following_id_follower_id_pk": { + "name": "follows_following_id_follower_id_pk", + "columns": [ + "following_id", + "follower_id" + ] + } + }, + "uniqueConstraints": { + "follows_iri_unique": { + "name": "follows_iri_unique", + "nullsNotDistinct": false, + "columns": [ + "iri" + ] + } + }, + "policies": {}, + "checkConstraints": { + "ck_follows_self": { + "name": "ck_follows_self", + "value": "\"follows\".\"following_id\" != \"follows\".\"follower_id\"" + } + }, + "isRLSEnabled": false + }, + "public.import_job_items": { + "name": "import_job_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "import_job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "import_job_items_job_id_status_index": { + "name": "import_job_items_job_id_status_index", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "import_job_items_job_id_import_jobs_id_fk": { + "name": "import_job_items_job_id_import_jobs_id_fk", + "tableFrom": "import_job_items", + "tableTo": "import_jobs", + "columnsFrom": [ + "job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.import_jobs": { + "name": "import_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "account_owner_id": { + "name": "account_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "import_job_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "import_job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "total_items": { + "name": "total_items", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processed_items": { + "name": "processed_items", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "successful_items": { + "name": "successful_items", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed_items": { + "name": "failed_items", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "import_jobs_account_owner_id_status_index": { + "name": "import_jobs_account_owner_id_status_index", + "columns": [ + { + "expression": "account_owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "import_jobs_status_created_index": { + "name": "import_jobs_status_created_index", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "import_jobs_account_owner_id_account_owners_id_fk": { + "name": "import_jobs_account_owner_id_account_owners_id_fk", + "tableFrom": "import_jobs", + "tableTo": "account_owners", + "columnsFrom": [ + "account_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instances": { + "name": "instances", + "schema": "", + "columns": { + "host": { + "name": "host", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "software": { + "name": "software", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_version": { + "name": "software_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.likes": { + "name": "likes", + "schema": "", + "columns": { + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "likes_account_id_post_id_index": { + "name": "likes_account_id_post_id_index", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "likes_created_index": { + "name": "likes_created_index", + "columns": [ + { + "expression": "created", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "likes_post_id_posts_id_fk": { + "name": "likes_post_id_posts_id_fk", + "tableFrom": "likes", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "likes_account_id_accounts_id_fk": { + "name": "likes_account_id_accounts_id_fk", + "tableFrom": "likes", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "likes_post_id_account_id_pk": { + "name": "likes_post_id_account_id_pk", + "columns": [ + "post_id", + "account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.list_members": { + "name": "list_members", + "schema": "", + "columns": { + "list_id": { + "name": "list_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "list_members_list_id_lists_id_fk": { + "name": "list_members_list_id_lists_id_fk", + "tableFrom": "list_members", + "tableTo": "lists", + "columnsFrom": [ + "list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "list_members_account_id_accounts_id_fk": { + "name": "list_members_account_id_accounts_id_fk", + "tableFrom": "list_members", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "list_members_list_id_account_id_pk": { + "name": "list_members_list_id_account_id_pk", + "columns": [ + "list_id", + "account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.list_posts": { + "name": "list_posts", + "schema": "", + "columns": { + "list_id": { + "name": "list_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "list_posts_list_id_post_id_index": { + "name": "list_posts_list_id_post_id_index", + "columns": [ + { + "expression": "list_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "list_posts_list_id_lists_id_fk": { + "name": "list_posts_list_id_lists_id_fk", + "tableFrom": "list_posts", + "tableTo": "lists", + "columnsFrom": [ + "list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "list_posts_post_id_posts_id_fk": { + "name": "list_posts_post_id_posts_id_fk", + "tableFrom": "list_posts", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "list_posts_list_id_post_id_pk": { + "name": "list_posts_list_id_post_id_pk", + "columns": [ + "list_id", + "post_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lists": { + "name": "lists", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "account_owner_id": { + "name": "account_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "replies_policy": { + "name": "replies_policy", + "type": "list_replies_policy", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'list'" + }, + "exclusive": { + "name": "exclusive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "lists_account_owner_id_account_owners_id_fk": { + "name": "lists_account_owner_id_account_owners_id_fk", + "tableFrom": "lists", + "tableTo": "account_owners", + "columnsFrom": [ + "account_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.markers": { + "name": "markers", + "schema": "", + "columns": { + "account_owner_id": { + "name": "account_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "marker_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "last_read_id": { + "name": "last_read_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "markers_account_owner_id_account_owners_id_fk": { + "name": "markers_account_owner_id_account_owners_id_fk", + "tableFrom": "markers", + "tableTo": "account_owners", + "columnsFrom": [ + "account_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "markers_account_owner_id_type_pk": { + "name": "markers_account_owner_id_type_pk", + "columns": [ + "account_owner_id", + "type" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "thumbnail_type": { + "name": "thumbnail_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thumbnail_url": { + "name": "thumbnail_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thumbnail_width": { + "name": "thumbnail_width", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "thumbnail_height": { + "name": "thumbnail_height", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "thumbnail_cleaned": { + "name": "thumbnail_cleaned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "media_post_id_index": { + "name": "media_post_id_index", + "columns": [ + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "media_post_id_posts_id_fk": { + "name": "media_post_id_posts_id_fk", + "tableFrom": "media", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mentions": { + "name": "mentions", + "schema": "", + "columns": { + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mentions_post_id_account_id_index": { + "name": "mentions_post_id_account_id_index", + "columns": [ + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mentions_post_id_posts_id_fk": { + "name": "mentions_post_id_posts_id_fk", + "tableFrom": "mentions", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mentions_account_id_accounts_id_fk": { + "name": "mentions_account_id_accounts_id_fk", + "tableFrom": "mentions", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mentions_post_id_account_id_pk": { + "name": "mentions_post_id_account_id_pk", + "columns": [ + "post_id", + "account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mutes": { + "name": "mutes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "muted_account_id": { + "name": "muted_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "notifications": { + "name": "notifications", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "duration": { + "name": "duration", + "type": "interval", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "mutes_account_id_index": { + "name": "mutes_account_id_index", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mutes_account_id_accounts_id_fk": { + "name": "mutes_account_id_accounts_id_fk", + "tableFrom": "mutes", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mutes_muted_account_id_accounts_id_fk": { + "name": "mutes_muted_account_id_accounts_id_fk", + "tableFrom": "mutes", + "tableTo": "accounts", + "columnsFrom": [ + "muted_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mutes_account_id_muted_account_id_unique": { + "name": "mutes_account_id_muted_account_id_unique", + "nullsNotDistinct": false, + "columns": [ + "account_id", + "muted_account_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_groups": { + "name": "notification_groups", + "schema": "", + "columns": { + "group_key": { + "name": "group_key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_owner_id": { + "name": "account_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "target_post_id": { + "name": "target_post_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "notifications_count": { + "name": "notifications_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "most_recent_notification_id": { + "name": "most_recent_notification_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sample_account_ids": { + "name": "sample_account_ids", + "type": "uuid[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::uuid[]" + }, + "latest_page_notification_at": { + "name": "latest_page_notification_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "page_min_id": { + "name": "page_min_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "page_max_id": { + "name": "page_max_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "notification_groups_account_owner_id_updated_index": { + "name": "notification_groups_account_owner_id_updated_index", + "columns": [ + { + "expression": "account_owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_groups_account_owner_id_type_index": { + "name": "notification_groups_account_owner_id_type_index", + "columns": [ + { + "expression": "account_owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notification_groups_account_owner_id_account_owners_id_fk": { + "name": "notification_groups_account_owner_id_account_owners_id_fk", + "tableFrom": "notification_groups", + "tableTo": "account_owners", + "columnsFrom": [ + "account_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_groups_target_post_id_posts_id_fk": { + "name": "notification_groups_target_post_id_posts_id_fk", + "tableFrom": "notification_groups", + "tableTo": "posts", + "columnsFrom": [ + "target_post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_groups_most_recent_notification_id_notifications_id_fk": { + "name": "notification_groups_most_recent_notification_id_notifications_id_fk", + "tableFrom": "notification_groups", + "tableTo": "notifications", + "columnsFrom": [ + "most_recent_notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "account_owner_id": { + "name": "account_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "actor_account_id": { + "name": "actor_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_post_id": { + "name": "target_post_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_account_id": { + "name": "target_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_poll_id": { + "name": "target_poll_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "group_key": { + "name": "group_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "read_at": { + "name": "read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "notifications_account_owner_id_created_index": { + "name": "notifications_account_owner_id_created_index", + "columns": [ + { + "expression": "account_owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_account_owner_id_read_at_index": { + "name": "notifications_account_owner_id_read_at_index", + "columns": [ + { + "expression": "account_owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "read_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_group_key_index": { + "name": "notifications_group_key_index", + "columns": [ + { + "expression": "group_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_created_index": { + "name": "notifications_created_index", + "columns": [ + { + "expression": "created", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_account_owner_id_account_owners_id_fk": { + "name": "notifications_account_owner_id_account_owners_id_fk", + "tableFrom": "notifications", + "tableTo": "account_owners", + "columnsFrom": [ + "account_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_actor_account_id_accounts_id_fk": { + "name": "notifications_actor_account_id_accounts_id_fk", + "tableFrom": "notifications", + "tableTo": "accounts", + "columnsFrom": [ + "actor_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_target_post_id_posts_id_fk": { + "name": "notifications_target_post_id_posts_id_fk", + "tableFrom": "notifications", + "tableTo": "posts", + "columnsFrom": [ + "target_post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_target_account_id_accounts_id_fk": { + "name": "notifications_target_account_id_accounts_id_fk", + "tableFrom": "notifications", + "tableTo": "accounts", + "columnsFrom": [ + "target_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_target_poll_id_polls_id_fk": { + "name": "notifications_target_poll_id_polls_id_fk", + "tableFrom": "notifications", + "tableTo": "polls", + "columnsFrom": [ + "target_poll_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pinned_posts": { + "name": "pinned_posts", + "schema": "", + "columns": { + "index": { + "name": "index", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "pinned_posts_account_id_post_id_index": { + "name": "pinned_posts_account_id_post_id_index", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pinned_posts_account_id_accounts_id_fk": { + "name": "pinned_posts_account_id_accounts_id_fk", + "tableFrom": "pinned_posts", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pinned_posts_post_id_account_id_posts_id_actor_id_fk": { + "name": "pinned_posts_post_id_account_id_posts_id_actor_id_fk", + "tableFrom": "pinned_posts", + "tableTo": "posts", + "columnsFrom": [ + "post_id", + "account_id" + ], + "columnsTo": [ + "id", + "actor_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "pinned_posts_post_id_account_id_unique": { + "name": "pinned_posts_post_id_account_id_unique", + "nullsNotDistinct": false, + "columns": [ + "post_id", + "account_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.poll_options": { + "name": "poll_options", + "schema": "", + "columns": { + "poll_id": { + "name": "poll_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "votes_count": { + "name": "votes_count", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "poll_options_poll_id_index_index": { + "name": "poll_options_poll_id_index_index", + "columns": [ + { + "expression": "poll_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "poll_options_poll_id_polls_id_fk": { + "name": "poll_options_poll_id_polls_id_fk", + "tableFrom": "poll_options", + "tableTo": "polls", + "columnsFrom": [ + "poll_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "poll_options_poll_id_index_pk": { + "name": "poll_options_poll_id_index_pk", + "columns": [ + "poll_id", + "index" + ] + } + }, + "uniqueConstraints": { + "poll_options_poll_id_title_unique": { + "name": "poll_options_poll_id_title_unique", + "nullsNotDistinct": false, + "columns": [ + "poll_id", + "title" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.poll_votes": { + "name": "poll_votes", + "schema": "", + "columns": { + "poll_id": { + "name": "poll_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "option_index": { + "name": "option_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "poll_votes_poll_id_account_id_index": { + "name": "poll_votes_poll_id_account_id_index", + "columns": [ + { + "expression": "poll_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "poll_votes_poll_id_polls_id_fk": { + "name": "poll_votes_poll_id_polls_id_fk", + "tableFrom": "poll_votes", + "tableTo": "polls", + "columnsFrom": [ + "poll_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "poll_votes_account_id_accounts_id_fk": { + "name": "poll_votes_account_id_accounts_id_fk", + "tableFrom": "poll_votes", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "poll_votes_poll_id_option_index_poll_options_poll_id_index_fk": { + "name": "poll_votes_poll_id_option_index_poll_options_poll_id_index_fk", + "tableFrom": "poll_votes", + "tableTo": "poll_options", + "columnsFrom": [ + "poll_id", + "option_index" + ], + "columnsTo": [ + "poll_id", + "index" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "poll_votes_poll_id_option_index_account_id_pk": { + "name": "poll_votes_poll_id_option_index_account_id_pk", + "columns": [ + "poll_id", + "option_index", + "account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.polls": { + "name": "polls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "multiple": { + "name": "multiple", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "voters_count": { + "name": "voters_count", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "expires": { + "name": "expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.posts": { + "name": "posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "iri": { + "name": "iri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "post_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reply_target_id": { + "name": "reply_target_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sharing_id": { + "name": "sharing_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "quote_target_id": { + "name": "quote_target_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "post_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_html": { + "name": "content_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "poll_id": { + "name": "poll_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "emojis": { + "name": "emojis", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "sensitive": { + "name": "sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_card": { + "name": "preview_card", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "replies_count": { + "name": "replies_count", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "shares_count": { + "name": "shares_count", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "likes_count": { + "name": "likes_count", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "quotes_count": { + "name": "quotes_count", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "idempotence_key": { + "name": "idempotence_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published": { + "name": "published", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "posts_sharing_id_index": { + "name": "posts_sharing_id_index", + "columns": [ + { + "expression": "sharing_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_actor_id_index": { + "name": "posts_actor_id_index", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_actor_id_sharing_id_index": { + "name": "posts_actor_id_sharing_id_index", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sharing_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_reply_target_id_index": { + "name": "posts_reply_target_id_index", + "columns": [ + { + "expression": "reply_target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_actor_id_reply_target_id_index": { + "name": "posts_actor_id_reply_target_id_index", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reply_target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_quote_target_id_index": { + "name": "posts_quote_target_id_index", + "columns": [ + { + "expression": "quote_target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"posts\".\"quote_target_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_visibility_actor_id_index": { + "name": "posts_visibility_actor_id_index", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_visibility_actor_id_sharing_id_index": { + "name": "posts_visibility_actor_id_sharing_id_index", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sharing_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"posts\".\"sharing_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_visibility_actor_id_reply_target_id_index": { + "name": "posts_visibility_actor_id_reply_target_id_index", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reply_target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"posts\".\"reply_target_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "posts_actor_id_accounts_id_fk": { + "name": "posts_actor_id_accounts_id_fk", + "tableFrom": "posts", + "tableTo": "accounts", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "posts_application_id_applications_id_fk": { + "name": "posts_application_id_applications_id_fk", + "tableFrom": "posts", + "tableTo": "applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "posts_reply_target_id_posts_id_fk": { + "name": "posts_reply_target_id_posts_id_fk", + "tableFrom": "posts", + "tableTo": "posts", + "columnsFrom": [ + "reply_target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "posts_sharing_id_posts_id_fk": { + "name": "posts_sharing_id_posts_id_fk", + "tableFrom": "posts", + "tableTo": "posts", + "columnsFrom": [ + "sharing_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "posts_quote_target_id_posts_id_fk": { + "name": "posts_quote_target_id_posts_id_fk", + "tableFrom": "posts", + "tableTo": "posts", + "columnsFrom": [ + "quote_target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "posts_poll_id_polls_id_fk": { + "name": "posts_poll_id_polls_id_fk", + "tableFrom": "posts", + "tableTo": "polls", + "columnsFrom": [ + "poll_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "posts_iri_unique": { + "name": "posts_iri_unique", + "nullsNotDistinct": false, + "columns": [ + "iri" + ] + }, + "posts_id_actor_id_unique": { + "name": "posts_id_actor_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "actor_id" + ] + }, + "posts_poll_id_unique": { + "name": "posts_poll_id_unique", + "nullsNotDistinct": false, + "columns": [ + "poll_id" + ] + }, + "posts_actor_id_sharing_id_unique": { + "name": "posts_actor_id_sharing_id_unique", + "nullsNotDistinct": false, + "columns": [ + "actor_id", + "sharing_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reactions": { + "name": "reactions", + "schema": "", + "columns": { + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "custom_emoji": { + "name": "custom_emoji", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emoji_iri": { + "name": "emoji_iri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "reactions_post_id_index": { + "name": "reactions_post_id_index", + "columns": [ + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reactions_post_id_account_id_index": { + "name": "reactions_post_id_account_id_index", + "columns": [ + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reactions_created_index": { + "name": "reactions_created_index", + "columns": [ + { + "expression": "created", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reactions_post_id_posts_id_fk": { + "name": "reactions_post_id_posts_id_fk", + "tableFrom": "reactions", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reactions_account_id_accounts_id_fk": { + "name": "reactions_account_id_accounts_id_fk", + "tableFrom": "reactions", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "reactions_post_id_account_id_emoji_pk": { + "name": "reactions_post_id_account_id_emoji_pk", + "columns": [ + "post_id", + "account_id", + "emoji" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reports": { + "name": "reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "iri": { + "name": "iri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_account_id": { + "name": "target_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "posts": { + "name": "posts", + "type": "uuid[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::uuid[]" + } + }, + "indexes": {}, + "foreignKeys": { + "reports_account_id_accounts_id_fk": { + "name": "reports_account_id_accounts_id_fk", + "tableFrom": "reports", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reports_target_account_id_accounts_id_fk": { + "name": "reports_target_account_id_accounts_id_fk", + "tableFrom": "reports", + "tableTo": "accounts", + "columnsFrom": [ + "target_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "reports_iri_unique": { + "name": "reports_iri_unique", + "nullsNotDistinct": false, + "columns": [ + "iri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.timeline_posts": { + "name": "timeline_posts", + "schema": "", + "columns": { + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "timeline_posts_account_id_post_id_index": { + "name": "timeline_posts_account_id_post_id_index", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "timeline_posts_account_id_account_owners_id_fk": { + "name": "timeline_posts_account_id_account_owners_id_fk", + "tableFrom": "timeline_posts", + "tableTo": "account_owners", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "timeline_posts_post_id_posts_id_fk": { + "name": "timeline_posts_post_id_posts_id_fk", + "tableFrom": "timeline_posts", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "timeline_posts_account_id_post_id_pk": { + "name": "timeline_posts_account_id_post_id_pk", + "columns": [ + "account_id", + "post_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.totps": { + "name": "totps", + "schema": "", + "columns": { + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "digits": { + "name": "digits", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "period": { + "name": "period", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.account_type": { + "name": "account_type", + "schema": "public", + "values": [ + "Application", + "Group", + "Organization", + "Person", + "Service" + ] + }, + "public.grant_type": { + "name": "grant_type", + "schema": "public", + "values": [ + "authorization_code", + "client_credentials" + ] + }, + "public.import_job_category": { + "name": "import_job_category", + "schema": "public", + "values": [ + "following_accounts", + "lists", + "muted_accounts", + "blocked_accounts", + "bookmarks" + ] + }, + "public.import_job_status": { + "name": "import_job_status", + "schema": "public", + "values": [ + "pending", + "processing", + "completed", + "failed", + "cancelled" + ] + }, + "public.list_replies_policy": { + "name": "list_replies_policy", + "schema": "public", + "values": [ + "followed", + "list", + "none" + ] + }, + "public.marker_type": { + "name": "marker_type", + "schema": "public", + "values": [ + "notifications", + "home" + ] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": [ + "mention", + "status", + "reblog", + "follow", + "follow_request", + "favourite", + "emoji_reaction", + "poll", + "update", + "admin.sign_up", + "admin.report", + "quote", + "quoted_update" + ] + }, + "public.post_type": { + "name": "post_type", + "schema": "public", + "values": [ + "Article", + "Note", + "Question" + ] + }, + "public.post_visibility": { + "name": "post_visibility", + "schema": "public", + "values": [ + "public", + "unlisted", + "private", + "direct" + ] + }, + "public.scope": { + "name": "scope", + "schema": "public", + "values": [ + "read", + "read:accounts", + "read:blocks", + "read:bookmarks", + "read:favourites", + "read:filters", + "read:follows", + "read:lists", + "read:mutes", + "read:notifications", + "read:search", + "read:statuses", + "write", + "write:accounts", + "write:blocks", + "write:bookmarks", + "write:conversations", + "write:favourites", + "write:filters", + "write:follows", + "write:lists", + "write:media", + "write:mutes", + "write:notifications", + "write:reports", + "write:statuses", + "follow", + "push", + "profile" + ] + }, + "public.theme_color": { + "name": "theme_color", + "schema": "public", + "values": [ + "amber", + "azure", + "blue", + "cyan", + "fuchsia", + "green", + "grey", + "indigo", + "jade", + "lime", + "orange", + "pink", + "pumpkin", + "purple", + "red", + "sand", + "slate", + "violet", + "yellow", + "zinc" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 3885e4dd..62bd9521 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -561,6 +561,13 @@ "when": 1775993648845, "tag": "0079_account_owners_expand_spoilers", "breakpoints": true + }, + { + "idx": 80, + "version": "7", + "when": 1776805400028, + "tag": "0080_add_thumbnail_cleaned_flag", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/pages/thumbnail_cleanup.tsx b/src/pages/thumbnail_cleanup.tsx index 88cf1366..09adbb73 100644 --- a/src/pages/thumbnail_cleanup.tsx +++ b/src/pages/thumbnail_cleanup.tsx @@ -1,6 +1,6 @@ import { Temporal } from "@js-temporal/polyfill"; import { getLogger } from "@logtape/logtape"; -import { and, count, eq, exists, ilike, lt, notExists } from "drizzle-orm"; +import { and, count, eq, exists, ilike, lt, not, notExists } from "drizzle-orm"; import { alias } from "drizzle-orm/pg-core"; import { Hono } from "hono"; @@ -58,6 +58,7 @@ data.get("/", async (c) => { .innerJoin(accounts, eq(posts.accountId, accounts.id)) .where( and( + not(media.thumbnailCleaned), ilike(media.thumbnailUrl, `${STORAGE_URL_BASE}%`), lt(media.created, new Date(new Date().getFullYear() - 1, 0, 1)), notExists( @@ -152,6 +153,7 @@ data.get("/", async (c) => { .innerJoin(accounts, eq(posts.accountId, accounts.id)) .where( and( + not(media.thumbnailCleaned), ilike(media.thumbnailUrl, `${STORAGE_URL_BASE}%`), lt(media.created, new Date(new Date().getFullYear() - 1, 0, 1)), notExists( @@ -183,6 +185,7 @@ data.get("/", async (c) => { .innerJoin(accounts, eq(posts.accountId, accounts.id)) .where( and( + not(media.thumbnailCleaned), ilike(media.thumbnailUrl, `${STORAGE_URL_BASE}%`), lt(media.created, oneYearAgo), notExists( @@ -277,6 +280,7 @@ data.get("/", async (c) => { .innerJoin(accounts, eq(posts.accountId, accounts.id)) .where( and( + not(media.thumbnailCleaned), ilike(media.thumbnailUrl, `${STORAGE_URL_BASE}%`), lt(media.created, oneYearAgo), notExists( @@ -303,6 +307,7 @@ data.get("/", async (c) => { .innerJoin(accounts, eq(posts.accountId, accounts.id)) .where( and( + not(media.thumbnailCleaned), ilike(media.thumbnailUrl, `${STORAGE_URL_BASE}%`), notExists( db @@ -324,7 +329,12 @@ data.get("/", async (c) => { count: count(), }) .from(media) - .where(ilike(media.thumbnailUrl, `${STORAGE_URL_BASE}%`)); + .where( + and( + not(media.thumbnailCleaned), + ilike(media.thumbnailUrl, `${STORAGE_URL_BASE}%`), + ), + ); } catch { thumbnailsCountResult = [{ count: 0 }]; } @@ -507,6 +517,7 @@ function readFilesToDelete( .innerJoin(accounts, eq(posts.accountId, accounts.id)) .where( and( + not(media.thumbnailCleaned), ilike(media.thumbnailUrl, `${keyPrefix}%`), lt(media.created, before), notExists( @@ -689,6 +700,10 @@ data.post("/clean", async (c) => { for (const medium of mediaToDelete) { if (medium.key != null) { await disk.delete(medium.key); + await db + .update(media) + .set({ thumbnailCleaned: true }) + .where(eq(media.id, medium.id)); ++deletionCounter; } ++processCounter; diff --git a/src/schema.ts b/src/schema.ts index 1e54bd30..8adb271b 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -546,6 +546,7 @@ export const media = pgTable( created: timestamp("created", { withTimezone: true }) .notNull() .default(currentTimestamp), + thumbnailCleaned: boolean("thumbnail_cleaned").notNull().default(false), }, (table) => [index().on(table.postId)], ); From c833e3a94748be6d04fef8a8f1aa2dc5a2256b9d Mon Sep 17 00:00:00 2001 From: aliceif <7098860+aliceif@users.noreply.github.com> Date: Sun, 19 Apr 2026 20:58:54 +0200 Subject: [PATCH 03/16] implement worker for cleanup jobs --- bin/server.ts | 28 +- drizzle/0081_add_cleanup_jobs.sql | 29 + drizzle/meta/0081_snapshot.json | 4058 +++++++++++++++++++++++++++++ drizzle/meta/_journal.json | 7 + src/cleanup/processors.ts | 52 + src/cleanup/worker.ts | 266 ++ src/schema.ts | 81 + 7 files changed, 4511 insertions(+), 10 deletions(-) create mode 100644 drizzle/0081_add_cleanup_jobs.sql create mode 100644 drizzle/meta/0081_snapshot.json create mode 100644 src/cleanup/processors.ts create mode 100644 src/cleanup/worker.ts diff --git a/bin/server.ts b/bin/server.ts index 45b7577e..7f194641 100644 --- a/bin/server.ts +++ b/bin/server.ts @@ -65,14 +65,21 @@ if (NODE_TYPE === "web" || NODE_TYPE === "all") { } // Start workers if running as worker or all node -let stopWorker: (() => void) | undefined; +let stopWorkers: (() => void) | undefined; if (NODE_TYPE === "worker" || NODE_TYPE === "all") { - const [{ federation }, { startImportWorker, stopImportWorker }] = - await Promise.all([ - import("../src/federation"), - import("../src/import/worker"), - ]); - stopWorker = stopImportWorker; + const [ + { federation }, + { startImportWorker, stopImportWorker }, + { startCleanupWorker, stopCleanupWorker }, + ] = await Promise.all([ + import("../src/federation"), + import("../src/import/worker"), + import("../src/cleanup/worker"), + ]); + stopWorkers = () => { + stopImportWorker(); + stopCleanupWorker(); + }; // Start the Fedify message queue const controller = new AbortController(); @@ -83,17 +90,18 @@ if (NODE_TYPE === "worker" || NODE_TYPE === "all") { process.exit(1); }); - // Start the import worker for background job processing + // Start the workers for background job processing startImportWorker(); + startCleanupWorker(); - console.log("Worker started (Fedify queue + Import worker)"); + console.log("Worker started (Fedify queue + Import worker + Cleanup worker)"); } // Graceful shutdown handling const shutdown = () => { if (NODE_TYPE === "worker" || NODE_TYPE === "all") { console.log("Stopping workers..."); - stopWorker?.(); + stopWorkers?.(); } process.exit(0); }; diff --git a/drizzle/0081_add_cleanup_jobs.sql b/drizzle/0081_add_cleanup_jobs.sql new file mode 100644 index 00000000..46ff1f81 --- /dev/null +++ b/drizzle/0081_add_cleanup_jobs.sql @@ -0,0 +1,29 @@ +CREATE TYPE "public"."cleanup_job_category" AS ENUM('cleanup_thumbnails');--> statement-breakpoint +CREATE TYPE "public"."cleanup_job_status" AS ENUM('pending', 'processing', 'completed', 'failed', 'cancelled');--> statement-breakpoint +CREATE TABLE "cleanup_job_items" ( + "id" uuid PRIMARY KEY NOT NULL, + "job_id" uuid NOT NULL, + "status" "cleanup_job_status" DEFAULT 'pending' NOT NULL, + "data" jsonb NOT NULL, + "error_message" text, + "processed_at" timestamp with time zone, + "created" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "cleanup_jobs" ( + "id" uuid PRIMARY KEY NOT NULL, + "category" "cleanup_job_category" NOT NULL, + "status" "cleanup_job_status" DEFAULT 'pending' NOT NULL, + "total_items" integer DEFAULT 0 NOT NULL, + "processed_items" integer DEFAULT 0 NOT NULL, + "successful_items" integer DEFAULT 0 NOT NULL, + "failed_items" integer DEFAULT 0 NOT NULL, + "error_message" text, + "created" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + "started_at" timestamp with time zone, + "completed_at" timestamp with time zone +); +--> statement-breakpoint +ALTER TABLE "cleanup_job_items" ADD CONSTRAINT "cleanup_job_items_job_id_cleanup_jobs_id_fk" FOREIGN KEY ("job_id") REFERENCES "public"."cleanup_jobs"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "cleanup_job_items_job_id_status_index" ON "cleanup_job_items" USING btree ("job_id","status");--> statement-breakpoint +CREATE INDEX "cleanup_jobs_status_created_index" ON "cleanup_jobs" USING btree ("status","created"); \ No newline at end of file diff --git a/drizzle/meta/0081_snapshot.json b/drizzle/meta/0081_snapshot.json new file mode 100644 index 00000000..7b3a3e6a --- /dev/null +++ b/drizzle/meta/0081_snapshot.json @@ -0,0 +1,4058 @@ +{ + "id": "fd422c15-e909-4887-ba49-6a7ddac1a000", + "prevId": "326b202d-0fe6-4c19-9b7b-fe851ba533ed", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.access_grants": { + "name": "access_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_in": { + "name": "expires_in", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "scope[]", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_owner_id": { + "name": "resource_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "revoked": { + "name": "revoked", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "access_grants_resource_owner_id_index": { + "name": "access_grants_resource_owner_id_index", + "columns": [ + { + "expression": "resource_owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "access_grants_application_id_applications_id_fk": { + "name": "access_grants_application_id_applications_id_fk", + "tableFrom": "access_grants", + "tableTo": "applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "access_grants_resource_owner_id_account_owners_id_fk": { + "name": "access_grants_resource_owner_id_account_owners_id_fk", + "tableFrom": "access_grants", + "tableTo": "account_owners", + "columnsFrom": [ + "resource_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "access_grants_code_unique": { + "name": "access_grants_code_unique", + "nullsNotDistinct": false, + "columns": [ + "code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.access_tokens": { + "name": "access_tokens", + "schema": "", + "columns": { + "code": { + "name": "code", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_owner_id": { + "name": "account_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "grant_type": { + "name": "grant_type", + "type": "grant_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'authorization_code'" + }, + "scopes": { + "name": "scopes", + "type": "scope[]", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "access_tokens_application_id_applications_id_fk": { + "name": "access_tokens_application_id_applications_id_fk", + "tableFrom": "access_tokens", + "tableTo": "applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "access_tokens_account_owner_id_account_owners_id_fk": { + "name": "access_tokens_account_owner_id_account_owners_id_fk", + "tableFrom": "access_tokens", + "tableTo": "account_owners", + "columnsFrom": [ + "account_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account_owners": { + "name": "account_owners", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rsa_private_key_jwk": { + "name": "rsa_private_key_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "rsa_public_key_jwk": { + "name": "rsa_public_key_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "ed25519_private_key_jwk": { + "name": "ed25519_private_key_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "ed25519_public_key_jwk": { + "name": "ed25519_public_key_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "fields": { + "name": "fields", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "followed_tags": { + "name": "followed_tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "visibility": { + "name": "visibility", + "type": "post_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'en'" + }, + "discoverable": { + "name": "discoverable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "expand_spoilers": { + "name": "expand_spoilers", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme_color": { + "name": "theme_color", + "type": "theme_color", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_owners_id_accounts_id_fk": { + "name": "account_owners_id_accounts_id_fk", + "tableFrom": "account_owners", + "tableTo": "accounts", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "account_owners_handle_unique": { + "name": "account_owners_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "iri": { + "name": "iri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "account_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bio_html": { + "name": "bio_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "protected": { + "name": "protected", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_url": { + "name": "cover_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_url": { + "name": "inbox_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "followers_url": { + "name": "followers_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_inbox_url": { + "name": "shared_inbox_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "featured_url": { + "name": "featured_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "following_count": { + "name": "following_count", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "followers_count": { + "name": "followers_count", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "posts_count": { + "name": "posts_count", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "field_htmls": { + "name": "field_htmls", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "emojis": { + "name": "emojis", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "sensitive": { + "name": "sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "successor_id": { + "name": "successor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "aliases": { + "name": "aliases", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "(ARRAY[]::text[])" + }, + "instance_host": { + "name": "instance_host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "published": { + "name": "published", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "fetched": { + "name": "fetched", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "accounts_successor_id_accounts_id_fk": { + "name": "accounts_successor_id_accounts_id_fk", + "tableFrom": "accounts", + "tableTo": "accounts", + "columnsFrom": [ + "successor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "accounts_instance_host_instances_host_fk": { + "name": "accounts_instance_host_instances_host_fk", + "tableFrom": "accounts", + "tableTo": "instances", + "columnsFrom": [ + "instance_host" + ], + "columnsTo": [ + "host" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "accounts_iri_unique": { + "name": "accounts_iri_unique", + "nullsNotDistinct": false, + "columns": [ + "iri" + ] + }, + "accounts_handle_unique": { + "name": "accounts_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.applications": { + "name": "applications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "scope[]", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidential": { + "name": "confidential", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "applications_client_id_unique": { + "name": "applications_client_id_unique", + "nullsNotDistinct": false, + "columns": [ + "client_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.blocks": { + "name": "blocks", + "schema": "", + "columns": { + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "blocked_account_id": { + "name": "blocked_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "blocks_account_id_index": { + "name": "blocks_account_id_index", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "blocks_blocked_account_id_index": { + "name": "blocks_blocked_account_id_index", + "columns": [ + { + "expression": "blocked_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "blocks_account_id_accounts_id_fk": { + "name": "blocks_account_id_accounts_id_fk", + "tableFrom": "blocks", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "blocks_blocked_account_id_accounts_id_fk": { + "name": "blocks_blocked_account_id_accounts_id_fk", + "tableFrom": "blocks", + "tableTo": "accounts", + "columnsFrom": [ + "blocked_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "blocks_account_id_blocked_account_id_pk": { + "name": "blocks_account_id_blocked_account_id_pk", + "columns": [ + "account_id", + "blocked_account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bookmarks": { + "name": "bookmarks", + "schema": "", + "columns": { + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_owner_id": { + "name": "account_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "bookmarks_post_id_account_owner_id_index": { + "name": "bookmarks_post_id_account_owner_id_index", + "columns": [ + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bookmarks_post_id_posts_id_fk": { + "name": "bookmarks_post_id_posts_id_fk", + "tableFrom": "bookmarks", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bookmarks_account_owner_id_account_owners_id_fk": { + "name": "bookmarks_account_owner_id_account_owners_id_fk", + "tableFrom": "bookmarks", + "tableTo": "account_owners", + "columnsFrom": [ + "account_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "bookmarks_post_id_account_owner_id_pk": { + "name": "bookmarks_post_id_account_owner_id_pk", + "columns": [ + "post_id", + "account_owner_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cleanup_job_items": { + "name": "cleanup_job_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "cleanup_job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "cleanup_job_items_job_id_status_index": { + "name": "cleanup_job_items_job_id_status_index", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cleanup_job_items_job_id_cleanup_jobs_id_fk": { + "name": "cleanup_job_items_job_id_cleanup_jobs_id_fk", + "tableFrom": "cleanup_job_items", + "tableTo": "cleanup_jobs", + "columnsFrom": [ + "job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cleanup_jobs": { + "name": "cleanup_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "category": { + "name": "category", + "type": "cleanup_job_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "cleanup_job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "total_items": { + "name": "total_items", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processed_items": { + "name": "processed_items", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "successful_items": { + "name": "successful_items", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed_items": { + "name": "failed_items", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "cleanup_jobs_status_created_index": { + "name": "cleanup_jobs_status_created_index", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentials": { + "name": "credentials", + "schema": "", + "columns": { + "email": { + "name": "email", + "type": "varchar(254)", + "primaryKey": true, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_emojis": { + "name": "custom_emojis", + "schema": "", + "columns": { + "shortcode": { + "name": "shortcode", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.featured_tags": { + "name": "featured_tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "account_owner_id": { + "name": "account_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "featured_tags_account_owner_id_account_owners_id_fk": { + "name": "featured_tags_account_owner_id_account_owners_id_fk", + "tableFrom": "featured_tags", + "tableTo": "account_owners", + "columnsFrom": [ + "account_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "featured_tags_account_owner_id_name_unique": { + "name": "featured_tags_account_owner_id_name_unique", + "nullsNotDistinct": false, + "columns": [ + "account_owner_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.follows": { + "name": "follows", + "schema": "", + "columns": { + "iri": { + "name": "iri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "following_id": { + "name": "following_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "follower_id": { + "name": "follower_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "shares": { + "name": "shares", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notify": { + "name": "notify", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "languages": { + "name": "languages", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "approved": { + "name": "approved", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "follows_following_id_approved_index": { + "name": "follows_following_id_approved_index", + "columns": [ + { + "expression": "following_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "approved", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"follows\".\"approved\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "follows_following_id_created_index": { + "name": "follows_following_id_created_index", + "columns": [ + { + "expression": "following_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "follows_following_id_accounts_id_fk": { + "name": "follows_following_id_accounts_id_fk", + "tableFrom": "follows", + "tableTo": "accounts", + "columnsFrom": [ + "following_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "follows_follower_id_accounts_id_fk": { + "name": "follows_follower_id_accounts_id_fk", + "tableFrom": "follows", + "tableTo": "accounts", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "follows_following_id_follower_id_pk": { + "name": "follows_following_id_follower_id_pk", + "columns": [ + "following_id", + "follower_id" + ] + } + }, + "uniqueConstraints": { + "follows_iri_unique": { + "name": "follows_iri_unique", + "nullsNotDistinct": false, + "columns": [ + "iri" + ] + } + }, + "policies": {}, + "checkConstraints": { + "ck_follows_self": { + "name": "ck_follows_self", + "value": "\"follows\".\"following_id\" != \"follows\".\"follower_id\"" + } + }, + "isRLSEnabled": false + }, + "public.import_job_items": { + "name": "import_job_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "import_job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "import_job_items_job_id_status_index": { + "name": "import_job_items_job_id_status_index", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "import_job_items_job_id_import_jobs_id_fk": { + "name": "import_job_items_job_id_import_jobs_id_fk", + "tableFrom": "import_job_items", + "tableTo": "import_jobs", + "columnsFrom": [ + "job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.import_jobs": { + "name": "import_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "account_owner_id": { + "name": "account_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "import_job_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "import_job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "total_items": { + "name": "total_items", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processed_items": { + "name": "processed_items", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "successful_items": { + "name": "successful_items", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed_items": { + "name": "failed_items", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "import_jobs_account_owner_id_status_index": { + "name": "import_jobs_account_owner_id_status_index", + "columns": [ + { + "expression": "account_owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "import_jobs_status_created_index": { + "name": "import_jobs_status_created_index", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "import_jobs_account_owner_id_account_owners_id_fk": { + "name": "import_jobs_account_owner_id_account_owners_id_fk", + "tableFrom": "import_jobs", + "tableTo": "account_owners", + "columnsFrom": [ + "account_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instances": { + "name": "instances", + "schema": "", + "columns": { + "host": { + "name": "host", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "software": { + "name": "software", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_version": { + "name": "software_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.likes": { + "name": "likes", + "schema": "", + "columns": { + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "likes_account_id_post_id_index": { + "name": "likes_account_id_post_id_index", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "likes_created_index": { + "name": "likes_created_index", + "columns": [ + { + "expression": "created", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "likes_post_id_posts_id_fk": { + "name": "likes_post_id_posts_id_fk", + "tableFrom": "likes", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "likes_account_id_accounts_id_fk": { + "name": "likes_account_id_accounts_id_fk", + "tableFrom": "likes", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "likes_post_id_account_id_pk": { + "name": "likes_post_id_account_id_pk", + "columns": [ + "post_id", + "account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.list_members": { + "name": "list_members", + "schema": "", + "columns": { + "list_id": { + "name": "list_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "list_members_list_id_lists_id_fk": { + "name": "list_members_list_id_lists_id_fk", + "tableFrom": "list_members", + "tableTo": "lists", + "columnsFrom": [ + "list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "list_members_account_id_accounts_id_fk": { + "name": "list_members_account_id_accounts_id_fk", + "tableFrom": "list_members", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "list_members_list_id_account_id_pk": { + "name": "list_members_list_id_account_id_pk", + "columns": [ + "list_id", + "account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.list_posts": { + "name": "list_posts", + "schema": "", + "columns": { + "list_id": { + "name": "list_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "list_posts_list_id_post_id_index": { + "name": "list_posts_list_id_post_id_index", + "columns": [ + { + "expression": "list_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "list_posts_list_id_lists_id_fk": { + "name": "list_posts_list_id_lists_id_fk", + "tableFrom": "list_posts", + "tableTo": "lists", + "columnsFrom": [ + "list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "list_posts_post_id_posts_id_fk": { + "name": "list_posts_post_id_posts_id_fk", + "tableFrom": "list_posts", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "list_posts_list_id_post_id_pk": { + "name": "list_posts_list_id_post_id_pk", + "columns": [ + "list_id", + "post_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lists": { + "name": "lists", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "account_owner_id": { + "name": "account_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "replies_policy": { + "name": "replies_policy", + "type": "list_replies_policy", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'list'" + }, + "exclusive": { + "name": "exclusive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "lists_account_owner_id_account_owners_id_fk": { + "name": "lists_account_owner_id_account_owners_id_fk", + "tableFrom": "lists", + "tableTo": "account_owners", + "columnsFrom": [ + "account_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.markers": { + "name": "markers", + "schema": "", + "columns": { + "account_owner_id": { + "name": "account_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "marker_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "last_read_id": { + "name": "last_read_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "markers_account_owner_id_account_owners_id_fk": { + "name": "markers_account_owner_id_account_owners_id_fk", + "tableFrom": "markers", + "tableTo": "account_owners", + "columnsFrom": [ + "account_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "markers_account_owner_id_type_pk": { + "name": "markers_account_owner_id_type_pk", + "columns": [ + "account_owner_id", + "type" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "thumbnail_type": { + "name": "thumbnail_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thumbnail_url": { + "name": "thumbnail_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thumbnail_width": { + "name": "thumbnail_width", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "thumbnail_height": { + "name": "thumbnail_height", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "thumbnail_cleaned": { + "name": "thumbnail_cleaned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "media_post_id_index": { + "name": "media_post_id_index", + "columns": [ + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "media_post_id_posts_id_fk": { + "name": "media_post_id_posts_id_fk", + "tableFrom": "media", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mentions": { + "name": "mentions", + "schema": "", + "columns": { + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mentions_post_id_account_id_index": { + "name": "mentions_post_id_account_id_index", + "columns": [ + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mentions_post_id_posts_id_fk": { + "name": "mentions_post_id_posts_id_fk", + "tableFrom": "mentions", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mentions_account_id_accounts_id_fk": { + "name": "mentions_account_id_accounts_id_fk", + "tableFrom": "mentions", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mentions_post_id_account_id_pk": { + "name": "mentions_post_id_account_id_pk", + "columns": [ + "post_id", + "account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mutes": { + "name": "mutes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "muted_account_id": { + "name": "muted_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "notifications": { + "name": "notifications", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "duration": { + "name": "duration", + "type": "interval", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "mutes_account_id_index": { + "name": "mutes_account_id_index", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mutes_account_id_accounts_id_fk": { + "name": "mutes_account_id_accounts_id_fk", + "tableFrom": "mutes", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mutes_muted_account_id_accounts_id_fk": { + "name": "mutes_muted_account_id_accounts_id_fk", + "tableFrom": "mutes", + "tableTo": "accounts", + "columnsFrom": [ + "muted_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mutes_account_id_muted_account_id_unique": { + "name": "mutes_account_id_muted_account_id_unique", + "nullsNotDistinct": false, + "columns": [ + "account_id", + "muted_account_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_groups": { + "name": "notification_groups", + "schema": "", + "columns": { + "group_key": { + "name": "group_key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_owner_id": { + "name": "account_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "target_post_id": { + "name": "target_post_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "notifications_count": { + "name": "notifications_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "most_recent_notification_id": { + "name": "most_recent_notification_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sample_account_ids": { + "name": "sample_account_ids", + "type": "uuid[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::uuid[]" + }, + "latest_page_notification_at": { + "name": "latest_page_notification_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "page_min_id": { + "name": "page_min_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "page_max_id": { + "name": "page_max_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "notification_groups_account_owner_id_updated_index": { + "name": "notification_groups_account_owner_id_updated_index", + "columns": [ + { + "expression": "account_owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_groups_account_owner_id_type_index": { + "name": "notification_groups_account_owner_id_type_index", + "columns": [ + { + "expression": "account_owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notification_groups_account_owner_id_account_owners_id_fk": { + "name": "notification_groups_account_owner_id_account_owners_id_fk", + "tableFrom": "notification_groups", + "tableTo": "account_owners", + "columnsFrom": [ + "account_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_groups_target_post_id_posts_id_fk": { + "name": "notification_groups_target_post_id_posts_id_fk", + "tableFrom": "notification_groups", + "tableTo": "posts", + "columnsFrom": [ + "target_post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_groups_most_recent_notification_id_notifications_id_fk": { + "name": "notification_groups_most_recent_notification_id_notifications_id_fk", + "tableFrom": "notification_groups", + "tableTo": "notifications", + "columnsFrom": [ + "most_recent_notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "account_owner_id": { + "name": "account_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "actor_account_id": { + "name": "actor_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_post_id": { + "name": "target_post_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_account_id": { + "name": "target_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_poll_id": { + "name": "target_poll_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "group_key": { + "name": "group_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "read_at": { + "name": "read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "notifications_account_owner_id_created_index": { + "name": "notifications_account_owner_id_created_index", + "columns": [ + { + "expression": "account_owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_account_owner_id_read_at_index": { + "name": "notifications_account_owner_id_read_at_index", + "columns": [ + { + "expression": "account_owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "read_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_group_key_index": { + "name": "notifications_group_key_index", + "columns": [ + { + "expression": "group_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_created_index": { + "name": "notifications_created_index", + "columns": [ + { + "expression": "created", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_account_owner_id_account_owners_id_fk": { + "name": "notifications_account_owner_id_account_owners_id_fk", + "tableFrom": "notifications", + "tableTo": "account_owners", + "columnsFrom": [ + "account_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_actor_account_id_accounts_id_fk": { + "name": "notifications_actor_account_id_accounts_id_fk", + "tableFrom": "notifications", + "tableTo": "accounts", + "columnsFrom": [ + "actor_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_target_post_id_posts_id_fk": { + "name": "notifications_target_post_id_posts_id_fk", + "tableFrom": "notifications", + "tableTo": "posts", + "columnsFrom": [ + "target_post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_target_account_id_accounts_id_fk": { + "name": "notifications_target_account_id_accounts_id_fk", + "tableFrom": "notifications", + "tableTo": "accounts", + "columnsFrom": [ + "target_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_target_poll_id_polls_id_fk": { + "name": "notifications_target_poll_id_polls_id_fk", + "tableFrom": "notifications", + "tableTo": "polls", + "columnsFrom": [ + "target_poll_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pinned_posts": { + "name": "pinned_posts", + "schema": "", + "columns": { + "index": { + "name": "index", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "pinned_posts_account_id_post_id_index": { + "name": "pinned_posts_account_id_post_id_index", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pinned_posts_account_id_accounts_id_fk": { + "name": "pinned_posts_account_id_accounts_id_fk", + "tableFrom": "pinned_posts", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pinned_posts_post_id_account_id_posts_id_actor_id_fk": { + "name": "pinned_posts_post_id_account_id_posts_id_actor_id_fk", + "tableFrom": "pinned_posts", + "tableTo": "posts", + "columnsFrom": [ + "post_id", + "account_id" + ], + "columnsTo": [ + "id", + "actor_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "pinned_posts_post_id_account_id_unique": { + "name": "pinned_posts_post_id_account_id_unique", + "nullsNotDistinct": false, + "columns": [ + "post_id", + "account_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.poll_options": { + "name": "poll_options", + "schema": "", + "columns": { + "poll_id": { + "name": "poll_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "votes_count": { + "name": "votes_count", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "poll_options_poll_id_index_index": { + "name": "poll_options_poll_id_index_index", + "columns": [ + { + "expression": "poll_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "poll_options_poll_id_polls_id_fk": { + "name": "poll_options_poll_id_polls_id_fk", + "tableFrom": "poll_options", + "tableTo": "polls", + "columnsFrom": [ + "poll_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "poll_options_poll_id_index_pk": { + "name": "poll_options_poll_id_index_pk", + "columns": [ + "poll_id", + "index" + ] + } + }, + "uniqueConstraints": { + "poll_options_poll_id_title_unique": { + "name": "poll_options_poll_id_title_unique", + "nullsNotDistinct": false, + "columns": [ + "poll_id", + "title" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.poll_votes": { + "name": "poll_votes", + "schema": "", + "columns": { + "poll_id": { + "name": "poll_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "option_index": { + "name": "option_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "poll_votes_poll_id_account_id_index": { + "name": "poll_votes_poll_id_account_id_index", + "columns": [ + { + "expression": "poll_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "poll_votes_poll_id_polls_id_fk": { + "name": "poll_votes_poll_id_polls_id_fk", + "tableFrom": "poll_votes", + "tableTo": "polls", + "columnsFrom": [ + "poll_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "poll_votes_account_id_accounts_id_fk": { + "name": "poll_votes_account_id_accounts_id_fk", + "tableFrom": "poll_votes", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "poll_votes_poll_id_option_index_poll_options_poll_id_index_fk": { + "name": "poll_votes_poll_id_option_index_poll_options_poll_id_index_fk", + "tableFrom": "poll_votes", + "tableTo": "poll_options", + "columnsFrom": [ + "poll_id", + "option_index" + ], + "columnsTo": [ + "poll_id", + "index" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "poll_votes_poll_id_option_index_account_id_pk": { + "name": "poll_votes_poll_id_option_index_account_id_pk", + "columns": [ + "poll_id", + "option_index", + "account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.polls": { + "name": "polls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "multiple": { + "name": "multiple", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "voters_count": { + "name": "voters_count", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "expires": { + "name": "expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.posts": { + "name": "posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "iri": { + "name": "iri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "post_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reply_target_id": { + "name": "reply_target_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sharing_id": { + "name": "sharing_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "quote_target_id": { + "name": "quote_target_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "post_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_html": { + "name": "content_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "poll_id": { + "name": "poll_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "emojis": { + "name": "emojis", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "sensitive": { + "name": "sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_card": { + "name": "preview_card", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "replies_count": { + "name": "replies_count", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "shares_count": { + "name": "shares_count", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "likes_count": { + "name": "likes_count", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "quotes_count": { + "name": "quotes_count", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "idempotence_key": { + "name": "idempotence_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published": { + "name": "published", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "posts_sharing_id_index": { + "name": "posts_sharing_id_index", + "columns": [ + { + "expression": "sharing_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_actor_id_index": { + "name": "posts_actor_id_index", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_actor_id_sharing_id_index": { + "name": "posts_actor_id_sharing_id_index", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sharing_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_reply_target_id_index": { + "name": "posts_reply_target_id_index", + "columns": [ + { + "expression": "reply_target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_actor_id_reply_target_id_index": { + "name": "posts_actor_id_reply_target_id_index", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reply_target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_quote_target_id_index": { + "name": "posts_quote_target_id_index", + "columns": [ + { + "expression": "quote_target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"posts\".\"quote_target_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_visibility_actor_id_index": { + "name": "posts_visibility_actor_id_index", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_visibility_actor_id_sharing_id_index": { + "name": "posts_visibility_actor_id_sharing_id_index", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sharing_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"posts\".\"sharing_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_visibility_actor_id_reply_target_id_index": { + "name": "posts_visibility_actor_id_reply_target_id_index", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reply_target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"posts\".\"reply_target_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "posts_actor_id_accounts_id_fk": { + "name": "posts_actor_id_accounts_id_fk", + "tableFrom": "posts", + "tableTo": "accounts", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "posts_application_id_applications_id_fk": { + "name": "posts_application_id_applications_id_fk", + "tableFrom": "posts", + "tableTo": "applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "posts_reply_target_id_posts_id_fk": { + "name": "posts_reply_target_id_posts_id_fk", + "tableFrom": "posts", + "tableTo": "posts", + "columnsFrom": [ + "reply_target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "posts_sharing_id_posts_id_fk": { + "name": "posts_sharing_id_posts_id_fk", + "tableFrom": "posts", + "tableTo": "posts", + "columnsFrom": [ + "sharing_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "posts_quote_target_id_posts_id_fk": { + "name": "posts_quote_target_id_posts_id_fk", + "tableFrom": "posts", + "tableTo": "posts", + "columnsFrom": [ + "quote_target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "posts_poll_id_polls_id_fk": { + "name": "posts_poll_id_polls_id_fk", + "tableFrom": "posts", + "tableTo": "polls", + "columnsFrom": [ + "poll_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "posts_iri_unique": { + "name": "posts_iri_unique", + "nullsNotDistinct": false, + "columns": [ + "iri" + ] + }, + "posts_id_actor_id_unique": { + "name": "posts_id_actor_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "actor_id" + ] + }, + "posts_poll_id_unique": { + "name": "posts_poll_id_unique", + "nullsNotDistinct": false, + "columns": [ + "poll_id" + ] + }, + "posts_actor_id_sharing_id_unique": { + "name": "posts_actor_id_sharing_id_unique", + "nullsNotDistinct": false, + "columns": [ + "actor_id", + "sharing_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reactions": { + "name": "reactions", + "schema": "", + "columns": { + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "custom_emoji": { + "name": "custom_emoji", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emoji_iri": { + "name": "emoji_iri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "reactions_post_id_index": { + "name": "reactions_post_id_index", + "columns": [ + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reactions_post_id_account_id_index": { + "name": "reactions_post_id_account_id_index", + "columns": [ + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reactions_created_index": { + "name": "reactions_created_index", + "columns": [ + { + "expression": "created", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reactions_post_id_posts_id_fk": { + "name": "reactions_post_id_posts_id_fk", + "tableFrom": "reactions", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reactions_account_id_accounts_id_fk": { + "name": "reactions_account_id_accounts_id_fk", + "tableFrom": "reactions", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "reactions_post_id_account_id_emoji_pk": { + "name": "reactions_post_id_account_id_emoji_pk", + "columns": [ + "post_id", + "account_id", + "emoji" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reports": { + "name": "reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "iri": { + "name": "iri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_account_id": { + "name": "target_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "posts": { + "name": "posts", + "type": "uuid[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::uuid[]" + } + }, + "indexes": {}, + "foreignKeys": { + "reports_account_id_accounts_id_fk": { + "name": "reports_account_id_accounts_id_fk", + "tableFrom": "reports", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reports_target_account_id_accounts_id_fk": { + "name": "reports_target_account_id_accounts_id_fk", + "tableFrom": "reports", + "tableTo": "accounts", + "columnsFrom": [ + "target_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "reports_iri_unique": { + "name": "reports_iri_unique", + "nullsNotDistinct": false, + "columns": [ + "iri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.timeline_posts": { + "name": "timeline_posts", + "schema": "", + "columns": { + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "timeline_posts_account_id_post_id_index": { + "name": "timeline_posts_account_id_post_id_index", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "timeline_posts_account_id_account_owners_id_fk": { + "name": "timeline_posts_account_id_account_owners_id_fk", + "tableFrom": "timeline_posts", + "tableTo": "account_owners", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "timeline_posts_post_id_posts_id_fk": { + "name": "timeline_posts_post_id_posts_id_fk", + "tableFrom": "timeline_posts", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "timeline_posts_account_id_post_id_pk": { + "name": "timeline_posts_account_id_post_id_pk", + "columns": [ + "account_id", + "post_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.totps": { + "name": "totps", + "schema": "", + "columns": { + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "digits": { + "name": "digits", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "period": { + "name": "period", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.account_type": { + "name": "account_type", + "schema": "public", + "values": [ + "Application", + "Group", + "Organization", + "Person", + "Service" + ] + }, + "public.cleanup_job_category": { + "name": "cleanup_job_category", + "schema": "public", + "values": [ + "cleanup_thumbnails" + ] + }, + "public.cleanup_job_status": { + "name": "cleanup_job_status", + "schema": "public", + "values": [ + "pending", + "processing", + "completed", + "failed", + "cancelled" + ] + }, + "public.grant_type": { + "name": "grant_type", + "schema": "public", + "values": [ + "authorization_code", + "client_credentials" + ] + }, + "public.import_job_category": { + "name": "import_job_category", + "schema": "public", + "values": [ + "following_accounts", + "lists", + "muted_accounts", + "blocked_accounts", + "bookmarks" + ] + }, + "public.import_job_status": { + "name": "import_job_status", + "schema": "public", + "values": [ + "pending", + "processing", + "completed", + "failed", + "cancelled" + ] + }, + "public.list_replies_policy": { + "name": "list_replies_policy", + "schema": "public", + "values": [ + "followed", + "list", + "none" + ] + }, + "public.marker_type": { + "name": "marker_type", + "schema": "public", + "values": [ + "notifications", + "home" + ] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": [ + "mention", + "status", + "reblog", + "follow", + "follow_request", + "favourite", + "emoji_reaction", + "poll", + "update", + "admin.sign_up", + "admin.report", + "quote", + "quoted_update" + ] + }, + "public.post_type": { + "name": "post_type", + "schema": "public", + "values": [ + "Article", + "Note", + "Question" + ] + }, + "public.post_visibility": { + "name": "post_visibility", + "schema": "public", + "values": [ + "public", + "unlisted", + "private", + "direct" + ] + }, + "public.scope": { + "name": "scope", + "schema": "public", + "values": [ + "read", + "read:accounts", + "read:blocks", + "read:bookmarks", + "read:favourites", + "read:filters", + "read:follows", + "read:lists", + "read:mutes", + "read:notifications", + "read:search", + "read:statuses", + "write", + "write:accounts", + "write:blocks", + "write:bookmarks", + "write:conversations", + "write:favourites", + "write:filters", + "write:follows", + "write:lists", + "write:media", + "write:mutes", + "write:notifications", + "write:reports", + "write:statuses", + "follow", + "push", + "profile" + ] + }, + "public.theme_color": { + "name": "theme_color", + "schema": "public", + "values": [ + "amber", + "azure", + "blue", + "cyan", + "fuchsia", + "green", + "grey", + "indigo", + "jade", + "lime", + "orange", + "pink", + "pumpkin", + "purple", + "red", + "sand", + "slate", + "violet", + "yellow", + "zinc" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 62bd9521..7be0b395 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -568,6 +568,13 @@ "when": 1776805400028, "tag": "0080_add_thumbnail_cleaned_flag", "breakpoints": true + }, + { + "idx": 81, + "version": "7", + "when": 1776805957287, + "tag": "0081_add_cleanup_jobs", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/cleanup/processors.ts b/src/cleanup/processors.ts new file mode 100644 index 00000000..1813eb08 --- /dev/null +++ b/src/cleanup/processors.ts @@ -0,0 +1,52 @@ +import { getLogger } from "@logtape/logtape"; +import { eq } from "drizzle-orm"; + +import db from "../db"; +import * as schema from "../schema"; +import { drive } from "../storage"; +import { STORAGE_URL_BASE } from "../storage-config"; +import type { Uuid } from "../uuid"; + +const logger = getLogger(["hollo", "cleanup-processors"]); + +// Type for thumbnail cleanup item data +interface ThumbnailCleanupItemData { + id: Uuid; +} + +export async function processThumbnailDeletion( + item: schema.CleanupJobItem, +): Promise { + const data = item.data as unknown as ThumbnailCleanupItemData; + + const medium = await db.query.media.findFirst({ + where: eq(schema.media.id, data.id), + }); + + if (medium == null) { + logger.error("medium missing in database: {id}", { id: data.id }); + throw new Error(`medium missing in database: ${data.id}`); + } + + if (!medium.thumbnailUrl.startsWith(STORAGE_URL_BASE as string)) { + logger.error( + "The thumbnail URL {thumbnailUrl} does not match the storage URL pattern {storageUrlBase}!", + { + thumbnailUrl: medium.thumbnailUrl, + STORAGE_URL_BASE, + }, + ); + throw new Error( + `The thumbnail URL ${medium.thumbnailUrl} does not match the storage URL pattern ${STORAGE_URL_BASE}!`, + ); + } + + const key = medium.thumbnailUrl.replace(STORAGE_URL_BASE as string, ""); + + const disk = drive.use(); + await disk.delete(key); + await db + .update(schema.media) + .set({ thumbnailCleaned: true }) + .where(eq(schema.media.id, medium.id)); +} diff --git a/src/cleanup/worker.ts b/src/cleanup/worker.ts new file mode 100644 index 00000000..9df5e3b3 --- /dev/null +++ b/src/cleanup/worker.ts @@ -0,0 +1,266 @@ +import { getLogger } from "@logtape/logtape"; +import { and, eq, inArray, sql } from "drizzle-orm"; + +import db, { type Transaction } from "../db"; +import * as schema from "../schema"; +import { processThumbnailDeletion } from "./processors"; + +const logger = getLogger(["hollo", "cleanup-worker"]); + +// Configuration constants +const POLL_INTERVAL_MS = 5000; // Check for jobs every 5 seconds +const BATCH_SIZE = 100; // Items to fetch per poll +const CONCURRENT_ITEMS = 10; // Max parallel item processing + +let isRunning = false; +let pollTimer: ReturnType | null = null; + +export function startCleanupWorker(): void { + if (isRunning) { + logger.warn("Cleanup worker is already running"); + return; + } + + isRunning = true; + logger.info("Starting cleanup worker"); + + // Initial poll + pollAndProcess().catch((error) => { + logger.error("Error in cleanup cleanup worker poll: {error}", { error }); + }); + + // Set up periodic polling + pollTimer = setInterval(() => { + pollAndProcess().catch((error) => { + logger.error("Error in cleanup worker poll: {error}", { error }); + }); + }, POLL_INTERVAL_MS); +} + +export function stopCleanupWorker(): void { + if (pollTimer) { + clearInterval(pollTimer); + pollTimer = null; + } + isRunning = false; + logger.info("Cleanup worker stopped"); +} + +async function pollAndProcess(): Promise { + try { + // Use a transaction with FOR UPDATE SKIP LOCKED to prevent + // multiple workers from processing the same job + await db.transaction(async (tx) => { + // Find pending jobs and start them (only process one job at a time) + const [pendingJob] = await tx + .select() + .from(schema.cleanupJobs) + .where(eq(schema.cleanupJobs.status, "pending")) + .orderBy(schema.cleanupJobs.created) + .limit(1) + .for("update", { skipLocked: true }); + + if (pendingJob) { + await startJob(tx, pendingJob); + return; // Process one job per poll cycle + } + + // Continue processing jobs that are already "processing" + const [processingJob] = await tx + .select() + .from(schema.cleanupJobs) + .where(eq(schema.cleanupJobs.status, "processing")) + .limit(1) + .for("update", { skipLocked: true }); + + if (processingJob) { + await processJobItems(tx, processingJob); + } + }); + } catch (error) { + logger.error("Error in cleanup worker poll: {error}", { error }); + } +} + +async function startJob( + tx: Transaction, + job: schema.CleanupJob, +): Promise { + logger.info("Starting cleanup job {jobId} for category {category}", { + jobId: job.id, + category: job.category, + }); + + await tx + .update(schema.cleanupJobs) + .set({ + status: "processing", + startedAt: new Date(), + }) + .where(eq(schema.cleanupJobs.id, job.id)); + + await processJobItems(tx, { + ...job, + status: "processing", + startedAt: new Date(), + }); +} + +async function processJobItems( + tx: Transaction, + job: schema.CleanupJob, +): Promise { + // Check if job has been cancelled (use tx to see latest state within transaction) + const [currentJob] = await tx + .select() + .from(schema.cleanupJobs) + .where(eq(schema.cleanupJobs.id, job.id)); + + if (!currentJob || currentJob.status === "cancelled") { + logger.info("Cleanup job {jobId} was cancelled", { jobId: job.id }); + await finalizeJob(tx, job, "cancelled"); + return; + } + + // Get pending items for this job with lock + const pendingItems = await tx + .select() + .from(schema.cleanupJobItems) + .where( + and( + eq(schema.cleanupJobItems.jobId, job.id), + eq(schema.cleanupJobItems.status, "pending"), + ), + ) + .limit(BATCH_SIZE) + .for("update", { skipLocked: true }); + + if (pendingItems.length === 0) { + // No more items - mark job as completed + await finalizeJob(tx, job, "completed"); + return; + } + + // Mark items as processing within the transaction + const itemsToProcess = pendingItems.slice(0, CONCURRENT_ITEMS); + const itemIds = itemsToProcess.map((item) => item.id); + + await tx + .update(schema.cleanupJobItems) + .set({ status: "processing" }) + .where( + and( + eq(schema.cleanupJobItems.jobId, job.id), + inArray(schema.cleanupJobItems.id, itemIds), + ), + ); + + // Update processed count within transaction + await tx + .update(schema.cleanupJobs) + .set({ + processedItems: sql`${schema.cleanupJobs.processedItems} + ${itemsToProcess.length}`, + }) + .where(eq(schema.cleanupJobs.id, job.id)); + + // Process items outside the transaction (federation calls are external) + // We schedule this to run after the transaction commits + setTimeout(() => { + Promise.allSettled( + itemsToProcess.map((item) => processItem(job, item)), + ).catch((error) => { + logger.error("Error processing cleanup items: {error}", { error }); + }); + }, 0); +} + +async function finalizeJob( + tx: Transaction, + job: schema.CleanupJob, + status: "completed" | "cancelled" | "failed", +): Promise { + const [stats] = await tx + .select({ + successful: + sql`COUNT(*) FILTER (WHERE status = 'completed')`.mapWith( + Number, + ), + failed: sql`COUNT(*) FILTER (WHERE status = 'failed')`.mapWith( + Number, + ), + }) + .from(schema.cleanupJobItems) + .where(eq(schema.cleanupJobItems.jobId, job.id)); + + await tx + .update(schema.cleanupJobs) + .set({ + status, + completedAt: new Date(), + successfulItems: stats.successful, + failedItems: stats.failed, + }) + .where(eq(schema.cleanupJobs.id, job.id)); + + logger.info( + "Cleanup job {jobId} {status}: {successful} successful, {failed} failed", + { + jobId: job.id, + status, + successful: stats.successful, + failed: stats.failed, + }, + ); +} + +async function processItem( + job: schema.CleanupJob, + item: schema.CleanupJobItem, +): Promise { + // Item is already marked as "processing" in the transaction + try { + switch (job.category) { + case "cleanup_thumbnails": + await processThumbnailDeletion(item); + break; + } + + await db + .update(schema.cleanupJobItems) + .set({ + status: "completed", + processedAt: new Date(), + }) + .where(eq(schema.cleanupJobItems.id, item.id)); + + // Update successful count + await db + .update(schema.cleanupJobs) + .set({ + successfulItems: sql`${schema.cleanupJobs.successfulItems} + 1`, + }) + .where(eq(schema.cleanupJobs.id, job.id)); + } catch (error) { + logger.error("Failed to process cleanup item {itemId}: {error}", { + itemId: item.id, + error, + }); + + await db + .update(schema.cleanupJobItems) + .set({ + status: "failed", + errorMessage: error instanceof Error ? error.message : String(error), + processedAt: new Date(), + }) + .where(eq(schema.cleanupJobItems.id, item.id)); + + // Update failed count + await db + .update(schema.cleanupJobs) + .set({ + failedItems: sql`${schema.cleanupJobs.failedItems} + 1`, + }) + .where(eq(schema.cleanupJobs.id, job.id)); + } +} diff --git a/src/schema.ts b/src/schema.ts index 8adb271b..b613f3a1 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1382,3 +1382,84 @@ export const importJobItemRelations = relations(importJobItems, ({ one }) => ({ references: [importJobs.id], }), })); + +// Cleanup Job Status Enum +export const cleanupJobStatusEnum = pgEnum("cleanup_job_status", [ + "pending", + "processing", + "completed", + "failed", + "cancelled", +]); + +export type CleanupJobStatus = (typeof cleanupJobStatusEnum.enumValues)[number]; + +// Cleanup Job Category Enum +export const cleanupJobCategoryEnum = pgEnum("cleanup_job_category", [ + "cleanup_thumbnails", +]); + +export type CleanupJobCategory = + (typeof cleanupJobCategoryEnum.enumValues)[number]; + +// Cleanup Jobs Table +export const cleanupJobs = pgTable( + "cleanup_jobs", + { + id: uuid("id").$type().primaryKey(), + category: cleanupJobCategoryEnum("category").notNull(), + status: cleanupJobStatusEnum("status").notNull().default("pending"), + totalItems: integer("total_items").notNull().default(0), + processedItems: integer("processed_items").notNull().default(0), + successfulItems: integer("successful_items").notNull().default(0), + failedItems: integer("failed_items").notNull().default(0), + errorMessage: text("error_message"), + created: timestamp("created", { withTimezone: true }) + .notNull() + .default(currentTimestamp), + startedAt: timestamp("started_at", { withTimezone: true }), + completedAt: timestamp("completed_at", { withTimezone: true }), + }, + (table) => [index().on(table.status, table.created)], +); + +export type CleanupJob = typeof cleanupJobs.$inferSelect; +export type NewCleanupJob = typeof cleanupJobs.$inferInsert; + +// Cleanup Job Items Table +export const cleanupJobItems = pgTable( + "cleanup_job_items", + { + id: uuid("id").$type().primaryKey(), + jobId: uuid("job_id") + .$type() + .notNull() + .references(() => cleanupJobs.id, { onDelete: "cascade" }), + status: cleanupJobStatusEnum("status").notNull().default("pending"), + data: jsonb("data").notNull().$type>(), + errorMessage: text("error_message"), + processedAt: timestamp("processed_at", { withTimezone: true }), + created: timestamp("created", { withTimezone: true }) + .notNull() + .default(currentTimestamp), + }, + (table) => [index().on(table.jobId, table.status)], +); + +export type CleanupJobItem = typeof cleanupJobItems.$inferSelect; +export type NewCleanupJobItem = typeof cleanupJobItems.$inferInsert; + +// Cleanup Job Relations +export const cleanupJobRelations = relations(cleanupJobs, ({ many }) => ({ + items: many(cleanupJobItems), +})); + +export const cleanupJobItemRelations = relations( + cleanupJobItems, + ({ one }) => ({ + job: one(cleanupJobs, { + fields: [cleanupJobItems.jobId], + references: [cleanupJobs.id], + }), + }), +); From 42d9b3bf9005410ed8d724815815257db3a988b2 Mon Sep 17 00:00:00 2001 From: aliceif <7098860+aliceif@users.noreply.github.com> Date: Sun, 19 Apr 2026 22:40:04 +0200 Subject: [PATCH 04/16] add query for media for thumbnail cleanup to entities file --- src/entities/medium.ts | 124 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 123 insertions(+), 1 deletion(-) diff --git a/src/entities/medium.ts b/src/entities/medium.ts index 073b321d..47942e75 100644 --- a/src/entities/medium.ts +++ b/src/entities/medium.ts @@ -1,4 +1,18 @@ -import type { Medium } from "../schema"; +import { and, eq, exists, ilike, lt, not, notExists } from "drizzle-orm"; +import { alias } from "drizzle-orm/pg-core"; + +import db from "../db"; +import { + accountOwners, + accounts, + bookmarks, + likes, + type Medium, + media, + posts, + reactions, +} from "../schema"; +import { STORAGE_URL_BASE } from "../storage-config"; function normalizeAttachmentType(type: string): string { if (["image", "video", "audio", "gifv", "unknown"].includes(type)) { @@ -38,3 +52,111 @@ export function serializeMedium(medium: Medium): Record { blurhash: null, }; } + +export async function getMediaWithDeletableThumbnails( + before: Date, +): Promise { + const sharingPosts = alias(posts, "sharingPosts"); + const quotingPosts = alias(posts, "quotingPosts"); + + return await db + .select({ + id: media.id, + type: media.type, + url: media.url, + description: media.description, + postId: media.postId, + width: media.width, + height: media.height, + thumbnailType: media.thumbnailType, + thumbnailWidth: media.thumbnailWidth, + thumbnailHeight: media.thumbnailHeight, + thumbnailUrl: media.thumbnailUrl, + thumbnailCleaned: media.thumbnailCleaned, + created: media.created, + }) + .from(media) + .innerJoin(posts, eq(media.postId, posts.id)) + .innerJoin(accounts, eq(posts.accountId, accounts.id)) + .where( + and( + not(media.thumbnailCleaned), + ilike(media.thumbnailUrl, `${STORAGE_URL_BASE}%`), + lt(media.created, before), + notExists( + db + .select() + .from(accountOwners) + .where(eq(accounts.id, accountOwners.id)), + ), + notExists( + db.select().from(bookmarks).where(eq(posts.id, bookmarks.postId)), + ), + notExists( + db + .select() + .from(likes) + .where( + and( + eq(posts.id, likes.postId), + exists( + db + .select() + .from(accountOwners) + .where(eq(likes.accountId, accountOwners.id)), + ), + ), + ), + ), + notExists( + db + .select() + .from(reactions) + .where( + and( + eq(posts.id, reactions.postId), + exists( + db + .select() + .from(accountOwners) + .where(eq(reactions.accountId, accountOwners.id)), + ), + ), + ), + ), + notExists( + db + .select() + .from(sharingPosts) + .where( + and( + eq(posts.id, sharingPosts.sharingId), + exists( + db + .select() + .from(accountOwners) + .where(eq(sharingPosts.accountId, accountOwners.id)), + ), + ), + ), + ), + notExists( + db + .select() + .from(quotingPosts) + .where( + and( + eq(posts.id, quotingPosts.quoteTargetId), + exists( + db + .select() + .from(accountOwners) + .where(eq(quotingPosts.accountId, accountOwners.id)), + ), + ), + ), + ), + ), + ) + .orderBy(media.created); +} From 6f8e366c29b81ea267d28b039b1c9d37a676e2ba Mon Sep 17 00:00:00 2001 From: aliceif <7098860+aliceif@users.noreply.github.com> Date: Sun, 19 Apr 2026 22:40:32 +0200 Subject: [PATCH 05/16] make thumbnail_cleanup page worker-based --- src/pages/thumbnail_cleanup.tsx | 716 +++++++++----------------------- 1 file changed, 195 insertions(+), 521 deletions(-) diff --git a/src/pages/thumbnail_cleanup.tsx b/src/pages/thumbnail_cleanup.tsx index 09adbb73..52fb9d39 100644 --- a/src/pages/thumbnail_cleanup.tsx +++ b/src/pages/thumbnail_cleanup.tsx @@ -1,24 +1,21 @@ -import { Temporal } from "@js-temporal/polyfill"; import { getLogger } from "@logtape/logtape"; -import { and, count, eq, exists, ilike, lt, not, notExists } from "drizzle-orm"; -import { alias } from "drizzle-orm/pg-core"; +import { and, count, eq, ilike, inArray, not, notExists } from "drizzle-orm"; import { Hono } from "hono"; import { DashboardLayout } from "../components/DashboardLayout"; import db from "../db"; +import { getMediaWithDeletableThumbnails } from "../entities/medium"; import { loginRequired } from "../login"; import { accountOwners, accounts, - bookmarks, - likes, + cleanupJobItems, + cleanupJobs, media, posts, - reactions, } from "../schema"; -import { drive } from "../storage"; import { STORAGE_URL_BASE } from "../storage-config"; -import type { Uuid } from "../uuid"; +import { isUuid, uuidv7 } from "../uuid"; const logger = getLogger(["hollo", "pages", "thumbnail_cleanup"]); @@ -33,9 +30,7 @@ data.get("/", async (c) => { const fileCount = c.req.query("fileCount"); const firstFile = c.req.query("firstFile"); const lastFile = c.req.query("lastFile"); - const todo = c.req.query("todo"); - const processed = c.req.query("processed"); - const deleted = c.req.query("deleted"); + const cleanupDataResult = c.req.query("cleanup-data-result"); const suggestedCleanupCutoff = typeof before === "string" @@ -44,261 +39,29 @@ data.get("/", async (c) => { .toISOString() .split("T")[0]; - const sharingPosts = alias(posts, "sharingPosts"); - const quotingPosts = alias(posts, "quotingPosts"); - - let thumbnailsBeforeLastYearAndOnlyMaybeRepliedResult: { count: number }[]; - try { - thumbnailsBeforeLastYearAndOnlyMaybeRepliedResult = await db - .select({ - count: count(), - }) - .from(media) - .innerJoin(posts, eq(media.postId, posts.id)) - .innerJoin(accounts, eq(posts.accountId, accounts.id)) - .where( - and( - not(media.thumbnailCleaned), - ilike(media.thumbnailUrl, `${STORAGE_URL_BASE}%`), - lt(media.created, new Date(new Date().getFullYear() - 1, 0, 1)), - notExists( - db - .select() - .from(accountOwners) - .where(eq(accounts.id, accountOwners.id)), - ), - notExists( - db.select().from(bookmarks).where(eq(posts.id, bookmarks.postId)), - ), - notExists( - db - .select() - .from(likes) - .where( - and( - eq(posts.id, likes.postId), - exists( - db - .select() - .from(accountOwners) - .where(eq(likes.accountId, accountOwners.id)), - ), - ), - ), - ), - notExists( - db - .select() - .from(reactions) - .where( - and( - eq(posts.id, reactions.postId), - exists( - db - .select() - .from(accountOwners) - .where(eq(reactions.accountId, accountOwners.id)), - ), - ), - ), + // Check for active cleanup job (from query param or database) + const cleanupJobId = c.req.query("cleanup-job"); + const activeJob = + cleanupJobId && isUuid(cleanupJobId) + ? await db.query.cleanupJobs.findFirst({ + where: and(eq(cleanupJobs.id, cleanupJobId)), + }) + : await db.query.cleanupJobs.findFirst({ + where: and( + inArray(cleanupJobs.status, ["pending", "processing"]), + inArray(cleanupJobs.category, ["cleanup_thumbnails"]), ), - notExists( - db - .select() - .from(sharingPosts) - .where( - and( - eq(posts.id, sharingPosts.sharingId), - exists( - db - .select() - .from(accountOwners) - .where(eq(sharingPosts.accountId, accountOwners.id)), - ), - ), - ), - ), - notExists( - db - .select() - .from(quotingPosts) - .where( - and( - eq(posts.id, quotingPosts.quoteTargetId), - exists( - db - .select() - .from(accountOwners) - .where(eq(quotingPosts.accountId, accountOwners.id)), - ), - ), - ), - ), - ), - ); - } catch { - thumbnailsBeforeLastYearAndOnlyMaybeRepliedResult = [{ count: 0 }]; - } - const thumbnailsBeforeLastYearAndOnlyMaybeRepliedCount = - thumbnailsBeforeLastYearAndOnlyMaybeRepliedResult[0].count; + orderBy: (cleanupJobs, { desc }) => [desc(cleanupJobs.created)], + }); - let thumbnailsBeforeLastYearResult: { count: number }[]; - try { - thumbnailsBeforeLastYearResult = await db - .select({ - count: count(), - }) - .from(media) - .innerJoin(posts, eq(media.postId, posts.id)) - .innerJoin(accounts, eq(posts.accountId, accounts.id)) - .where( - and( - not(media.thumbnailCleaned), - ilike(media.thumbnailUrl, `${STORAGE_URL_BASE}%`), - lt(media.created, new Date(new Date().getFullYear() - 1, 0, 1)), - notExists( - db - .select() - .from(accountOwners) - .where(eq(accounts.id, accountOwners.id)), - ), - ), - ); - } catch { - thumbnailsBeforeLastYearResult = [{ count: 0 }]; - } - const thumbnailsBeforeLastYearCount = thumbnailsBeforeLastYearResult[0].count; - - const oneYearAgo = new Date( - Temporal.Now.zonedDateTimeISO().subtract(new Temporal.Duration(1)) - .epochMilliseconds, - ); - - let thumbnailsYearOldAndOnlyMaybeRepliedResult: { count: number }[]; - try { - thumbnailsYearOldAndOnlyMaybeRepliedResult = await db - .select({ - count: count(), - }) - .from(media) - .innerJoin(posts, eq(media.postId, posts.id)) - .innerJoin(accounts, eq(posts.accountId, accounts.id)) - .where( - and( - not(media.thumbnailCleaned), - ilike(media.thumbnailUrl, `${STORAGE_URL_BASE}%`), - lt(media.created, oneYearAgo), - notExists( - db - .select() - .from(accountOwners) - .where(eq(accounts.id, accountOwners.id)), - ), - notExists( - db.select().from(bookmarks).where(eq(posts.id, bookmarks.postId)), - ), - notExists( - db - .select() - .from(likes) - .where( - and( - eq(posts.id, likes.postId), - exists( - db - .select() - .from(accountOwners) - .where(eq(likes.accountId, accountOwners.id)), - ), - ), - ), - ), - notExists( - db - .select() - .from(reactions) - .where( - and( - eq(posts.id, reactions.postId), - exists( - db - .select() - .from(accountOwners) - .where(eq(reactions.accountId, accountOwners.id)), - ), - ), - ), - ), - notExists( - db - .select() - .from(sharingPosts) - .where( - and( - eq(posts.id, sharingPosts.sharingId), - exists( - db - .select() - .from(accountOwners) - .where(eq(sharingPosts.accountId, accountOwners.id)), - ), - ), - ), - ), - notExists( - db - .select() - .from(quotingPosts) - .where( - and( - eq(posts.id, quotingPosts.quoteTargetId), - exists( - db - .select() - .from(accountOwners) - .where(eq(quotingPosts.accountId, accountOwners.id)), - ), - ), - ), - ), - ), - ); - } catch { - thumbnailsYearOldAndOnlyMaybeRepliedResult = [{ count: 0 }]; - } - const thumbnailsYearOldAndOnlyMaybeRepliedCount = - thumbnailsYearOldAndOnlyMaybeRepliedResult[0].count; - - let thumbnailsYearOldResult: { count: number }[]; - try { - thumbnailsYearOldResult = await db - .select({ - count: count(), - }) - .from(media) - .innerJoin(posts, eq(media.postId, posts.id)) - .innerJoin(accounts, eq(posts.accountId, accounts.id)) - .where( - and( - not(media.thumbnailCleaned), - ilike(media.thumbnailUrl, `${STORAGE_URL_BASE}%`), - lt(media.created, oneYearAgo), - notExists( - db - .select() - .from(accountOwners) - .where(eq(accounts.id, accountOwners.id)), - ), - ), - ); - } catch { - thumbnailsYearOldResult = [{ count: 0 }]; - } - const thumbnailsYearOldCount = thumbnailsYearOldResult[0].count; + // Check if we need to auto-refresh (job in progress) + const shouldAutoRefresh = + activeJob?.status === "pending" || activeJob?.status === "processing"; - let remoteThumbnailsResult: { count: number }[]; + // compute statistics table + let remoteThumbnailsCountResult: { count: number }[]; try { - remoteThumbnailsResult = await db + remoteThumbnailsCountResult = await db .select({ count: count(), }) @@ -318,9 +81,9 @@ data.get("/", async (c) => { ), ); } catch { - remoteThumbnailsResult = [{ count: 0 }]; + remoteThumbnailsCountResult = [{ count: 0 }]; } - const thumbnailsRemoteCount = remoteThumbnailsResult[0].count; + const thumbnailsRemoteCount = remoteThumbnailsCountResult[0].count; let thumbnailsCountResult: { count: number }[]; try { @@ -341,28 +104,13 @@ data.get("/", async (c) => { const thumbnailsCount = thumbnailsCountResult[0].count; const thumbnailsTable: { caption: string; count: number }[] = [ - { caption: "Total, thumbnail hosted locally", count: thumbnailsCount }, - { - caption: "Remote, thumbnail hosted locally", - count: thumbnailsRemoteCount, - }, { - caption: "From before 1 year ago, remote, thumbnail hosted locally", - count: thumbnailsYearOldCount, + caption: "Total, thumbnail hosted locally", + count: thumbnailsCount, }, { - caption: - "From before 1 year ago, remote, thumbnail hosted locally, not interacted with outside of maybe replying", - count: thumbnailsYearOldAndOnlyMaybeRepliedCount, - }, - { - caption: "From before last year, remote, thumbnail hosted locally", - count: thumbnailsBeforeLastYearCount, - }, - { - caption: - "From before last year, remote, thumbnail hosted locally, not interacted with outside of maybe replying", - count: thumbnailsBeforeLastYearAndOnlyMaybeRepliedCount, + caption: "Remote, thumbnail hosted locally", + count: thumbnailsRemoteCount, }, ]; @@ -402,7 +150,7 @@ data.get("/", async (c) => { -
    +

    Preview cleanup

    @@ -427,11 +175,11 @@ data.get("/", async (c) => { aria-invalid={error === "clean" ? "true" : undefined} /> {error === "clean_preview" ? ( - Something went wrong while cleaning up. + Something went wrong while previewing the cleanup. ) : ( The date before which remote thumbnails get deleted. )} @@ -447,25 +195,120 @@ data.get("/", async (c) => {

    )}
    -
    + + {/* Cleanup Progress Section */} + {activeJob && ( +
    +
    +
    +

    + {activeJob.status === "pending" + ? "Cleanup Queued" + : activeJob.status === "processing" + ? "Cleanup in Progress" + : activeJob.status === "completed" + ? "Cleanup Completed" + : activeJob.status === "cancelled" + ? "Cleanup Cancelled" + : "Cleanup Failed"} +

    +

    + Cleanup + {activeJob.status === "pending" && " waiting to start"} + {activeJob.status === "processing" && " processing..."} +

    +
    +
    + + + +

    + {activeJob.processedItems.toLocaleString("en-US")}{" "} + / {activeJob.totalItems.toLocaleString("en-US")} items processed + {activeJob.processedItems > 0 && ( + <> + {" "} + ( + + {activeJob.successfulItems.toLocaleString("en-US")} + {" "} + successful + {activeJob.failedItems > 0 && ( + <> + ,{" "} + + {activeJob.failedItems.toLocaleString("en-US")} + {" "} + failed + + )} + ) + + )} +

    + + {shouldAutoRefresh && ( + <> +
    + +
    + + This page refreshes automatically every 5 seconds. You can + navigate away safely — the cleanup will continue in the + background. + +