2
0
mirror of https://github.com/frappe/books.git synced 2024-11-08 14:50:56 +00:00

fix: better handling of FOREIGN KEY constraint failed

This commit is contained in:
18alantom 2022-08-30 14:34:18 +05:30
parent d90d113981
commit 702a09e6c0
4 changed files with 183 additions and 41 deletions

View File

@ -17,9 +17,9 @@ import {
import { getIsNullOrUndef, getMapFromList, getRandomString } from 'utils';
import { markRaw } from 'vue';
import { isPesa } from '../utils/index';
import { getDbSyncError } from './errorHelpers';
import {
areDocValuesEqual,
getInsertionError,
getMissingMandatoryMessage,
getPreDefaultValues,
setChildDocIdx,
@ -687,7 +687,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
try {
data = await this.fyo.db.insert(this.schemaName, validDict);
} catch (err) {
throw getInsertionError(err as Error, validDict);
throw await getDbSyncError(err as Error, this, this.fyo);
}
await this._syncValues(data);
@ -701,7 +701,11 @@ export class Doc extends Observable<DocValue | Doc[]> {
await this._preSync();
const data = this.getValidDict(false, true);
await this.fyo.db.update(this.schemaName, data);
try {
await this.fyo.db.update(this.schemaName, data);
} catch (err) {
throw await getDbSyncError(err as Error, this, this.fyo);
}
await this._syncValues(data);
return this;

170
fyo/model/errorHelpers.ts Normal file
View File

@ -0,0 +1,170 @@
import { Fyo } from 'fyo';
import { DuplicateEntryError, NotFoundError } from 'fyo/utils/errors';
import {
DynamicLinkField,
Field,
FieldTypeEnum,
TargetField,
} from 'schemas/types';
import { Doc } from './doc';
type NotFoundDetails = { label: string; value: string };
export async function getDbSyncError(
err: Error,
doc: Doc,
fyo: Fyo
): Promise<Error> {
if (err.message.includes('UNIQUE constraint failed:')) {
return getDuplicateEntryError(err, doc);
}
if (err.message.includes('FOREIGN KEY constraint failed')) {
return getNotFoundError(err, doc, fyo);
}
return err;
}
function getDuplicateEntryError(
err: Error,
doc: Doc
): Error | DuplicateEntryError {
const matches = err.message.match(/UNIQUE constraint failed:\s(\w+)\.(\w+)$/);
if (!matches) {
return err;
}
const schemaName = matches[1];
const fieldname = matches[2];
if (!schemaName || !fieldname) {
return err;
}
const duplicateEntryError = new DuplicateEntryError(err.message, false);
const validDict = doc.getValidDict(false, true);
duplicateEntryError.stack = err.stack;
duplicateEntryError.more = {
schemaName,
fieldname,
value: validDict[fieldname],
};
return duplicateEntryError;
}
async function getNotFoundError(
err: Error,
doc: Doc,
fyo: Fyo
): Promise<NotFoundError> {
const notFoundError = new NotFoundError(fyo.t`Cannot perform operation.`);
notFoundError.stack = err.stack;
notFoundError.more.message = err.message;
const details = await getNotFoundDetails(doc, fyo);
if (!details) {
notFoundError.shouldStore = true;
return notFoundError;
}
notFoundError.shouldStore = false;
notFoundError.message = fyo.t`${details.label} value ${details.value} does not exist.`;
return notFoundError;
}
async function getNotFoundDetails(
doc: Doc,
fyo: Fyo
): Promise<NotFoundDetails | null> {
/**
* Since 'FOREIGN KEY constraint failed' doesn't inform
* how the operation failed, all Link and DynamicLink fields
* must be checked for value existance so as to provide a
* decent error message.
*/
for (const field of doc.schema.fields) {
const details = await getNotFoundDetailsIfDoesNotExists(field, doc, fyo);
if (details) {
return details;
}
}
return null;
}
async function getNotFoundDetailsIfDoesNotExists(
field: Field,
doc: Doc,
fyo: Fyo
): Promise<NotFoundDetails | null> {
const value = doc.get(field.fieldname);
if (field.fieldtype === FieldTypeEnum.Link && value) {
return getNotFoundLinkDetails(field as TargetField, value as string, fyo);
}
if (field.fieldtype === FieldTypeEnum.DynamicLink && value) {
return getNotFoundDynamicLinkDetails(
field as DynamicLinkField,
value as string,
fyo,
doc
);
}
if (
field.fieldtype === FieldTypeEnum.Table &&
(value as Doc[] | undefined)?.length
) {
return getNotFoundTableDetails(value as Doc[], fyo);
}
return null;
}
async function getNotFoundLinkDetails(
field: TargetField,
value: string,
fyo: Fyo
): Promise<NotFoundDetails | null> {
const { target } = field;
const exists = await fyo.db.exists(target as string, value);
if (!exists) {
return { label: field.label, value };
}
return null;
}
async function getNotFoundDynamicLinkDetails(
field: DynamicLinkField,
value: string,
fyo: Fyo,
doc: Doc
): Promise<NotFoundDetails | null> {
const { references } = field;
const target = doc.get(references);
if (!target) {
return null;
}
const exists = await fyo.db.exists(target as string, value);
if (!exists) {
return { label: field.label, value };
}
return null;
}
async function getNotFoundTableDetails(
value: Doc[],
fyo: Fyo
): Promise<NotFoundDetails | null> {
for (const childDoc of value) {
const details = getNotFoundDetails(childDoc, fyo);
if (details) {
return details;
}
}
return null;
}

View File

@ -1,7 +1,6 @@
import { Fyo } from 'fyo';
import { DocValue, DocValueMap } from 'fyo/core/types';
import { DocValue } from 'fyo/core/types';
import { isPesa } from 'fyo/utils';
import { DuplicateEntryError } from 'fyo/utils/errors';
import { isEqual } from 'lodash';
import { Money } from 'pesa';
import { Field, FieldType, FieldTypeEnum } from 'schemas/types';
@ -115,37 +114,3 @@ export function setChildDocIdx(childDocs: Doc[]) {
childDocs[idx].idx = +idx;
}
}
export function getInsertionError(err: Error, validDict: DocValueMap): Error {
if (err.message.includes('UNIQUE constraint failed:')) {
return getDuplicateEntryError(err, validDict);
}
return err;
}
export function getDuplicateEntryError(
err: Error,
validDict: DocValueMap
): Error | DuplicateEntryError {
const matches = err.message.match(/UNIQUE constraint failed:\s(\w+)\.(\w+)$/);
if (!matches) {
return err;
}
const schemaName = matches[1];
const fieldname = matches[2];
if (!schemaName || !fieldname) {
return err;
}
const duplicateEntryError = new DuplicateEntryError(err.message, false);
duplicateEntryError.stack = err.stack;
duplicateEntryError.more = {
schemaName,
fieldname,
value: validDict[fieldname],
};
return duplicateEntryError;
}

View File

@ -36,7 +36,7 @@ async function reportError(errorLogObj: ErrorLog) {
};
if (fyo.store.isDevelopment) {
console.log(body);
console.log('reportError', body);
}
await ipcRenderer.invoke(IPC_ACTIONS.SEND_ERROR, JSON.stringify(body));
@ -95,7 +95,10 @@ export async function handleErrorWithDialog(
await handleError(false, error, { errorMessage, doc });
const label = getErrorLabel(error);
const options: MessageDialogOptions = { message: label, detail: errorMessage };
const options: MessageDialogOptions = {
message: label,
detail: errorMessage,
};
if (reportError) {
options.detail = truncate(options.detail, { length: 128 });