first commit

This commit is contained in:
2026-06-09 18:35:55 -03:00
commit 3581e221d0
448 changed files with 519709 additions and 0 deletions
+194
View File
@@ -0,0 +1,194 @@
import {
ClarizenApiError,
entityObjectPath,
type Clarizen,
type EntityId,
type JsonValue,
} from "@arco/clarizen-lib";
import type { EntityRecord } from "../query/fetch-all.js";
import { ProgressReporter } from "./progress.js";
const BATCH_SIZE = 100;
export interface UpdateError {
id: EntityId;
displayName?: string;
message: string;
errorCode?: string;
}
export interface BulkUpdateResult {
updated: number;
errors: UpdateError[];
}
interface BulkResponse {
responses?: Array<{
statusCode?: number;
body?: unknown;
}>;
}
function shortId(id: EntityId): string {
const str = String(id);
return str.includes("/") ? str.split("/").pop()! : str;
}
function buildUpdateBody(fieldName: string, value: JsonValue): string {
return JSON.stringify({ [fieldName]: value });
}
function extractErrorMessage(body: unknown): { message: string; errorCode?: string } {
if (body && typeof body === "object") {
const obj = body as Record<string, unknown>;
const message =
typeof obj.message === "string" ? obj.message : JSON.stringify(body);
const errorCode =
typeof obj.errorCode === "string" ? obj.errorCode : undefined;
return { message, errorCode };
}
return { message: String(body) };
}
async function updateSingle(
clarizen: Clarizen,
typeName: string,
id: EntityId,
fieldName: string,
value: JsonValue,
): Promise<void> {
await clarizen.objects.update(typeName, id, { [fieldName]: value });
}
async function updateBatchBulk(
clarizen: Clarizen,
typeName: string,
batch: EntityRecord[],
fieldName: string,
value: JsonValue,
): Promise<Array<{ record: EntityRecord; error?: UpdateError }>> {
const requests = batch.map((record) => ({
url: entityObjectPath(typeName, shortId(record.id)).replace(
/^\/V2\.0\/services/,
"",
),
method: "POST" as const,
body: buildUpdateBody(fieldName, value),
}));
try {
const result = (await clarizen.bulk.execute({
requests,
})) as BulkResponse;
const responses = result.responses ?? [];
return batch.map((record, index) => {
const response = responses[index];
if (!response || response.statusCode === 200) {
return { record };
}
const { message, errorCode } = extractErrorMessage(response.body);
return {
record,
error: {
id: record.id,
displayName: record.displayName,
message,
errorCode,
},
};
});
} catch (err) {
const message =
err instanceof ClarizenApiError
? err.message
: err instanceof Error
? err.message
: "Falha no lote bulk";
return batch.map((record) => ({
record,
error: {
id: record.id,
displayName: record.displayName,
message,
errorCode:
err instanceof ClarizenApiError ? String(err.errorCode) : undefined,
},
}));
}
}
async function retryIndividually(
clarizen: Clarizen,
typeName: string,
record: EntityRecord,
fieldName: string,
value: JsonValue,
): Promise<UpdateError | undefined> {
try {
await updateSingle(clarizen, typeName, record.id, fieldName, value);
return undefined;
} catch (err) {
if (err instanceof ClarizenApiError) {
return {
id: record.id,
displayName: record.displayName,
message: err.message,
errorCode: String(err.errorCode),
};
}
return {
id: record.id,
displayName: record.displayName,
message: err instanceof Error ? err.message : "Erro desconhecido",
};
}
}
export async function bulkUpdateField(
clarizen: Clarizen,
typeName: string,
records: EntityRecord[],
fieldName: string,
value: JsonValue,
): Promise<BulkUpdateResult> {
const progress = new ProgressReporter(records.length);
let updated = 0;
const errors: UpdateError[] = [];
for (let i = 0; i < records.length; i += BATCH_SIZE) {
const batch = records.slice(i, i + BATCH_SIZE);
const results = await updateBatchBulk(
clarizen,
typeName,
batch,
fieldName,
value,
);
for (const { record, error } of results) {
if (!error) {
updated++;
progress.update(updated + errors.length);
continue;
}
const retryError = await retryIndividually(
clarizen,
typeName,
record,
fieldName,
value,
);
if (retryError) {
errors.push(retryError);
} else {
updated++;
}
progress.update(updated + errors.length);
}
}
progress.finish();
return { updated, errors };
}
+56
View File
@@ -0,0 +1,56 @@
const BAR_WIDTH = 30;
function formatDuration(ms: number): string {
if (!Number.isFinite(ms) || ms < 0) return "?";
const totalSeconds = Math.ceil(ms / 1000);
if (totalSeconds < 60) return `${totalSeconds}s`;
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
if (minutes < 60) return `${minutes}m ${seconds}s`;
const hours = Math.floor(minutes / 60);
const remMinutes = minutes % 60;
return `${hours}h ${remMinutes}m`;
}
function renderBar(percent: number): string {
const clamped = Math.max(0, Math.min(100, percent));
const filled = Math.round((clamped / 100) * BAR_WIDTH);
const empty = BAR_WIDTH - filled;
return `[${"█".repeat(filled)}${"░".repeat(empty)}]`;
}
export class ProgressReporter {
private readonly startTime = Date.now();
private lastLineLength = 0;
constructor(private readonly total: number) {}
update(processed: number): void {
const percent = this.total > 0 ? (processed / this.total) * 100 : 100;
const elapsed = Date.now() - this.startTime;
const avgMs = processed > 0 ? elapsed / processed : 0;
const remaining = this.total - processed;
const etaMs = processed > 0 ? remaining * avgMs : 0;
const line = [
renderBar(percent),
`${percent.toFixed(1)}%`,
`(${processed}/${this.total})`,
processed > 0 && remaining > 0 ? `~${formatDuration(etaMs)} restantes` : "",
]
.filter(Boolean)
.join(" ");
const padded =
line.length < this.lastLineLength
? line + " ".repeat(this.lastLineLength - line.length)
: line;
this.lastLineLength = padded.length;
process.stdout.write(`\r${padded}`);
}
finish(): void {
process.stdout.write("\n");
}
}