diff --git a/fyo/model/doc.ts b/fyo/model/doc.ts index 3e960a2d..8800f91f 100644 --- a/fyo/model/doc.ts +++ b/fyo/model/doc.ts @@ -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 { 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 { 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; diff --git a/fyo/model/errorHelpers.ts b/fyo/model/errorHelpers.ts new file mode 100644 index 00000000..9c148512 --- /dev/null +++ b/fyo/model/errorHelpers.ts @@ -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 { + 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 { + 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 { + /** + * 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 { + 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 { + 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 { + 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 { + for (const childDoc of value) { + const details = getNotFoundDetails(childDoc, fyo); + if (details) { + return details; + } + } + + return null; +} diff --git a/fyo/model/helpers.ts b/fyo/model/helpers.ts index 0a05d8d8..927a8c93 100644 --- a/fyo/model/helpers.ts +++ b/fyo/model/helpers.ts @@ -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; -} diff --git a/src/errorHandling.ts b/src/errorHandling.ts index eb7dd125..bb2a7357 100644 --- a/src/errorHandling.ts +++ b/src/errorHandling.ts @@ -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 });