Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"author": "Transcend Inc.",
"name": "@transcend-io/cli",
"description": "A command line interface for programmatic operations across Transcend.",
"version": "8.1.1",
"version": "8.1.2",
"homepage": "https://github.com/transcend-io/cli",
"repository": {
"type": "git",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { AnyTotals } from '../../ui';
import { readFailingUpdatesFromReceipt } from './readFailingUpdatesFromReceipt';
import { resolveReceiptPath } from './resolveReceiptPath';
import { summarizeReceipt } from './summarizeReceipt';

/**
* Applies the summary of a receipt to the overall aggregation.
*
* @param opts - Options for applying the receipt summary
*/
export function applyReceiptSummary(opts: {
/** Folder where receipts are stored */
receiptsFolder: string;
/** Path to the file being processed */
filePath: string;
/** Path to the receipt file, if different from the default */
receiptFilepath?: string | null;
/** Aggregation object to update */
agg: AnyTotals;
/** Whether this is a dry run (no actual updates) */
dryRun: boolean;
/** Array to collect failing updates from the receipt */
failingUpdatesMem: Array<unknown>;
}): void {
const {
receiptsFolder,
filePath,
receiptFilepath,
agg,
dryRun,
failingUpdatesMem,
} = opts;

const resolved =
(typeof receiptFilepath === 'string' && receiptFilepath) ||
resolveReceiptPath(receiptsFolder, filePath);

if (!resolved) return;

const summary = summarizeReceipt(resolved, dryRun);

// collect failing updates
failingUpdatesMem.push(...readFailingUpdatesFromReceipt(resolved, filePath));

// merge totals
if (summary.mode === 'upload' && agg.mode === 'upload') {
agg.success += summary.success;
agg.skipped += summary.skipped;
agg.error += summary.error;
Object.entries(summary.errors).forEach(([k, v]) => {
(agg.errors as Record<string, number>)[k] =
(agg.errors[k] ?? 0) + (v as number);
});
} else if (summary.mode === 'check' && agg.mode === 'check') {
agg.totalPending += summary.totalPending;
agg.pendingConflicts += summary.pendingConflicts;
agg.pendingSafe += summary.pendingSafe;
agg.skipped += summary.skipped;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './readFailingUpdatesFromReceipt';
export * from './summarizeReceipt';
export * from './resolveReceiptPath';
export * from './applyReceiptSummary';
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { readFileSync } from 'node:fs';
import type { FailingUpdateRow } from '..';

/**
* Parse failing updates out of a receipts.json file.
* Returns rows you can merge into your in-memory buffer.
*
* @param receiptPath - The path to the receipts.json file
* @param sourceFile - Optional source file for context
* @returns An array of FailingUpdateRow objects
*/
export function readFailingUpdatesFromReceipt(
receiptPath: string,
sourceFile?: string,
): FailingUpdateRow[] {
try {
const raw = readFileSync(receiptPath, 'utf8');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const json = JSON.parse(raw) as any;
const failing = json?.failingUpdates ?? {};
const out: FailingUpdateRow[] = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
for (const [primaryKey, val] of Object.entries<any>(failing)) {
out.push({
primaryKey,
uploadedAt: val?.uploadedAt ?? '',
error: val?.error ?? '',
updateJson: val?.update ? JSON.stringify(val.update) : '',
sourceFile,
});
}
return out;
} catch {
return [];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { join } from 'node:path';
import { getFilePrefix } from '../computeFiles';
import { existsSync, readdirSync, statSync } from 'node:fs';

/**
* Find the receipt JSON for a given input file (supports suffixes like __1).
*
* @param receiptsFolder - Where to look for receipts
* @param filePath - The input file path to match against
* @returns The path to the receipt file, or null if not found
*/
export function resolveReceiptPath(
receiptsFolder: string,
filePath: string,
): string | null {
const base = `${getFilePrefix(filePath)}-receipts.json`;
const exact = join(receiptsFolder, base);
if (existsSync(exact)) return exact;

const prefix = `${getFilePrefix(filePath)}-receipts`;
try {
const entries = readdirSync(receiptsFolder)
.filter((n) => n.startsWith(prefix) && n.endsWith('.json'))
.map((name) => {
const full = join(receiptsFolder, name);
let mtime = 0;
try {
mtime = statSync(full).mtimeMs;
} catch {
// ignore if stat fails
}
return { full, mtime };
})
.sort((a, b) => b.mtime - a.mtime);
return entries[0]?.full ?? null;
} catch {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { readFileSync } from 'node:fs';
import type { AnyTotals } from '../../ui';

/**
* Summarize a receipts JSON into dashboard counters.
*
* @param receiptPath - The path to the receipt file
* @param dryRun - Whether this is a dry run (no actual upload)
* @returns An object summarizing the receipt data
*/
export function summarizeReceipt(
receiptPath: string,
dryRun: boolean,
): AnyTotals {
try {
const raw = readFileSync(receiptPath, 'utf8');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const json = JSON.parse(raw) as any;

const skippedCount = Object.values(json?.skippedUpdates ?? {}).length;

if (!dryRun) {
const success = Object.values(json?.successfulUpdates ?? {}).length;
const failed = Object.values(json?.failingUpdates ?? {}).length;
const errors: Record<string, number> = {};
Object.values(json?.failingUpdates ?? {}).forEach((v) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const msg = (v as any)?.error ?? 'Unknown error';
errors[msg] = (errors[msg] ?? 0) + 1;
});
return {
mode: 'upload',
success,
skipped: skippedCount,
error: failed,
errors,
};
}

const totalPending = Object.values(json?.pendingUpdates ?? {}).length;
const pendingConflicts = Object.values(
json?.pendingConflictUpdates ?? {},
).length;
const pendingSafe = Object.values(json?.pendingSafeUpdates ?? {}).length;

return {
mode: 'check',
totalPending,
pendingConflicts,
pendingSafe,
skipped: skippedCount,
};
} catch {
return !dryRun
? { mode: 'upload', success: 0, skipped: 0, error: 0, errors: {} }
: {
mode: 'check',
totalPending: 0,
pendingConflicts: 0,
pendingSafe: 0,
skipped: 0,
};
}
}
Loading
Loading