mirror of
https://github.com/lobehub/lobehub.git
synced 2026-09-20 04:56:13 +08:00
b4c5ff79ff
* ✨ feat: add document_likes table for workspace document likes Introduce the `document_likes` storage layer so workspace members can like a document: one row per (document, user), unique on that pair, with cascading FKs to documents / users / workspaces and paging-friendly indexes. Ships the Drizzle schema, relations, and an idempotent migration (0155); the model, router, and UI land in a follow-up PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HNfPCfAEiWyuvTbXMoqNTZ * 🐛 fix: keep document_likes consistent across document scope transfers DocumentModel.transferTo now rehomes like rows to the target workspace and deletes them on a workspace→personal transfer, mirroring how comments and mention rows are handled. Adds a regression test covering both directions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HNfPCfAEiWyuvTbXMoqNTZ * 🐛 fix: count teammates' likes as foreign rows in document transfer guard subtreeHasForeignRows now also detects document_likes rows owned by other users, so a non-owner member cannot transfer a subtree that would rehome or delete teammates' reactions. Covered by a regression test asserting the caller's own like stays transferable while a teammate's blocks it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HNfPCfAEiWyuvTbXMoqNTZ * ✨ feat: add DocumentLikeModel with transfer-serialized like creation Ships the model half of the likes storage layer in the same PR as the schema so the serialization contract stays reviewable in one place: like() runs in a transaction that locks the document row FOR UPDATE (mirroring DocumentCommentModel.create), so a like racing a concurrent scope transfer blocks on the transfer's row lock and re-validates the workspace after it commits instead of stamping the stale source workspace_id — the transfer-side foreign-row guard and rehoming from the previous commits therefore see a consistent row set. Includes the DocumentLikeSummary types and model unit tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HNfPCfAEiWyuvTbXMoqNTZ * 🐛 fix: recheck foreign rows inside the transfer transaction The non-owner foreign-row guard used to run as a router preflight outside the transfer transaction, so a teammate's like (or comment) committed between the preflight and the transfer could still be rehomed or deleted. transferTo now locks every subtree document row FOR UPDATE and, when called with forbidForeignRows, re-runs the shared foreign-row predicate inside the transaction, throwing DOCUMENT_TRANSFER_FOREIGN_ROWS which the router maps to the existing OwnerOnly error. Concurrent like/comment writers lock the same document row before stamping a workspace, so they either commit before the recheck (and are seen) or block until the transfer commits and then re-validate their scope. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HNfPCfAEiWyuvTbXMoqNTZ * 🐛 fix: serialize unlike and summary against document scope transfers unlike() and summary() shared like()'s validate-then-act shape without its serialization, so a transfer committing between the workspace validation and the delete/reads let a stale source-workspace caller mutate or read rehomed rows. All three operations now run through a shared lockDocument helper inside a transaction — writes lock the document row FOR UPDATE, summary takes FOR SHARE so the validation stays true for the duration of its reads while concurrent transfers wait. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HNfPCfAEiWyuvTbXMoqNTZ * 🐛 fix: route resource-manager transfers through the in-transaction guard file.transferEntity was the remaining caller still running the foreign-row check as a pre-transaction preflight, leaving the same TOCTOU window the document router just closed. It now passes forbidForeignRows to transferTo and translates DOCUMENT_TRANSFER_FOREIGN_ROWS to the OwnerOnly error exactly like document.transferDocument. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HNfPCfAEiWyuvTbXMoqNTZ --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
19 lines
1.8 KiB
SQL
19 lines
1.8 KiB
SQL
CREATE TABLE IF NOT EXISTS "document_likes" (
|
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"document_id" text NOT NULL,
|
|
"user_id" text NOT NULL,
|
|
"workspace_id" text NOT NULL,
|
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
);
|
|
--> statement-breakpoint
|
|
ALTER TABLE "document_likes" DROP CONSTRAINT IF EXISTS "document_likes_document_id_documents_id_fk";--> statement-breakpoint
|
|
ALTER TABLE "document_likes" ADD CONSTRAINT "document_likes_document_id_documents_id_fk" FOREIGN KEY ("document_id") REFERENCES "public"."documents"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
ALTER TABLE "document_likes" DROP CONSTRAINT IF EXISTS "document_likes_user_id_users_id_fk";--> statement-breakpoint
|
|
ALTER TABLE "document_likes" ADD CONSTRAINT "document_likes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
ALTER TABLE "document_likes" DROP CONSTRAINT IF EXISTS "document_likes_workspace_id_workspaces_id_fk";--> statement-breakpoint
|
|
ALTER TABLE "document_likes" ADD CONSTRAINT "document_likes_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
CREATE UNIQUE INDEX IF NOT EXISTS "document_likes_document_id_user_id_unique" ON "document_likes" USING btree ("document_id","user_id");--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "document_likes_document_id_created_at_idx" ON "document_likes" USING btree ("document_id","created_at");--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "document_likes_user_id_idx" ON "document_likes" USING btree ("user_id");--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "document_likes_workspace_id_idx" ON "document_likes" USING btree ("workspace_id");
|