mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-23 00:27:16 +00:00
fix: record failed data grid saves in history
This commit is contained in:
@@ -577,10 +577,11 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
statements: string[],
|
||||
rollbackStatements: string[],
|
||||
elapsed: number,
|
||||
historyResult?: { affected_rows?: number },
|
||||
historyResult?: { affected_rows?: number; success?: boolean; error?: string },
|
||||
) {
|
||||
if (!connectionId.value || !database.value || !tableMeta.value) return;
|
||||
const connName = connectionStore.getConfig(connectionId.value)?.name || "";
|
||||
const success = historyResult?.success ?? true;
|
||||
const details = {
|
||||
schema: tableMeta.value.schema,
|
||||
table: tableMeta.value.tableName,
|
||||
@@ -588,7 +589,8 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
updated_rows: dirtyRows.value.size,
|
||||
deleted_rows: deletedRows.value.size,
|
||||
statements,
|
||||
rollback_statements: rollbackStatements,
|
||||
rollback_statements: success ? rollbackStatements : [],
|
||||
error: success ? undefined : historyResult?.error,
|
||||
};
|
||||
await historyStore.add({
|
||||
connection_id: connectionId.value,
|
||||
@@ -596,16 +598,35 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
database: database.value,
|
||||
sql: statements.join("\n"),
|
||||
execution_time_ms: elapsed,
|
||||
success: true,
|
||||
success,
|
||||
error: success ? undefined : historyResult?.error,
|
||||
activity_kind: "data_change",
|
||||
operation: dataChangeOperation(),
|
||||
target: tableHistoryTarget(),
|
||||
affected_rows: historyResult?.affected_rows ?? statements.length,
|
||||
rollback_sql: rollbackStatements.length ? rollbackStatements.join("\n") : undefined,
|
||||
affected_rows: success ? (historyResult?.affected_rows ?? statements.length) : undefined,
|
||||
rollback_sql: success && rollbackStatements.length ? rollbackStatements.join("\n") : undefined,
|
||||
details_json: JSON.stringify(details),
|
||||
});
|
||||
}
|
||||
|
||||
async function recordFailedDataGridHistory(
|
||||
statements: string[],
|
||||
rollbackStatements: string[],
|
||||
start: number,
|
||||
error: unknown,
|
||||
) {
|
||||
const message = normalizeDataGridSaveError(databaseType.value, error);
|
||||
try {
|
||||
await recordDataGridHistory(statements, rollbackStatements, Date.now() - start, {
|
||||
success: false,
|
||||
error: message,
|
||||
});
|
||||
} catch (historyError) {
|
||||
console.warn("[DBX] failed to record data grid history", historyError);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
function reloadCurrentData() {
|
||||
options.emit(
|
||||
"reload",
|
||||
@@ -691,7 +712,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
preparedSave?.executionSchema,
|
||||
);
|
||||
} catch (e: any) {
|
||||
saveError.value = normalizeDataGridSaveError(databaseType.value, e);
|
||||
saveError.value = await recordFailedDataGridHistory(stmts, rollbackStmts, start, e);
|
||||
isSaving.value = false;
|
||||
return;
|
||||
}
|
||||
@@ -699,7 +720,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
try {
|
||||
apiResult = await api.executeBatch(connectionId.value, database.value, stmts);
|
||||
} catch (e: any) {
|
||||
saveError.value = normalizeDataGridSaveError(databaseType.value, e);
|
||||
saveError.value = await recordFailedDataGridHistory(stmts, rollbackStmts, start, e);
|
||||
isSaving.value = false;
|
||||
return;
|
||||
}
|
||||
@@ -709,7 +730,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
await onExecuteSql.value(sqlStmt);
|
||||
}
|
||||
} catch (e: any) {
|
||||
saveError.value = normalizeDataGridSaveError(databaseType.value, e);
|
||||
saveError.value = await recordFailedDataGridHistory(stmts, rollbackStmts, start, e);
|
||||
isSaving.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -443,3 +443,90 @@ test("saving manually typed JSON from a MySQL grid normalizes smart quotes", asy
|
||||
`UPDATE "settings" SET "payload" = '{"2:3":"3:4","3:2":"4:3","21:9":"16:9"}' WHERE "id" = 1;`,
|
||||
]);
|
||||
});
|
||||
|
||||
test("failed table data save records a failed history entry", async () => {
|
||||
setActivePinia(createPinia());
|
||||
installBrowserTestGlobals();
|
||||
|
||||
const permissionError = "Statement 1 failed: Server error: ERROR 42000 (1142): UPDATE command denied to user";
|
||||
const savedHistoryEntries: Array<Record<string, unknown>> = [];
|
||||
globalThis.fetch = (async (input, init) => {
|
||||
const url = String(input);
|
||||
if (url === "/api/query/prepare-data-grid-save") {
|
||||
const body = JSON.parse(String(init?.body ?? "{}"));
|
||||
const options = body.options as DataGridSaveStatementOptions;
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
statements: mockPreparedSaveStatements(options),
|
||||
rollbackStatements: [`UPDATE "pp_questions" SET "title" = 'Old title' WHERE "id" = 1;`],
|
||||
executionSchema: options.tableMeta.schema,
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
if (url === "/api/query/execute-in-transaction") {
|
||||
return new Response(permissionError, { status: 500 });
|
||||
}
|
||||
if (url === "/api/history/save") {
|
||||
savedHistoryEntries.push(JSON.parse(String(init?.body ?? "{}")).entry);
|
||||
return new Response("null", { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
}
|
||||
return new Response(`unexpected request: ${url}`, { status: 500 });
|
||||
}) as typeof fetch;
|
||||
|
||||
const result = computed(() => ({
|
||||
columns: ["id", "title"],
|
||||
rows: [[1, "Old title"] as CellValue[]],
|
||||
}));
|
||||
const rowStatusFilter = ref<"all" | "changed" | "edited" | "new" | "deleted">("all");
|
||||
const editor = useDataGridEditor({
|
||||
result,
|
||||
editable: computed(() => true),
|
||||
databaseType: computed(() => "mysql"),
|
||||
connectionId: computed(() => "conn-1"),
|
||||
database: computed(() => "app_db"),
|
||||
tableMeta: computed(() => ({
|
||||
tableName: "pp_questions",
|
||||
columns: [column("id", true), column("title")],
|
||||
primaryKeys: ["id"],
|
||||
})),
|
||||
onExecuteSql: computed(() => undefined),
|
||||
customSave: computed(() => undefined),
|
||||
sql: computed(() => "SELECT id, title FROM pp_questions"),
|
||||
searchText: ref(""),
|
||||
whereFilterInput: ref(""),
|
||||
orderByInput: ref(""),
|
||||
rowStatusFilter,
|
||||
pageSize: ref(50),
|
||||
currentPage: ref(1),
|
||||
getRowItem: (rowId) => {
|
||||
if (rowId !== 0) return undefined;
|
||||
return {
|
||||
id: 0,
|
||||
sourceIndex: 0,
|
||||
data: result.value.rows[0],
|
||||
isNew: false,
|
||||
isDeleted: false,
|
||||
isDirtyCol: [false, false],
|
||||
status: "clean",
|
||||
};
|
||||
},
|
||||
emit: () => {},
|
||||
});
|
||||
|
||||
editor.applyCellValue(0, 1, "New title");
|
||||
await editor.saveChanges();
|
||||
|
||||
assert.equal(editor.saveError.value, permissionError);
|
||||
assert.equal(editor.dirtyRows.value.size, 1);
|
||||
assert.equal(savedHistoryEntries.length, 1);
|
||||
const historyEntry = savedHistoryEntries[0];
|
||||
assert.equal(historyEntry.success, false);
|
||||
assert.equal(historyEntry.error, permissionError);
|
||||
assert.equal(historyEntry.activity_kind, "data_change");
|
||||
assert.equal(historyEntry.operation, "UPDATE");
|
||||
assert.equal(historyEntry.target, "pp_questions");
|
||||
assert.equal(historyEntry.rollback_sql, undefined);
|
||||
assert.equal(historyEntry.affected_rows, undefined);
|
||||
assert.equal(historyEntry.sql, `UPDATE "pp_questions" SET "title" = 'New title' WHERE "id" = 1;`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user