fix(n8n): make replacement replay idempotent
This commit is contained in:
@@ -109,10 +109,6 @@ export function buildPublishPayload(
|
||||
): NdcPublishPayload {
|
||||
if (!Number.isInteger(sequence) || sequence < 0 || sequence > 2_147_483_647) throw new Error('batch_sequence_invalid');
|
||||
requireIdentifier(dataProductId, 'dataProductId');
|
||||
const runId = `run-${sha256(`${workflowId}|${executionId}`).slice(0, 48)}`;
|
||||
const idempotencyKey = `publish-${sha256(
|
||||
`${workflowId}|${executionId}|${nodeId}|${dataProductId}|${sequence}`,
|
||||
)}`;
|
||||
const facts = factsFromItems(items, publishMode === 'replace');
|
||||
const explicitGenerationAt = items.length === 1 && Array.isArray(items[0]?.json?.facts)
|
||||
? items[0].json.generationAt
|
||||
@@ -120,6 +116,15 @@ export function buildPublishPayload(
|
||||
const generationAt = publishMode === 'replace'
|
||||
? replacementGenerationAt(facts, explicitGenerationAt)
|
||||
: undefined;
|
||||
const replacementDigest = generationAt
|
||||
? replacementReplayDigest(dataProductId, sequence, generationAt, facts)
|
||||
: undefined;
|
||||
const runId = replacementDigest
|
||||
? `run-${replacementDigest.slice(0, 48)}`
|
||||
: `run-${sha256(`${workflowId}|${executionId}`).slice(0, 48)}`;
|
||||
const idempotencyKey = replacementDigest
|
||||
? `publish-${replacementDigest}`
|
||||
: `publish-${sha256(`${workflowId}|${executionId}|${nodeId}|${dataProductId}|${sequence}`)}`;
|
||||
return {
|
||||
schemaVersion: DATA_PRODUCT_PUBLISH_SCHEMA_VERSION,
|
||||
batch: {
|
||||
@@ -332,6 +337,35 @@ function replacementGenerationAt(facts: NdcFact[], explicit: unknown): string {
|
||||
return timestamps[0];
|
||||
}
|
||||
|
||||
function replacementReplayDigest(
|
||||
dataProductId: string,
|
||||
sequence: number,
|
||||
generationAt: string,
|
||||
facts: NdcFact[],
|
||||
): string {
|
||||
const canonicalFacts = [...facts].sort((left, right) => {
|
||||
const leftKey = `${left.sourceId}\u0000${left.semanticType}`;
|
||||
const rightKey = `${right.sourceId}\u0000${right.semanticType}`;
|
||||
return leftKey.localeCompare(rightKey);
|
||||
});
|
||||
return sha256(stableJson({
|
||||
schemaVersion: DATA_PRODUCT_PUBLISH_SCHEMA_VERSION,
|
||||
dataProductId,
|
||||
mode: 'replace',
|
||||
generationAt,
|
||||
sequence,
|
||||
facts: canonicalFacts,
|
||||
}));
|
||||
}
|
||||
|
||||
function stableJson(value: unknown): string {
|
||||
if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
|
||||
if (isObject(value)) {
|
||||
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`;
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function requireIsoTimestamp(value: unknown, field: string): string {
|
||||
const normalized = String(value ?? '').trim();
|
||||
if (!normalized || Number.isNaN(Date.parse(normalized))) throw new Error(`${field}_invalid`);
|
||||
|
||||
Reference in New Issue
Block a user