c35f10835a
- 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>
72 lines
3.0 KiB
TypeScript
72 lines
3.0 KiB
TypeScript
// 五列内容身份锚点共享 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 }
|
||
}
|