Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
-- 0023: Migrate legacy personal pages (`pages.note_id IS NULL`) into each owner's
-- default note, promote `pages.note_id` to NOT NULL, and drop `note_pages`.
--
-- 0023: 旧個人ページ(`pages.note_id IS NULL`)を所有者のデフォルトノートへ移し、
-- `pages.note_id` を NOT NULL に昇格し、`note_pages` を DROP する。
--
-- Idempotent / re-run safety: INSERT uses NOT EXISTS guards; DELETE targets orphans only.

-- 1) Orphan personal pages whose owner row no longer exists — delete before NOT NULL
DELETE FROM "pages"
WHERE "note_id" IS NULL
AND "owner_id" NOT IN (SELECT "id" FROM "user");
--> statement-breakpoint

-- 2) Safety net: ensure users who still have NULL note_id rows have a default note
INSERT INTO "notes" ("owner_id", "title", "visibility", "edit_permission", "is_default")
SELECT u."id", COALESCE(u."name", '') || 'のノート', 'private', 'owner_only', true
FROM "user" u
WHERE EXISTS (
SELECT 1 FROM "pages" p
WHERE p."owner_id" = u."id" AND p."note_id" IS NULL
)
AND NOT EXISTS (
SELECT 1 FROM "notes" n
WHERE n."owner_id" = u."id"
AND n."is_default" = true
AND n."is_deleted" = false
);
--> statement-breakpoint
Comment thread
coderabbitai[bot] marked this conversation as resolved.

-- 3) Backfill personal pages into the owner's default note
UPDATE "pages" p
SET "note_id" = (
SELECT n."id" FROM "notes" n
WHERE n."owner_id" = p."owner_id"
AND n."is_default" = true
AND n."is_deleted" = false
LIMIT 1
)
WHERE p."note_id" IS NULL;
--> statement-breakpoint

-- 4) Promote to NOT NULL
ALTER TABLE "pages" ALTER COLUMN "note_id" SET NOT NULL;
--> statement-breakpoint

-- 5) Drop link table (single membership model — Issue #823)
DROP TABLE IF EXISTS "note_pages";
7 changes: 7 additions & 0 deletions server/api/drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,13 @@
"when": 1778544000000,
"tag": "0022_add_default_note",
"breakpoints": true
},
{
"idx": 22,
"version": "7",
"when": 1778630400000,
"tag": "0023_migrate_personal_pages_drop_note_pages",
"breakpoints": true
}
]
}
6 changes: 6 additions & 0 deletions server/api/src/__tests__/routes/media.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,15 @@ vi.mock("@aws-sdk/client-s3", () => {
function MockDeleteObjectCommand() {
/* stub */
}
function MockHeadObjectCommand() {
/* stub — ownership probe on /confirm */
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
return {
S3Client: MockS3Client,
PutObjectCommand: MockPutObjectCommand,
GetObjectCommand: MockGetObjectCommand,
DeleteObjectCommand: MockDeleteObjectCommand,
HeadObjectCommand: MockHeadObjectCommand,
};
});

Expand Down Expand Up @@ -86,6 +90,8 @@ function createMediaApp(dbResults: unknown[]) {

beforeEach(() => {
mockS3Send.mockReset();
// POST /confirm always probes via HeadObject; tests that need other shapes use mockResolvedValueOnce.
mockS3Send.mockResolvedValue({ ContentLength: 1024 });
});

describe("POST /api/media/confirm — S3 key ownership validation", () => {
Expand Down
4 changes: 1 addition & 3 deletions server/api/src/__tests__/routes/notes/crud.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,9 +425,7 @@ describe("GET /api/notes/:noteId", () => {
expect(page).toHaveProperty("source_page_id");
expect(page).toHaveProperty("content_preview");
expect(page).toHaveProperty("thumbnail_url");
expect(page).toHaveProperty("sort_order");
expect(page).toHaveProperty("added_by_user_id");
expect(page).toHaveProperty("added_at");
expect(page).toHaveProperty("note_id", mockNote.id);
});

it("should return 404 for non-existent note", async () => {
Expand Down
Loading
Loading