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:
parent
d90d113981
commit
702a09e6c0
@ -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
170
fyo/model/errorHelpers.ts
Normal 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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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 });
|
||||
|
Loading…
Reference in New Issue
Block a user