feat: 외교 문서 변경을 입력 원장과 함께 감사 이력으로 저장
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
import type { ApiInputExecutionContext } from './inputEventBoundary.js';
|
||||
import type { ChangeJournal } from '@sammo-ts/common';
|
||||
import type { GameSessionTokenPayload } from '@sammo-ts/common/auth/gameToken';
|
||||
import type { DatabaseClient as InfraDatabaseClient, RedisConnector, GamePrisma } from '@sammo-ts/infra';
|
||||
@@ -99,6 +100,8 @@ export type InputJsonValue = GamePrisma.InputJsonValue;
|
||||
export type DatabaseClient = InfraDatabaseClient;
|
||||
|
||||
export interface GameApiContext {
|
||||
/** 현재 API 업무 transaction이 잠근 입력 원장의 인증된 식별자다. */
|
||||
auditInput?: ApiInputExecutionContext;
|
||||
requestId?: string;
|
||||
generalAccessTracking?: boolean;
|
||||
/** Validated server-issued proof for one realtime refresh burst. */
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface ApiInputPayloadIdentity {
|
||||
}
|
||||
|
||||
interface LockedInputEvent {
|
||||
sequence: bigint;
|
||||
target: 'API' | 'ENGINE';
|
||||
eventType: string;
|
||||
payload: GamePrisma.JsonValue;
|
||||
@@ -27,6 +28,14 @@ interface LockedInputEvent {
|
||||
attempts: number;
|
||||
}
|
||||
|
||||
export interface ApiInputExecutionContext {
|
||||
requestId: string;
|
||||
sequence: bigint;
|
||||
actorUserId: string | null;
|
||||
eventType: string;
|
||||
attempt: number;
|
||||
}
|
||||
|
||||
type InputEventOutcome<T> =
|
||||
{ kind: 'executed'; value: T } | { kind: 'replayed'; value: T } | { kind: 'failed'; error: unknown };
|
||||
|
||||
@@ -115,6 +124,7 @@ const lockInputEvent = async (db: DatabaseClient, requestId: string): Promise<Lo
|
||||
const rows = await db.$queryRaw<LockedInputEvent[]>(
|
||||
GamePrisma.sql`
|
||||
SELECT
|
||||
sequence,
|
||||
target,
|
||||
event_type AS "eventType",
|
||||
payload,
|
||||
@@ -235,7 +245,7 @@ export const executeInputEvent = async <T>(options: {
|
||||
payload: unknown;
|
||||
actorUserId?: string | null;
|
||||
acquireClockFence?: boolean;
|
||||
execute(db: DatabaseClient): Promise<T>;
|
||||
execute(db: DatabaseClient, context?: ApiInputExecutionContext): Promise<T>;
|
||||
}): Promise<T> => {
|
||||
const { db, requestId, eventType, payload, execute } = options;
|
||||
const actorUserId = options.actorUserId ?? null;
|
||||
@@ -275,7 +285,13 @@ export const executeInputEvent = async <T>(options: {
|
||||
await savepointDb.$executeRawUnsafe(`SAVEPOINT ${BUSINESS_SAVEPOINT}`);
|
||||
businessStarted = true;
|
||||
try {
|
||||
const value = await execute(transaction);
|
||||
const value = await execute(transaction, {
|
||||
requestId,
|
||||
sequence: row.sequence,
|
||||
actorUserId,
|
||||
eventType,
|
||||
attempt: row.attempts + 1,
|
||||
});
|
||||
const durableResult = canonicalJsonValue(value);
|
||||
await transaction.$executeRaw(GamePrisma.sql`
|
||||
UPDATE input_event
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import type { GameApiContext, GeneralRow, NationRow } from '../../context.js';
|
||||
import { insertMessage } from '../../messages/store.js';
|
||||
import { purifyDiplomacyHtml } from '../../security/diplomacyHtml.js';
|
||||
import { createDiplomacyDocumentAudit, projectAuditDocumentState } from '../../services/playAuditDiplomacy.js';
|
||||
import { readDatabaseWallTime } from '../../services/wallClock.js';
|
||||
import { accessAuthedInputProcedure, accessAuthedProcedure, router } from '../../trpc.js';
|
||||
import { getMyGeneral } from '../shared/general.js';
|
||||
@@ -216,6 +217,7 @@ export const diplomacyRouter = router({
|
||||
assertNationAccess(me);
|
||||
|
||||
const permission = await resolvePermissionLevel(ctx, me.nationId);
|
||||
const audit = createDiplomacyDocumentAudit(ctx, me, permission);
|
||||
if (permission < 4) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: '권한이 부족합니다.' });
|
||||
}
|
||||
@@ -262,20 +264,21 @@ export const diplomacyRouter = router({
|
||||
}
|
||||
|
||||
if (prevLetter.state === 'PROPOSED') {
|
||||
const before = projectAuditDocumentState(prevLetter);
|
||||
const aux = asRecord(prevLetter.aux);
|
||||
aux.reason = {
|
||||
who: me.id,
|
||||
action: 'new_letter',
|
||||
reason: 'new_letter',
|
||||
};
|
||||
await ctx.db.diplomacyLetter.update({
|
||||
const updated = await ctx.db.diplomacyLetter.update({
|
||||
where: { id: prevId },
|
||||
data: { state: 'REPLACED', aux: aux as GamePrisma.InputJsonValue },
|
||||
});
|
||||
audit.record(updated, before, 'LETTER_REPLACED');
|
||||
}
|
||||
|
||||
destNationId =
|
||||
prevLetter.srcNationId === me.nationId ? prevLetter.destNationId : prevLetter.srcNationId;
|
||||
destNationId = prevLetter.srcNationId === me.nationId ? prevLetter.destNationId : prevLetter.srcNationId;
|
||||
}
|
||||
|
||||
const nations = await ctx.db.nation.findMany({
|
||||
@@ -320,6 +323,7 @@ export const diplomacyRouter = router({
|
||||
},
|
||||
});
|
||||
|
||||
audit.record(created, null, 'LETTER_PROPOSED');
|
||||
const letterIdText = String(created.id);
|
||||
const josaYi = JosaUtil.pick(letterIdText, '이');
|
||||
const text = prevId
|
||||
@@ -333,6 +337,7 @@ export const diplomacyRouter = router({
|
||||
time: created.date,
|
||||
});
|
||||
|
||||
await audit.flush();
|
||||
return { id: created.id };
|
||||
}),
|
||||
respondLetter: accessAuthedInputProcedure(
|
||||
@@ -347,6 +352,7 @@ export const diplomacyRouter = router({
|
||||
assertNationAccess(me);
|
||||
|
||||
const permission = await resolvePermissionLevel(ctx, me.nationId);
|
||||
const audit = createDiplomacyDocumentAudit(ctx, me, permission);
|
||||
if (permission < 4) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: '권한이 부족합니다.' });
|
||||
}
|
||||
@@ -362,14 +368,11 @@ export const diplomacyRouter = router({
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: '서신이 없습니다.' });
|
||||
}
|
||||
|
||||
const { srcNation, destNation } = await loadLetterNations(
|
||||
ctx,
|
||||
letter.srcNationId,
|
||||
letter.destNationId
|
||||
);
|
||||
const { srcNation, destNation } = await loadLetterNations(ctx, letter.srcNationId, letter.destNationId);
|
||||
const messageSrc = buildActorTarget(me, destNation);
|
||||
const messageDest = buildNationTarget(srcNation);
|
||||
const messageTime = await readDatabaseWallTime(ctx.db);
|
||||
const before = projectAuditDocumentState(letter);
|
||||
const aux = asRecord(letter.aux);
|
||||
let messageText: string;
|
||||
if (input.agree) {
|
||||
@@ -379,7 +382,7 @@ export const diplomacyRouter = router({
|
||||
dest.generalIcon = messageSrc.icon;
|
||||
aux.dest = dest;
|
||||
|
||||
await ctx.db.diplomacyLetter.update({
|
||||
const updated = await ctx.db.diplomacyLetter.update({
|
||||
where: { id: letter.id },
|
||||
data: {
|
||||
state: 'ACTIVATED',
|
||||
@@ -388,16 +391,20 @@ export const diplomacyRouter = router({
|
||||
},
|
||||
});
|
||||
|
||||
audit.record(updated, before, 'LETTER_ACCEPTED');
|
||||
let prevId = letter.prevId;
|
||||
while (prevId) {
|
||||
const prevLetter = await ctx.db.diplomacyLetter.findFirst({ where: { id: prevId } });
|
||||
if (!prevLetter) {
|
||||
break;
|
||||
}
|
||||
await ctx.db.diplomacyLetter.update({
|
||||
const updated = await ctx.db.diplomacyLetter.update({
|
||||
where: { id: prevId },
|
||||
data: { state: 'REPLACED' },
|
||||
});
|
||||
if (prevLetter.state !== 'REPLACED') {
|
||||
audit.record(updated, projectAuditDocumentState(prevLetter), 'LETTER_REPLACED');
|
||||
}
|
||||
prevId = prevLetter.prevId;
|
||||
}
|
||||
messageText = `외교 서신( #${letter.id})이 승인되었습니다.`;
|
||||
@@ -407,10 +414,11 @@ export const diplomacyRouter = router({
|
||||
action: 'disagree',
|
||||
reason: input.reason ?? '',
|
||||
};
|
||||
await ctx.db.diplomacyLetter.update({
|
||||
const updated = await ctx.db.diplomacyLetter.update({
|
||||
where: { id: letter.id },
|
||||
data: { state: 'CANCELLED', aux: aux as GamePrisma.InputJsonValue },
|
||||
});
|
||||
audit.record(updated, before, 'LETTER_REJECTED');
|
||||
messageText = `외교 서신(#${letter.id})이 거부되었습니다.`;
|
||||
if (input.reason && input.reason !== '0') {
|
||||
messageText += ` 이유 : ${input.reason}`;
|
||||
@@ -426,14 +434,16 @@ export const diplomacyRouter = router({
|
||||
includeNational: true,
|
||||
});
|
||||
|
||||
await audit.flush();
|
||||
return { ok: true };
|
||||
}),
|
||||
rollbackLetter: accessAuthedInputProcedure(z.object({ letterId: z.number().int().positive() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
rollbackLetter: accessAuthedInputProcedure(z.object({ letterId: z.number().int().positive() })).mutation(
|
||||
async ({ ctx, input }) => {
|
||||
const me = await getMyGeneral(ctx);
|
||||
assertNationAccess(me);
|
||||
|
||||
const permission = await resolvePermissionLevel(ctx, me.nationId);
|
||||
const audit = createDiplomacyDocumentAudit(ctx, me, permission);
|
||||
if (permission < 4) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: '권한이 부족합니다.' });
|
||||
}
|
||||
@@ -449,14 +459,11 @@ export const diplomacyRouter = router({
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: '서신이 없습니다.' });
|
||||
}
|
||||
|
||||
const { srcNation, destNation } = await loadLetterNations(
|
||||
ctx,
|
||||
letter.srcNationId,
|
||||
letter.destNationId
|
||||
);
|
||||
const { srcNation, destNation } = await loadLetterNations(ctx, letter.srcNationId, letter.destNationId);
|
||||
const messageSrc = buildActorTarget(me, srcNation);
|
||||
const messageDest = buildNationTarget(destNation);
|
||||
const messageTime = await readDatabaseWallTime(ctx.db);
|
||||
const before = projectAuditDocumentState(letter);
|
||||
const aux = asRecord(letter.aux);
|
||||
aux.reason = {
|
||||
who: me.id,
|
||||
@@ -464,11 +471,12 @@ export const diplomacyRouter = router({
|
||||
reason: '회수',
|
||||
};
|
||||
|
||||
await ctx.db.diplomacyLetter.update({
|
||||
const updated = await ctx.db.diplomacyLetter.update({
|
||||
where: { id: letter.id },
|
||||
data: { state: 'CANCELLED', aux: aux as GamePrisma.InputJsonValue },
|
||||
});
|
||||
|
||||
audit.record(updated, before, 'LETTER_WITHDRAWN');
|
||||
await sendDocumentNotice({
|
||||
ctx,
|
||||
src: messageSrc,
|
||||
@@ -477,14 +485,17 @@ export const diplomacyRouter = router({
|
||||
time: messageTime,
|
||||
});
|
||||
|
||||
await audit.flush();
|
||||
return { ok: true };
|
||||
}),
|
||||
destroyLetter: accessAuthedInputProcedure(z.object({ letterId: z.number().int().positive() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
}
|
||||
),
|
||||
destroyLetter: accessAuthedInputProcedure(z.object({ letterId: z.number().int().positive() })).mutation(
|
||||
async ({ ctx, input }) => {
|
||||
const me = await getMyGeneral(ctx);
|
||||
assertNationAccess(me);
|
||||
|
||||
const permission = await resolvePermissionLevel(ctx, me.nationId);
|
||||
const audit = createDiplomacyDocumentAudit(ctx, me, permission);
|
||||
if (permission < 4) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: '권한이 부족합니다.' });
|
||||
}
|
||||
@@ -500,6 +511,7 @@ export const diplomacyRouter = router({
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: '서신이 없습니다.' });
|
||||
}
|
||||
|
||||
const before = projectAuditDocumentState(letter);
|
||||
const aux = asRecord(letter.aux);
|
||||
const stateOpt = typeof aux.state_opt === 'string' ? aux.state_opt : null;
|
||||
const myStateOpt = letter.srcNationId === me.nationId ? 'try_destroy_src' : 'try_destroy_dest';
|
||||
@@ -508,11 +520,7 @@ export const diplomacyRouter = router({
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: '이미 파기 신청을 했습니다.' });
|
||||
}
|
||||
|
||||
const { srcNation, destNation } = await loadLetterNations(
|
||||
ctx,
|
||||
letter.srcNationId,
|
||||
letter.destNationId
|
||||
);
|
||||
const { srcNation, destNation } = await loadLetterNations(ctx, letter.srcNationId, letter.destNationId);
|
||||
const actorNation = letter.srcNationId === me.nationId ? srcNation : destNation;
|
||||
const otherNation = letter.srcNationId === me.nationId ? destNation : srcNation;
|
||||
const messageSrc = buildActorTarget(me, actorNation);
|
||||
@@ -522,18 +530,20 @@ export const diplomacyRouter = router({
|
||||
let messageText: string;
|
||||
|
||||
if (stateOpt && stateOpt !== myStateOpt) {
|
||||
await ctx.db.diplomacyLetter.update({
|
||||
const updated = await ctx.db.diplomacyLetter.update({
|
||||
where: { id: letter.id },
|
||||
data: { state: 'CANCELLED', aux: aux as GamePrisma.InputJsonValue },
|
||||
});
|
||||
audit.record(updated, before, 'LETTER_DESTROYED');
|
||||
resultState = 'CANCELLED';
|
||||
messageText = `외교 서신(#${letter.id})을 파기했습니다.`;
|
||||
} else {
|
||||
aux.state_opt = myStateOpt;
|
||||
await ctx.db.diplomacyLetter.update({
|
||||
const updated = await ctx.db.diplomacyLetter.update({
|
||||
where: { id: letter.id },
|
||||
data: { aux: aux as GamePrisma.InputJsonValue },
|
||||
});
|
||||
audit.record(updated, before, 'LETTER_DESTROY_REQUESTED');
|
||||
resultState = 'ACTIVATED';
|
||||
messageText = `외교 서신(#${letter.id})을 파기 요청합니다.`;
|
||||
}
|
||||
@@ -545,6 +555,8 @@ export const diplomacyRouter = router({
|
||||
text: messageText,
|
||||
time: messageTime,
|
||||
});
|
||||
await audit.flush();
|
||||
return { state: resultState };
|
||||
}),
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import { asRecord, GameClock, inferClockPhase, parseGameClockPhase, readTurnRecovery } from '@sammo-ts/common';
|
||||
import {
|
||||
GamePrisma,
|
||||
hashAuditDiplomacyDocument,
|
||||
persistAuditDiplomacyEvents,
|
||||
readTurnRuntimeReady,
|
||||
type AuditDiplomacyEventDraft,
|
||||
} from '@sammo-ts/infra';
|
||||
import type { GameApiContext, GeneralRow } from '../context.js';
|
||||
|
||||
type Letter = GamePrisma.DiplomacyLetterGetPayload<Record<string, never>>;
|
||||
type DocumentAction =
|
||||
| 'LETTER_PROPOSED'
|
||||
| 'LETTER_REPLACED'
|
||||
| 'LETTER_ACCEPTED'
|
||||
| 'LETTER_REJECTED'
|
||||
| 'LETTER_WITHDRAWN'
|
||||
| 'LETTER_DESTROY_REQUESTED'
|
||||
| 'LETTER_DESTROYED';
|
||||
|
||||
export const projectAuditDocumentState = (letter: Letter): Record<string, unknown> => {
|
||||
const aux = asRecord(letter.aux);
|
||||
const src = asRecord(aux.src);
|
||||
const dest = asRecord(aux.dest);
|
||||
const reason = asRecord(aux.reason);
|
||||
return {
|
||||
state: letter.state,
|
||||
srcSignerId: letter.srcSignerId,
|
||||
destSignerId: letter.destSignerId,
|
||||
srcNationName: typeof src.nationName === 'string' ? src.nationName : null,
|
||||
destNationName: typeof dest.nationName === 'string' ? dest.nationName : null,
|
||||
srcSignerName: typeof src.generalName === 'string' ? src.generalName : null,
|
||||
destSignerName: typeof dest.generalName === 'string' ? dest.generalName : null,
|
||||
stateOption: typeof aux.state_opt === 'string' ? aux.state_opt : null,
|
||||
reason: typeof reason.reason === 'string' ? reason.reason : null,
|
||||
reasonAction: typeof reason.action === 'string' ? reason.action : null,
|
||||
reasonActorId: typeof reason.who === 'number' ? reason.who : null,
|
||||
};
|
||||
};
|
||||
|
||||
interface AuditCoordinateRow {
|
||||
serverId: string | null;
|
||||
year: number;
|
||||
month: number;
|
||||
wallNow: Date;
|
||||
clockBaseTime: Date | null;
|
||||
clockTick: bigint | null;
|
||||
clockMode: string;
|
||||
clockWallAnchor: Date | null;
|
||||
clockPhase: string;
|
||||
clockRevision: bigint;
|
||||
clockRecoveryStartTick: bigint | null;
|
||||
clockRecoveryEndTick: bigint | null;
|
||||
clockRecoveryStartWallAt: Date | null;
|
||||
tickSeconds: number;
|
||||
}
|
||||
|
||||
/** API 입력 transaction의 기존 clock fence 안에서 작은 시계/기수 투영만 읽는다. */
|
||||
const readCoordinate = async (ctx: GameApiContext) => {
|
||||
const [row] = await ctx.db.$queryRaw<AuditCoordinateRow[]>(GamePrisma.sql`
|
||||
SELECT meta->>'serverId' AS "serverId", current_year AS year, current_month AS month,
|
||||
CURRENT_TIMESTAMP AT TIME ZONE 'UTC' AS "wallNow",
|
||||
clock_base_time AS "clockBaseTime", clock_tick AS "clockTick", clock_mode AS "clockMode",
|
||||
clock_wall_anchor AS "clockWallAnchor", clock_phase AS "clockPhase", clock_revision AS "clockRevision",
|
||||
clock_recovery_start_tick AS "clockRecoveryStartTick", clock_recovery_end_tick AS "clockRecoveryEndTick",
|
||||
clock_recovery_start_wall_at AS "clockRecoveryStartWallAt", tick_seconds AS "tickSeconds"
|
||||
FROM world_state ORDER BY id LIMIT 1
|
||||
`);
|
||||
if (!row?.serverId?.trim()) return null;
|
||||
let tick: bigint | null = null;
|
||||
if (row.clockBaseTime && row.clockTick !== null && row.clockWallAnchor) {
|
||||
const clockTick = Number(row.clockTick);
|
||||
const revision = Number(row.clockRevision);
|
||||
if (!Number.isSafeInteger(clockTick) || !Number.isSafeInteger(revision))
|
||||
throw new Error('Play audit diplomacy clock outside safe integer range');
|
||||
const mode = row.clockMode === 'manual' ? 'manual' : 'realtime';
|
||||
const phase = row.clockPhase ? parseGameClockPhase(row.clockPhase) : inferClockPhase(mode);
|
||||
const clock = new GameClock({
|
||||
baseTime: row.clockBaseTime,
|
||||
tick: clockTick,
|
||||
mode,
|
||||
wallAnchor: row.clockWallAnchor,
|
||||
recovery: readTurnRecovery(row),
|
||||
turnSeconds: row.tickSeconds,
|
||||
phase,
|
||||
revision,
|
||||
});
|
||||
const ready =
|
||||
phase !== 'RUNNING' || mode !== 'realtime' || (await readTurnRuntimeReady(ctx.db, row.clockRevision));
|
||||
tick = BigInt(ready ? clock.nowTick(row.wallNow) : clock.tick);
|
||||
}
|
||||
return {
|
||||
serverId: row.serverId,
|
||||
year: row.year,
|
||||
month: row.month,
|
||||
tick,
|
||||
clockRevision: row.clockRevision,
|
||||
wallAt: row.wallNow,
|
||||
};
|
||||
};
|
||||
|
||||
export const createDiplomacyDocumentAudit = (ctx: GameApiContext, actor: GeneralRow, permission: number) => {
|
||||
const changes: {
|
||||
letter: Letter;
|
||||
before: Record<string, unknown> | null;
|
||||
after: Record<string, unknown>;
|
||||
eventType: DocumentAction;
|
||||
}[] = [];
|
||||
return {
|
||||
record: (letter: Letter, before: Record<string, unknown> | null, eventType: DocumentAction) => {
|
||||
if (!ctx.auditInput) return;
|
||||
changes.push({ letter, before, after: projectAuditDocumentState(letter), eventType });
|
||||
},
|
||||
flush: async (): Promise<void> => {
|
||||
// 무transaction legacy unit fixture에는 입력 원장 identity를 꾸며 넣지 않는다.
|
||||
const input = ctx.auditInput;
|
||||
if (!input || !changes.length) return;
|
||||
if (input.actorUserId !== actor.userId || input.actorUserId !== ctx.auth?.user.id)
|
||||
throw new Error('Play audit diplomacy actor mismatch');
|
||||
const coordinate = await readCoordinate(ctx);
|
||||
if (!coordinate) return;
|
||||
const events: AuditDiplomacyEventDraft[] = changes.map((change, index) => ({
|
||||
schemaVersion: 1,
|
||||
serverId: coordinate.serverId,
|
||||
srcNationId: change.letter.srcNationId,
|
||||
destNationId: change.letter.destNationId,
|
||||
category: 'DOCUMENT',
|
||||
source: 'API',
|
||||
eventType: change.eventType,
|
||||
documentId: change.letter.id,
|
||||
documentHash: hashAuditDiplomacyDocument(change.letter),
|
||||
previousDocumentId: change.letter.prevId,
|
||||
year: coordinate.year,
|
||||
month: coordinate.month,
|
||||
tick: coordinate.tick,
|
||||
clockRevision: coordinate.clockRevision,
|
||||
executionId: `api:${input.requestId}`,
|
||||
ordinal: index + 1,
|
||||
requestId: input.requestId,
|
||||
inputSequence: input.sequence,
|
||||
actor: {
|
||||
userId: actor.userId,
|
||||
generalId: actor.id,
|
||||
name: actor.name,
|
||||
nationId: actor.nationId,
|
||||
officerLevel: actor.officerLevel,
|
||||
npcState: actor.npcState,
|
||||
permission,
|
||||
},
|
||||
before: change.before,
|
||||
after: change.after,
|
||||
}));
|
||||
await persistAuditDiplomacyEvents(ctx.db, events);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -81,11 +81,12 @@ const createInputEventMiddleware = (acquireClockFence: boolean) =>
|
||||
payload,
|
||||
actorUserId: ctx.auth?.user.id,
|
||||
acquireClockFence,
|
||||
execute: async (transaction) => {
|
||||
execute: async (transaction, auditInput) => {
|
||||
const result = await next({
|
||||
ctx: {
|
||||
...ctx,
|
||||
db: transaction,
|
||||
auditInput,
|
||||
changeJournal,
|
||||
turnDaemon: new IdempotentTurnDaemonTransport(ctx.turnDaemon, requestId),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user