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
9 changes: 9 additions & 0 deletions 20260905-bookmark-import.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 导入时保留原保存时间

日期:2026-09-05

- 用户书签创建接口增加可选导入参数,写入原保存时间和加星状态。
- 导入参数存在时,已有用户书签关系不更新;普通重新保存仍使用当前时间。
- 导入变更记录和通知使用当前时间,避免旧保存日期导致增量同步漏掉新增书签。
- 正式版后端的导入测试验证日期、状态、已有关系和同步时间。无数据库迁移。
- 测试隔离 Prisma 运行时,CI 无需先生成数据库客户端即可执行。
11 changes: 8 additions & 3 deletions src/domain/bookmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ export interface addBookmarkReq {
}

export interface addUrlBookmarkReq {
saved_at?: string
is_starred?: boolean
import_only?: boolean
target_url: string
target_title?: string
thumbnail?: string
Expand Down Expand Up @@ -120,6 +123,7 @@ export class BookmarkService {
description?: string
siteName?: string
isArchive?: boolean
importMetadata?: { savedAt?: Date; starred: boolean }
}) {
if (options.type === 1) {
const urlEntity = new URL(options.targetUrl)
Expand Down Expand Up @@ -154,7 +158,7 @@ export class BookmarkService {

const [_, relation] = await Promise.all([
this.bookmarkSearchRepo.upsertUserBookmark(options.ctx.getUserId(), bmInfo.id),
this.bookmarkRepo.createBookmarkRelation(options.ctx.getUserId(), bmInfo.id, options.type, options.isArchive || false)
this.bookmarkRepo.createBookmarkRelation(options.ctx.getUserId(), bmInfo.id, options.type, options.isArchive || false, options.importMetadata)
])

if (relation.deleted_at) {
Expand All @@ -164,12 +168,13 @@ export class BookmarkService {
if (relation) {
// 在数据库中增加收藏添加记录
try {
await this.bookmarkRepo.createBookmarkChangeLog(options.ctx.getUserId(), options.targetUrl, relation.bookmark_id, 'add', relation.created_at)
const changeTime = options.importMetadata ? new Date() : relation.created_at
await this.bookmarkRepo.createBookmarkChangeLog(options.ctx.getUserId(), options.targetUrl, relation.bookmark_id, 'add', changeTime)
options.ctx.execution.waitUntil(
this.notifyMessage.sendBookmarkChange(options.ctx.env, {
user_id: options.ctx.getUserId(),
bookmark_id: options.ctx.hashIds.encodeId(relation.bookmark_id),
created_at: relation.created_at,
created_at: changeTime,
target_url: options.targetUrl,
action: 'add'
})
Expand Down
14 changes: 11 additions & 3 deletions src/infra/repository/dbBookmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,12 +255,20 @@ export class BookmarkRepo {
})
}

public async createBookmarkRelation(userId: number, bmId: number, type: number, isArchive: boolean) {
public async createBookmarkRelation(userId: number, bmId: number, type: number, isArchive: boolean, importMetadata?: { savedAt?: Date; starred: boolean }) {
// re-save bumps created_at (save time, tops inbox); replay may re-top
return await this.prismaPg().sr_user_bookmark.upsert({
where: { user_id_bookmark_id: { user_id: userId, bookmark_id: bmId } },
create: { user_id: userId, bookmark_id: bmId, created_at: new Date(), updated_at: new Date(), type, archive_status: isArchive ? 1 : 0 },
update: { created_at: new Date(), updated_at: new Date() }
create: {
user_id: userId,
bookmark_id: bmId,
created_at: importMetadata?.savedAt ?? new Date(),
updated_at: new Date(),
type,
archive_status: isArchive ? 1 : 0,
is_starred: importMetadata?.starred ?? false
},
update: importMetadata ? {} : { created_at: new Date(), updated_at: new Date() }
})
}

Expand Down
74 changes: 74 additions & 0 deletions test/bookmarkImportMetadata.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { describe, expect, it, vi } from 'vitest'

vi.mock('@prisma/client', () => ({
Prisma: { join: vi.fn(), sql: vi.fn() },
PrismaClient: class {}
}))

vi.mock('@prisma/hyperdrive-client', () => ({
PrismaClient: class {}
}))

import { BookmarkService } from '../src/domain/bookmark'
import { BookmarkRepo, queueStatus } from '../src/infra/repository/dbBookmark'

describe('bookmark import metadata', () => {
it('uses saved time and stars for new relations without changing an existing relation', async () => {
const repo = Object.create(BookmarkRepo.prototype) as BookmarkRepo
const upsert = vi.fn().mockResolvedValue({})
Object.assign(repo, { prismaPg: () => ({ sr_user_bookmark: { upsert } }) })
const savedAt = new Date('2020-01-02T03:04:05Z')

await repo.createBookmarkRelation(1, 42, 0, true, { savedAt, starred: true })

expect(upsert).toHaveBeenCalledWith(
expect.objectContaining({
create: expect.objectContaining({ created_at: savedAt, is_starred: true, archive_status: 1 }),
update: {}
})
)

await repo.createBookmarkRelation(1, 42, 0, false)
expect(upsert.mock.calls[1][0].update.created_at).toBeInstanceOf(Date)
})

it('uses current change-log time while preserving the original saved date', async () => {
const service = Object.create(BookmarkService.prototype) as BookmarkService
const oldDate = new Date('2020-01-02T03:04:05Z')
const createBookmarkRelation = vi.fn().mockResolvedValue({ bookmark_id: 42, created_at: oldDate, deleted_at: null })
const createBookmarkChangeLog = vi.fn()
const sendBookmarkChange = vi.fn().mockResolvedValue(undefined)
const waitUntil = vi.fn()
Object.assign(service, {
bookmarkRepo: {
createBookmark: vi.fn().mockResolvedValue({ id: 42, status: queueStatus.SUCCESS }),
createBookmarkRelation,
createBookmarkChangeLog
},
bookmarkSearchRepo: { upsertUserBookmark: vi.fn() },
notifyMessage: { sendBookmarkChange }
})
const ctx = {
env: {},
execution: { waitUntil },
getUserId: () => 1,
hashIds: { encodeId: (id: number) => `encoded-${id}` }
} as any
const before = Date.now()

await service.createBookmarkBase({
ctx,
targetUrl: 'https://example.com/article',
hostUrl: 'example.com',
title: 'Article',
type: 0,
importMetadata: { savedAt: oldDate, starred: false }
})

expect(createBookmarkRelation).toHaveBeenCalledWith(1, 42, 0, false, { savedAt: oldDate, starred: false })
const changeTime = createBookmarkChangeLog.mock.calls[0][4] as Date
expect(changeTime.getTime()).toBeGreaterThanOrEqual(before)
expect(sendBookmarkChange).toHaveBeenCalledWith(ctx.env, expect.objectContaining({ created_at: changeTime, bookmark_id: 'encoded-42' }))
expect(waitUntil).toHaveBeenCalledTimes(1)
})
})
Loading