diff --git a/package.json b/package.json index 72e4991..ea3360e 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,12 @@ "typescript": "^5.0.0" }, "scripts": { - "dev": "bun --filter 'server' dev & bun --filter 'ui' dev", + "dev": "bun --filter 'db' dev & bun --filter 'server' dev & bun --filter 'ponder' dev & bun --filter 'ui' dev", "format": "bunx @biomejs/biome format --write .", - "ponder:start": "bun --filter 'ponder' start" + "ponder:start": "bun --filter 'ponder' start", + "db:migrate": "bun --filter 'db' migrate", + "server:install": "bun --filter 'server' install", + "server:start": "bun --filter 'server' start" }, "packageManager": "^bun@1.2.0" } diff --git a/packages/db/index.ts b/packages/db/index.ts index 8fdb4f0..acab6e9 100644 --- a/packages/db/index.ts +++ b/packages/db/index.ts @@ -39,7 +39,10 @@ class Db { async getWritersByManager(managerAddress: Hex) { const writers = await this.pg.query.writer.findMany({ - where: arrayContains(writer.managers, [managerAddress]), + where: and( + arrayContains(writer.managers, [managerAddress]), + isNull(writer.deletedAt), + ), orderBy: (writer, { desc }) => [desc(writer.createdAt)], }); const storageAddresses = writers @@ -227,6 +230,13 @@ class Db { }) .returning(); } + + deleteWriter(address: Hex) { + return this.pg + .update(writer) + .set({ deletedAt: new Date() }) + .where(eq(writer.address, address)); + } } export function writerToJsonSafe(data: SelectWriter) { diff --git a/packages/db/src/migrations/0001_nifty_lord_tyger.sql b/packages/db/src/migrations/0001_nifty_lord_tyger.sql new file mode 100644 index 0000000..f0e34c9 --- /dev/null +++ b/packages/db/src/migrations/0001_nifty_lord_tyger.sql @@ -0,0 +1 @@ +ALTER TABLE "writer" ADD COLUMN "deleted_at" timestamp with time zone; \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0001_snapshot.json b/packages/db/src/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000..3a2a7fe --- /dev/null +++ b/packages/db/src/migrations/meta/0001_snapshot.json @@ -0,0 +1,589 @@ +{ + "id": "50377a7f-fec6-4ec2-993e-3516decd5804", + "prevId": "83a4201e-fd17-4b84-9cc7-55fb1d717827", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.chunk": { + "name": "chunk", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "entry_id": { + "name": "entry_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at_transaction_id": { + "name": "created_at_transaction_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "entry_index_idx": { + "name": "entry_index_idx", + "columns": [ + { + "expression": "entry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chunk_created_at_transaction_id_syndicate_tx_id_fk": { + "name": "chunk_created_at_transaction_id_syndicate_tx_id_fk", + "tableFrom": "chunk", + "tableTo": "syndicate_tx", + "columnsFrom": [ + "created_at_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "chunk_createdAtTransactionId_unique": { + "name": "chunk_createdAtTransactionId_unique", + "nullsNotDistinct": false, + "columns": [ + "created_at_transaction_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.entry": { + "name": "entry", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "exists": { + "name": "exists", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "on_chain_id": { + "name": "on_chain_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at_hash": { + "name": "created_at_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at_block": { + "name": "created_at_block", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at_block_datetime": { + "name": "created_at_block_datetime", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at_hash": { + "name": "deleted_at_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_at_block": { + "name": "deleted_at_block", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "deleted_at_block_datetime": { + "name": "deleted_at_block_datetime", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at_hash": { + "name": "updated_at_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at_block": { + "name": "updated_at_block", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "updated_at_block_datetime": { + "name": "updated_at_block_datetime", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "storage_address": { + "name": "storage_address", + "type": "varchar(42)", + "primaryKey": false, + "notNull": true + }, + "created_at_transaction_id": { + "name": "created_at_transaction_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "deleted_at_transaction_id": { + "name": "deleted_at_transaction_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "updated_at_transaction_id": { + "name": "updated_at_transaction_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "storage_address_on_chain_id_idx": { + "name": "storage_address_on_chain_id_idx", + "columns": [ + { + "expression": "storage_address", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "on_chain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "storage_address_idx": { + "name": "storage_address_idx", + "columns": [ + { + "expression": "storage_address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "entry_created_at_transaction_id_syndicate_tx_id_fk": { + "name": "entry_created_at_transaction_id_syndicate_tx_id_fk", + "tableFrom": "entry", + "tableTo": "syndicate_tx", + "columnsFrom": [ + "created_at_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "entry_deleted_at_transaction_id_syndicate_tx_id_fk": { + "name": "entry_deleted_at_transaction_id_syndicate_tx_id_fk", + "tableFrom": "entry", + "tableTo": "syndicate_tx", + "columnsFrom": [ + "deleted_at_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "entry_updated_at_transaction_id_syndicate_tx_id_fk": { + "name": "entry_updated_at_transaction_id_syndicate_tx_id_fk", + "tableFrom": "entry", + "tableTo": "syndicate_tx", + "columnsFrom": [ + "updated_at_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "entry_createdAtTransactionId_unique": { + "name": "entry_createdAtTransactionId_unique", + "nullsNotDistinct": false, + "columns": [ + "created_at_transaction_id" + ] + }, + "entry_deletedAtTransactionId_unique": { + "name": "entry_deletedAtTransactionId_unique", + "nullsNotDistinct": false, + "columns": [ + "deleted_at_transaction_id" + ] + }, + "entry_updatedAtTransactionId_unique": { + "name": "entry_updatedAtTransactionId_unique", + "nullsNotDistinct": false, + "columns": [ + "updated_at_transaction_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.syndicate_tx": { + "name": "syndicate_tx", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "chain_id": { + "name": "chain_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "block_number": { + "name": "block_number", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "request_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PENDING'" + }, + "function_signature": { + "name": "function_signature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "address": { + "name": "address", + "type": "varchar(42)", + "primaryKey": true, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.writer": { + "name": "writer", + "schema": "", + "columns": { + "address": { + "name": "address", + "type": "varchar(42)", + "primaryKey": true, + "notNull": true + }, + "storage_address": { + "name": "storage_address", + "type": "varchar(42)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "admin": { + "name": "admin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "managers": { + "name": "managers", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "is_private": { + "name": "is_private", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at_hash": { + "name": "created_at_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at_block": { + "name": "created_at_block", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at_block_datetime": { + "name": "created_at_block_datetime", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "transaction_id": { + "name": "transaction_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "writer_transaction_id_syndicate_tx_id_fk": { + "name": "writer_transaction_id_syndicate_tx_id_fk", + "tableFrom": "writer", + "tableTo": "syndicate_tx", + "columnsFrom": [ + "transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "writer_storageAddress_unique": { + "name": "writer_storageAddress_unique", + "nullsNotDistinct": false, + "columns": [ + "storage_address" + ] + }, + "writer_transactionId_unique": { + "name": "writer_transactionId_unique", + "nullsNotDistinct": false, + "columns": [ + "transaction_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.request_status": { + "name": "request_status", + "schema": "public", + "values": [ + "PENDING", + "PROCESSED", + "SUBMITTED", + "CONFIRMED", + "PAUSED", + "ABANDONED" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index 2bb4948..8283296 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1739690666798, "tag": "0000_previous_rick_jones", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1741071323984, + "tag": "0001_nifty_lord_tyger", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index c8defde..4bf7752 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -27,6 +27,7 @@ export const writer = pgTable("writer", { }), createdAt: timestamp({ withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp({ withTimezone: true }).notNull(), + deletedAt: timestamp({ withTimezone: true }), transactionId: varchar({ length: 255 }) .unique() .references(() => syndicateTx.id), diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index 61be231..eef1d8c 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -1,6 +1,5 @@ import { entryToJsonSafe, writerToJsonSafe } from "db"; import { Hono } from "hono"; -import { serveStatic } from "hono/bun"; import { cors } from "hono/cors"; import { randomBytes } from "node:crypto"; import { computeWriterAddress, computeWriterStorageAddress } from "utils"; @@ -33,9 +32,6 @@ import { syndicate } from "./syndicate"; const app = new Hono(); -// @note split out the frontend build from the server -app.use("*", serveStatic({ root: "../ui/dist" })); -app.use("*", serveStatic({ path: "../ui/dist/index.html" })); app.use("*", cors()); const api = app @@ -144,6 +140,23 @@ const api = app const writer = writerToJsonSafe(data[0]); return c.json({ writer }, 201); }) + .delete("/writer/:address", addressParamSchema, async (c) => { + const { address } = c.req.valid("param"); + const writer = await db.getWriter(address); + if (!writer) { + return c.json({ error: "writer not found" }, 404); + } + await db.deleteWriter(address); + return c.json( + { + writer: { + ...writerToJsonSafe(writer), + entries: writer.entries.map(entryToJsonSafe), + }, + }, + 200, + ); + }) .post( "/writer/:address/entry/createWithChunk", addressParamSchema, diff --git a/packages/ui/src/components/Block.tsx b/packages/ui/src/components/Block.tsx index ddc7ffd..1eca9b0 100644 --- a/packages/ui/src/components/Block.tsx +++ b/packages/ui/src/components/Block.tsx @@ -6,23 +6,21 @@ import { MD } from "./markdown/MD"; interface BlockProps { title?: string | null; - id?: string; href?: string; onClick?: MouseEventHandler; isLoading?: boolean; - leftIcon?: React.ReactNode; + bottom?: React.ReactNode; } export default function Block({ title, - id, href, onClick, isLoading, - leftIcon, + bottom, }: BlockProps) { const className = cn( - "border-0 border-neutral-700 bg-neutral-900 aspect-square flex flex-col justify-between overflow-auto", + "border-0 border-neutral-700 bg-neutral-900 aspect-square flex flex-col justify-between overflow-auto relative", { "hover:cursor-wait": isLoading, "hover:cursor-zoom-in": !isLoading, @@ -31,24 +29,19 @@ export default function Block({ const renderChildren = useCallback(() => { return ( -
+
{title} -
- {leftIcon &&
{leftIcon}
} -
- {isLoading ? ... : id} + {isLoading && ( +
+ ...
-
+ )} + {!isLoading && bottom}
); - }, [title, id, isLoading, leftIcon]); + }, [title, isLoading, bottom]); return href && !isLoading ? ( - + {renderChildren()} ) : ( diff --git a/packages/ui/src/components/Dropdown.tsx b/packages/ui/src/components/Dropdown.tsx index d2568d7..b4a926b 100644 --- a/packages/ui/src/components/Dropdown.tsx +++ b/packages/ui/src/components/Dropdown.tsx @@ -4,6 +4,7 @@ import { cn } from "../utils/cn"; interface DropdownProps { children: React.ReactNode; trigger: React.ReactNode; + side?: "left" | "right" | "top" | "bottom"; } const dropdownMenuItemClasses = cn( @@ -14,10 +15,16 @@ const dropdownMenuContentClasses = cn( "DropdownMenuContent min-w-xl w-full min-w-40 bg-neutral-900 p-1.5 shadow-sm will-change-transform will-change-[opacity]", ); -export function Dropdown({ children, trigger }: DropdownProps) { +export function Dropdown({ children, trigger, side = "left" }: DropdownProps) { return ( - + { + e.stopPropagation(); + e.preventDefault(); + }} + > +
+ Hide? +
+
+
+ } /> ))} diff --git a/packages/ui/src/routes/Writer.tsx b/packages/ui/src/routes/Writer.tsx index 59352f6..9fa0375 100644 --- a/packages/ui/src/routes/Writer.tsx +++ b/packages/ui/src/routes/Writer.tsx @@ -11,6 +11,7 @@ import { POLLING_INTERVAL } from "../constants"; import { WriterContext } from "../context"; import type { BlockCreateInput } from "../interfaces"; import { type Entry, createWithChunk, getWriter } from "../utils/api"; +import { cn } from "../utils/cn"; import { useFirstWallet } from "../utils/hooks"; import { getDerivedSigningKey, signCreateWithChunk } from "../utils/signer"; import { compress, encrypt, processEntry } from "../utils/utils"; @@ -64,16 +65,10 @@ export function Writer() { const publicEntries = data.entries.filter( (e) => !e.raw?.startsWith("enc:"), ); - console.log(publicEntries); for (const entry of publicEntries) { processedEntries.push(entry); } } - - // for (const entry of data.entries) { - // const processed = await processEntry(key, entry); - // processedEntries.push(processed); - // } setProcessedData(processedEntries); } }; @@ -144,11 +139,14 @@ export function Writer() { )} {!isExpanded && processedData.map((entry) => { - let id: undefined | string = undefined; + let createdAt: undefined | string = undefined; if (entry.createdAtBlockDatetime) { - id = format(new Date(entry.createdAtBlockDatetime), "MM-dd-yyyy"); + createdAt = format( + new Date(entry.createdAtBlockDatetime), + "MM-dd-yyyy", + ); } else { - id = format(new Date(entry.createdAt), "MM/dd/yyyy"); + createdAt = format(new Date(entry.createdAt), "MM/dd/yyyy"); } return ( @@ -157,11 +155,23 @@ export function Writer() { href={`/writer/${data?.address}/${entry.onChainId}`} isLoading={!entry.onChainId} title={entry.decompressed ? entry.decompressed : entry.raw} - id={id} - leftIcon={ - entry.version?.startsWith("enc") ? ( - - ) : null + bottom={ +
+ {entry.version?.startsWith("enc") && ( + + + + )} + {createdAt} +
} /> ); diff --git a/packages/ui/src/utils/api.ts b/packages/ui/src/utils/api.ts index 07c5442..e5fc57f 100644 --- a/packages/ui/src/utils/api.ts +++ b/packages/ui/src/utils/api.ts @@ -9,6 +9,16 @@ if (!import.meta.env.VITE_API_BASE_URL) { const client = hc(import.meta.env.VITE_API_BASE_URL); +export async function deleteWriter(address: Hex) { + const res = await client.writer[":address"].$delete({ + param: { address }, + }); + if (!res.ok) { + throw new Error(res.statusText); + } + return res.json(); +} + export async function getMe(address: Hex) { const res = await client.me[":address"].$get({ param: { address },