feat(editor) : notify peers of draft overwrite
CI / Verify (push) Successful in 1m20s
CI / Build immutable images and deploy (push) Successful in 1m51s

This commit is contained in:
2026-09-15 19:46:40 +07:00 Unverified
parent fb152c12fe
commit c3a2aa88d6
7 changed files with 121 additions and 50 deletions
+21 -15
View File
@@ -49,6 +49,11 @@ const DEFAULT_SECTIONS = [
{ kind: "team", slug: "team", title: "Team Calc", sortOrder: 3 },
] as const;
export interface GuideEventDetails {
operation: "overwrite";
actorName: string;
}
export class GuideVersionConflictError extends Error {
constructor(readonly currentVersion: number) {
super("ข้อมูลถูกแก้ไขจากแท็บอื่น กด Save now เพื่อบันทึก Local draft ทับเวอร์ชันล่าสุด");
@@ -56,13 +61,13 @@ export class GuideVersionConflictError extends Error {
}
}
async function emitGuideEvents(tx: Parameters<Parameters<ReturnType<typeof getDb>["transaction"]>[0]>[0], guideId: string, version: number, directoryChanged = false, scope?: AdminEditorScope) {
async function emitGuideEvents(tx: Parameters<Parameters<ReturnType<typeof getDb>["transaction"]>[0]>[0], guideId: string, version: number, directoryChanged = false, scope?: AdminEditorScope, details?: GuideEventDetails) {
await tx.insert(outboxEvents).values([
...(directoryChanged
? [{ topic: "directory", aggregateId: guideId, eventType: "directory.updated", payload: { id: guideId, version } }]
: []),
{ topic: `page:${guideId}`, aggregateId: guideId, eventType: "page.updated", payload: { id: guideId, version } },
{ topic: "admin", aggregateId: guideId, eventType: "admin.updated", payload: { id: guideId, version, ...(scope ? { scope } : {}) } },
{ topic: "admin", aggregateId: guideId, eventType: "admin.updated", payload: { id: guideId, version, ...(scope ? { scope } : {}), ...details } },
]);
}
@@ -102,23 +107,23 @@ export async function createGuide(input: unknown) {
});
}
async function advanceGuideVersion(tx: Parameters<Parameters<ReturnType<typeof getDb>["transaction"]>[0]>[0], guideId: string, expectedVersion: number, directoryChanged = false, scope?: AdminEditorScope) {
async function advanceGuideVersion(tx: Parameters<Parameters<ReturnType<typeof getDb>["transaction"]>[0]>[0], guideId: string, expectedVersion: number, directoryChanged = false, scope?: AdminEditorScope, details?: GuideEventDetails) {
const [guide] = await tx.update(guides).set({ version: expectedVersion + 1 }).where(and(eq(guides.id, guideId), eq(guides.version, expectedVersion), sql`${guides.trashedAt} is null`)).returning();
if (!guide) {
const [current] = await tx.select({ version: guides.version }).from(guides).where(eq(guides.id, guideId)).limit(1);
if (current) throw new GuideVersionConflictError(current.version);
throw new Error("ไม่พบ Guide");
}
await emitGuideEvents(tx, guide.id, guide.version, directoryChanged, scope);
await emitGuideEvents(tx, guide.id, guide.version, directoryChanged, scope, details);
return guide;
}
export async function saveWeapons(guideId: string, input: unknown) {
export async function saveWeapons(guideId: string, input: unknown, details?: GuideEventDetails) {
const data = weaponSectionSchema.parse(input);
const [guide] = await getDb().select().from(guides).where(eq(guides.id, guideId)).limit(1);
if (!guide) throw new Error("ไม่พบ Guide");
return getDb().transaction(async (tx) => {
const updated = await advanceGuideVersion(tx, guideId, data.expectedVersion, false, "weapon");
const updated = await advanceGuideVersion(tx, guideId, data.expectedVersion, false, "weapon", details);
await tx.update(guideSections).set({ note: data.note }).where(and(eq(guideSections.guideId, guideId), eq(guideSections.slug, "weapon")));
await tx.delete(weaponConditions).where(eq(weaponConditions.guideId, guideId));
for (const [sortOrder, group] of data.groups.entries()) {
@@ -129,7 +134,7 @@ export async function saveWeapons(guideId: string, input: unknown) {
});
}
export async function updateGuideBasics(guideId: string, input: unknown) {
export async function updateGuideBasics(guideId: string, input: unknown, details?: GuideEventDetails) {
const data = updateGuideSchema.parse(input);
const [current] = await getDb().select().from(guides).where(eq(guides.id, guideId)).limit(1);
if (!current) throw new Error("ไม่พบ Guide");
@@ -154,7 +159,7 @@ export async function updateGuideBasics(guideId: string, input: unknown) {
: [];
if (bleedingMedia.length !== bleedingMediaIds.length) throw new Error("ไม่พบภาพ Bleeding ที่อัปโหลดแล้ว");
}
const updated = await advanceGuideVersion(tx, guideId, data.expectedVersion, true, "overview");
const updated = await advanceGuideVersion(tx, guideId, data.expectedVersion, true, "overview", details);
await tx.update(guides).set({
overview: data.overview,
coverMediaId: data.coverMediaId,
@@ -202,12 +207,12 @@ export async function updateGuideBasics(guideId: string, input: unknown) {
});
}
export async function saveArtifacts(guideId: string, input: unknown) {
export async function saveArtifacts(guideId: string, input: unknown, details?: GuideEventDetails) {
const data = artifactSectionSchema.parse(input);
const [guide] = await getDb().select().from(guides).where(eq(guides.id, guideId)).limit(1);
if (!guide) throw new Error("ไม่พบ Guide");
return getDb().transaction(async (tx) => {
const updated = await advanceGuideVersion(tx, guideId, data.expectedVersion, false, "artifact");
const updated = await advanceGuideVersion(tx, guideId, data.expectedVersion, false, "artifact", details);
await tx.delete(artifactGroups).where(eq(artifactGroups.guideId, guideId));
for (const [sortOrder, group] of data.groups.entries()) {
const [created] = await tx.insert(artifactGroups).values({ guideId, name: group.name, sortOrder }).returning();
@@ -218,10 +223,10 @@ export async function saveArtifacts(guideId: string, input: unknown) {
});
}
export async function saveConstellations(guideId: string, input: unknown) {
export async function saveConstellations(guideId: string, input: unknown, details?: GuideEventDetails) {
const data = constellationSectionSchema.parse(input);
return getDb().transaction(async (tx) => {
const updated = await advanceGuideVersion(tx, guideId, data.expectedVersion, false, "constellations");
const updated = await advanceGuideVersion(tx, guideId, data.expectedVersion, false, "constellations", details);
await tx.update(guideSections).set({ note: data.note }).where(and(eq(guideSections.guideId, guideId), eq(guideSections.slug, "constellations")));
await tx.delete(constellationConditions).where(eq(constellationConditions.guideId, guideId));
for (const [sortOrder, group] of data.groups.entries()) {
@@ -232,7 +237,7 @@ export async function saveConstellations(guideId: string, input: unknown) {
});
}
export async function saveTeams(guideId: string, input: unknown) {
export async function saveTeams(guideId: string, input: unknown, details?: GuideEventDetails) {
const data = teamSectionSchema.parse(input);
const [guide] = await getDb().select().from(guides).where(eq(guides.id, guideId)).limit(1);
if (!guide) throw new Error("ไม่พบ Guide");
@@ -256,7 +261,7 @@ export async function saveTeams(guideId: string, input: unknown) {
}
}
return getDb().transaction(async (tx) => {
const updated = await advanceGuideVersion(tx, guideId, data.expectedVersion, false, "team");
const updated = await advanceGuideVersion(tx, guideId, data.expectedVersion, false, "team", details);
await tx.delete(teamConditions).where(eq(teamConditions.guideId, guideId));
for (const [groupOrder, group] of data.groups.entries()) {
const [condition] = await tx.insert(teamConditions).values({ guideId, name: group.name, note: group.note, sortOrder: groupOrder }).returning();
@@ -333,6 +338,7 @@ export async function saveExtraSection(
guideId: string,
sectionId: string,
input: unknown,
details?: GuideEventDetails,
) {
const data = saveExtraSectionSchema.parse(input);
return getDb().transaction(async (tx) => {
@@ -347,7 +353,7 @@ export async function saveExtraSection(
.limit(1);
if (!target) throw new Error("ไม่พบ Extra section");
const updated = await advanceGuideVersion(tx, guideId, data.expectedVersion, false, `extra:${sectionId}`);
const updated = await advanceGuideVersion(tx, guideId, data.expectedVersion, false, `extra:${sectionId}`, details);
await tx
.update(guideSections)
.set({ title: data.section.title })