Files
cms-website/apps/api/src/modules/import/anchors.ts
T
浅念 c35f10835a feat(publish): 显式发布命令(dry-run/confirm/审计/站点配置首版/锚点helper)
- publishSiteContent 零装饰器服务:dry-run 只读清单(current 非空且 published≠current)
  + 随机 confirm token;confirm 单事务批量 UPDATE published←current + audit_logs
  (action=content.publish,全仓第一个审计写入方)+ site_config_revisions v1 首版
  ({name,origin,locale,brand:{}},UNIQUE(site_id,version) 幂等)
- 空清单 no-op 零写入;SITE_NOT_FOUND/CONFIRM_TOKEN_REQUIRED 均先于任何写入
- P0 门禁不做(ADR-018 记偏离,服务 JSDoc 注明);导入永不发布(ADR-010)
- sf CLI + content:publish(--dry-run/--confirm/--actor,缺省 dry-run 安全优先,
  actor 缺省 super_admin@cli)
- anchors.ts:五列内容身份查询从 content/term/route 三写入器抽单源共享(零行为
  变更,既有 333 测试守护)
- 集成测试 7 例(临时库 mini 全链):dry-run 零写入/confirm/幂等/config 首版与
  既有跳过/actor/空清单/SITE_NOT_FOUND/缺 token

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-22 07:06:23 +08:00

72 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 五列内容身份锚点共享 helper(Plan 4 Task 1,ADR-006、Plan 3 前置事实 ④):
// source_records 的内容身份(site + connection + source_type + source_id + locale,
// 五列——快照列不在键上,跨快照复用同一 content_entry)→ 挂接 entry 的最新快照行
// (latest first)。此前同一查询在 content/term/route 三写入器各持一份拷贝——
// publish 域(第 4 个消费方触发 Plan 3 审查既定条件)起抽此单源共享,零行为变更
// (WHERE/ORDER BY/LIMIT 逐字保真,由既有集成测试守护)。
//
// 消费方契约:
// - content-writer(findPriorLinkedRecord):id/checksum/entryId 全消费(幂等三态);
// - term-writer(resolveContentEntryId)/ route-writer(resolveEntryAnchor):
// 只消费 contentEntryId(entry 锚点解析)。
// 六列(快照内)查询**不在**此处——那是 content/media 写入器各自的快照行查找
// (snapshot_id 进键),与五列身份语义不同,勿合并。
import { and, desc, eq, isNotNull } from 'drizzle-orm'
import { sourceRecords } from '../../db/schema.js'
import type { ImportDb } from './import.service.js'
/** 五列身份的固定侧(site + connection)——各写入器的 WriterContext 均结构性满足 */
export interface AnchorIdentity {
siteId: string
connectionId: string
}
/** 五列身份的行侧(UniversalRow 的身份三列投影形状) */
export interface AnchorRowKey {
sourceType: string
sourceId: string
locale: string
}
/** 命中行(contentEntryId 非 null——谓词保证;normalizedChecksum 可空:record-only 足迹无) */
export interface LinkedSourceRecord {
id: string
contentEntryId: string
normalizedChecksum: string | null
}
/**
* 五列内容身份 → 挂接 entry 的最新 source_records 行(latest first——createdAt 降序,
* 与既有三写入器同口径);无挂接行 → null。谓词含 content_entry_id IS NOT NULL
* (record-only / 占位足迹不参与锚点解析)。
*/
export async function findLatestLinkedSourceRecord(
db: ImportDb,
identity: AnchorIdentity,
row: AnchorRowKey,
): Promise<LinkedSourceRecord | null> {
const rows = await db
.select({
id: sourceRecords.id,
contentEntryId: sourceRecords.contentEntryId,
normalizedChecksum: sourceRecords.normalizedChecksum,
})
.from(sourceRecords)
.where(
and(
eq(sourceRecords.siteId, identity.siteId),
eq(sourceRecords.sourceConnectionId, identity.connectionId),
eq(sourceRecords.sourceType, row.sourceType),
eq(sourceRecords.sourceId, row.sourceId),
eq(sourceRecords.locale, row.locale),
isNotNull(sourceRecords.contentEntryId),
),
)
.orderBy(desc(sourceRecords.createdAt))
.limit(1)
const r = rows[0]
if (r === undefined) return null
// contentEntryId 非 null:查询谓词保证(drizzle 谓词不细化 select 类型)
return { id: r.id, contentEntryId: r.contentEntryId!, normalizedChecksum: r.normalizedChecksum }
}