Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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,51 @@
-- 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 plus
-- ON CONFLICT aligned with partial unique index `idx_notes_unique_default_per_owner`;
-- 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
)
ON CONFLICT ("owner_id") WHERE ("is_default" = true AND "is_deleted" = false) DO NOTHING;
--> 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
}
]
}
7 changes: 7 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 — /confirm での所有権確認用 Head。Ownership probe on POST /confirm. */
}
return {
S3Client: MockS3Client,
PutObjectCommand: MockPutObjectCommand,
GetObjectCommand: MockGetObjectCommand,
DeleteObjectCommand: MockDeleteObjectCommand,
HeadObjectCommand: MockHeadObjectCommand,
};
});

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

beforeEach(() => {
mockS3Send.mockReset();
// POST /confirm は常に HeadObject でプローブする。別レスポンスが必要なテストは mockResolvedValueOnce を使う。
// POST /confirm always probes via HeadObject; tests needing 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