Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: patch issues from error reports #457

Merged
merged 14 commits into from
Aug 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions fyo/core/converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export class Converter {
schemaName: string,
rawValueMap: RawValueMap | RawValueMap[]
): DocValueMap | DocValueMap[] {
rawValueMap ??= {};
if (Array.isArray(rawValueMap)) {
return rawValueMap.map((dv) => this.#toDocValueMap(schemaName, dv));
} else {
Expand All @@ -48,6 +49,7 @@ export class Converter {
schemaName: string,
docValueMap: DocValueMap | DocValueMap[]
): RawValueMap | RawValueMap[] {
docValueMap ??= {};
if (Array.isArray(docValueMap)) {
return docValueMap.map((dv) => this.#toRawValueMap(schemaName, dv));
} else {
Expand Down
14 changes: 12 additions & 2 deletions fyo/model/doc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import { getIsNullOrUndef, getMapFromList, getRandomString } from 'utils';
import { markRaw } from 'vue';
import { isPesa } from '../utils/index';
import { getDbSyncError } from './errorHelpers';
import {
areDocValuesEqual,
getMissingMandatoryMessage,
Expand Down Expand Up @@ -682,7 +683,12 @@ export class Doc extends Observable<DocValue | Doc[]> {
await this._preSync();

const validDict = this.getValidDict(false, true);
const data = await this.fyo.db.insert(this.schemaName, validDict);
let data: DocValueMap;
try {
data = await this.fyo.db.insert(this.schemaName, validDict);
} catch (err) {
throw await getDbSyncError(err as Error, this, this.fyo);
}
await this._syncValues(data);

this.fyo.telemetry.log(Verb.Created, this.schemaName);
Expand All @@ -695,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;
Expand Down
170 changes: 170 additions & 0 deletions fyo/model/errorHelpers.ts
Original file line number Diff line number Diff line change
@@ -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;
}
9 changes: 7 additions & 2 deletions fyo/utils/errors.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
export class BaseError extends Error {
more: Record<string, unknown> = {};
message: string;
statusCode: number;
shouldStore: boolean;

constructor(statusCode: number, message: string, shouldStore: boolean = true) {
constructor(
statusCode: number,
message: string,
shouldStore: boolean = true
) {
super(message);
this.name = 'BaseError';
this.statusCode = statusCode;
Expand Down Expand Up @@ -96,7 +101,7 @@ export function getDbError(err: Error) {
return CannotCommitError;
}

if (err.message.includes('SQLITE_CONSTRAINT: UNIQUE constraint failed:')) {
if (err.message.includes('UNIQUE constraint failed:')) {
return DuplicateEntryError;
}

Expand Down
9 changes: 8 additions & 1 deletion main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
app,
BrowserWindow,
BrowserWindowConstructorOptions,
protocol,
protocol
} from 'electron';
import Store from 'electron-store';
import { autoUpdater } from 'electron-updater';
Expand Down Expand Up @@ -41,6 +41,13 @@ export class Main {
autoUpdater.logger = console;
}

// https://github.com/electron-userland/electron-builder/issues/4987
app.commandLine.appendSwitch('disable-http2');
autoUpdater.requestHeaders = {
'Cache-Control':
'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0',
};

Store.initRenderer();

this.registerListeners();
Expand Down
34 changes: 32 additions & 2 deletions main/registerAutoUpdaterListeners.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { autoUpdater } from 'electron-updater';
import { app, dialog } from 'electron';
import { autoUpdater, UpdateInfo } from 'electron-updater';
import { Main } from '../main';
import { IPC_CHANNELS } from '../utils/messages';

export default function registerAutoUpdaterListeners(main: Main) {
autoUpdater.autoDownload = true;
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true;

autoUpdater.on('error', (error) => {
Expand All @@ -13,5 +14,34 @@ export default function registerAutoUpdaterListeners(main: Main) {
}

main.mainWindow!.webContents.send(IPC_CHANNELS.MAIN_PROCESS_ERROR, error);
dialog.showErrorBox(
'Update Error: ',
error == null ? 'unknown' : (error.stack || error).toString()
);
});

autoUpdater.on('update-available', async (info: UpdateInfo) => {
const currentVersion = app.getVersion();
const nextVersion = info.version;
const isCurrentBeta = currentVersion.includes('beta');
const isNextBeta = nextVersion.includes('beta');

let downloadUpdate = true;
if (!isCurrentBeta && isNextBeta) {
const option = await dialog.showMessageBox({
type: 'info',
title: `Update Frappe Books?`,
message: `Download version ${nextVersion}?`,
buttons: ['Yes', 'No'],
});

downloadUpdate = option.response === 0;
}

if (!downloadUpdate) {
return;
}

await autoUpdater.downloadUpdate();
});
}
36 changes: 1 addition & 35 deletions main/registerIpcMainActionListeners.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { app, dialog, ipcMain } from 'electron';
import { autoUpdater, UpdateInfo } from 'electron-updater';
import { autoUpdater } from 'electron-updater';
import fs from 'fs/promises';
import path from 'path';
import databaseManager from '../backend/database/manager';
Expand All @@ -15,40 +15,6 @@ import {
} from './helpers';
import { saveHtmlAsPdf } from './saveHtmlAsPdf';

autoUpdater.autoDownload = false;

autoUpdater.on('error', (error) => {
dialog.showErrorBox(
'Update Error: ',
error == null ? 'unknown' : (error.stack || error).toString()
);
});

autoUpdater.on('update-available', async (info: UpdateInfo) => {
const currentVersion = app.getVersion();
const nextVersion = info.version;
const isCurrentBeta = currentVersion.includes('beta');
const isNextBeta = nextVersion.includes('beta');

let downloadUpdate = true;
if (!isCurrentBeta && isNextBeta) {
const option = await dialog.showMessageBox({
type: 'info',
title: `Update Frappe Books?`,
message: `Download version ${nextVersion}?`,
buttons: ['Yes', 'No'],
});

downloadUpdate = option.response === 0;
}

if (!downloadUpdate) {
return;
}

await autoUpdater.downloadUpdate();
});

export default function registerIpcMainActionListeners(main: Main) {
ipcMain.handle(IPC_ACTIONS.GET_OPEN_FILEPATH, async (event, options) => {
return await dialog.showOpenDialog(main.mainWindow!, options);
Expand Down
2 changes: 1 addition & 1 deletion models/baseModels/Invoice/Invoice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export abstract class Invoice extends Transactional {
}

const tax = await this.getTax(item.tax!);
for (const { account, rate } of tax.details as TaxDetail[]) {
for (const { account, rate } of (tax.details ?? []) as TaxDetail[]) {
taxes[account] ??= {
account,
rate,
Expand Down
5 changes: 3 additions & 2 deletions models/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,9 @@ export async function getExchangeRate({
}
} catch (error) {
console.error(error);
throw new Error(
`Could not fetch exchange rate for ${fromCurrency} -> ${toCurrency}`
throw new NotFoundError(
`Could not fetch exchange rate for ${fromCurrency} -> ${toCurrency}`,
false
);
}
} else {
Expand Down
Loading