2
0
mirror of https://github.com/frappe/books.git synced 2025-01-03 15:17:30 +00:00

Merge pull request #687 from frappe/simplify-code-for-db-improvements

feat: create local backups on migrate, use preload
This commit is contained in:
Alan 2023-07-12 22:57:18 -07:00 committed by GitHub
commit 8d8d9ee06e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 710 additions and 453 deletions

View File

@ -16,10 +16,14 @@ import {
import { DatabaseBase, GetAllOptions, QueryFilter } from '../../utils/db/types';
import { getDefaultMetaFieldValueMap, sqliteTypeMap, SYSTEM } from '../helpers';
import {
AlterConfig,
ColumnDiff,
FieldValueMap,
GetQueryBuilderOptions,
MigrationConfig,
NonExtantConfig,
SingleValue,
UpdateSinglesConfig,
} from './types';
/**
@ -97,21 +101,73 @@ export default class DatabaseCore extends DatabaseBase {
await this.knex!.destroy();
}
async migrate() {
for (const schemaName in this.schemaMap) {
const schema = this.schemaMap[schemaName] as Schema;
if (schema.isSingle) {
async migrate(config: MigrationConfig = {}) {
const { create, alter } = await this.#getCreateAlterList();
const hasSingleValueTable = !create.includes('SingleValue');
let singlesConfig: UpdateSinglesConfig = {
update: [],
updateNonExtant: [],
};
if (hasSingleValueTable) {
singlesConfig = await this.#getSinglesUpdateList();
}
const shouldMigrate = !!(
create.length ||
alter.length ||
singlesConfig.update.length ||
singlesConfig.updateNonExtant.length
);
if (!shouldMigrate) {
return;
}
await config.pre?.();
for (const schemaName of create) {
await this.#createTable(schemaName);
}
for (const config of alter) {
await this.#alterTable(config);
}
if (!hasSingleValueTable) {
singlesConfig = await this.#getSinglesUpdateList();
}
await this.#initializeSingles(singlesConfig);
await config.post?.();
}
async #getCreateAlterList() {
const create: string[] = [];
const alter: AlterConfig[] = [];
for (const [schemaName, schema] of Object.entries(this.schemaMap)) {
if (!schema || schema.isSingle) {
continue;
}
if (await this.#tableExists(schemaName)) {
await this.#alterTable(schemaName);
} else {
await this.#createTable(schemaName);
const exists = await this.#tableExists(schemaName);
if (!exists) {
create.push(schemaName);
continue;
}
const diff: ColumnDiff = await this.#getColumnDiff(schemaName);
const newForeignKeys: Field[] = await this.#getNewForeignKeys(schemaName);
if (diff.added.length || diff.removed.length || newForeignKeys.length) {
alter.push({
schemaName,
diff,
newForeignKeys,
});
}
}
await this.#initializeSingles();
return { create, alter };
}
async exists(schemaName: string, name?: string): Promise<boolean> {
@ -584,11 +640,7 @@ export default class DatabaseCore extends DatabaseBase {
}
}
async #alterTable(schemaName: string) {
// get columns
const diff: ColumnDiff = await this.#getColumnDiff(schemaName);
const newForeignKeys: Field[] = await this.#getNewForeignKeys(schemaName);
async #alterTable({ schemaName, diff, newForeignKeys }: AlterConfig) {
await this.knex!.schema.table(schemaName, (table) => {
if (!diff.added.length) {
return;
@ -631,15 +683,17 @@ export default class DatabaseCore extends DatabaseBase {
.select('fieldname')) as { fieldname: string }[]
).map(({ fieldname }) => fieldname);
return this.schemaMap[singleSchemaName]!.fields.map(
({ fieldname, default: value }) => ({
fieldname,
value: value,
})
).filter(
({ fieldname, value }) =>
!existingFields.includes(fieldname) && value !== undefined
);
const nonExtant: NonExtantConfig['nonExtant'] = [];
const fields = this.schemaMap[singleSchemaName]?.fields ?? [];
for (const { fieldname, default: value } of fields) {
if (existingFields.includes(fieldname) || value === undefined) {
continue;
}
nonExtant.push({ fieldname, value });
}
return nonExtant;
}
async #deleteOne(schemaName: string, name: string) {
@ -798,22 +852,42 @@ export default class DatabaseCore extends DatabaseBase {
return await this.knex!('SingleValue').insert(fieldValueMap);
}
async #initializeSingles() {
const singleSchemaNames = Object.keys(this.schemaMap).filter(
(n) => this.schemaMap[n]!.isSingle
);
for (const schemaName of singleSchemaNames) {
if (await this.#singleExists(schemaName)) {
await this.#updateNonExtantSingleValues(schemaName);
async #getSinglesUpdateList() {
const update: string[] = [];
const updateNonExtant: NonExtantConfig[] = [];
for (const [schemaName, schema] of Object.entries(this.schemaMap)) {
if (!schema || !schema.isSingle) {
continue;
}
const exists = await this.#singleExists(schemaName);
if (!exists && schema.fields.some((f) => f.default !== undefined)) {
update.push(schemaName);
}
if (!exists) {
continue;
}
const nonExtant = await this.#getNonExtantSingleValues(schemaName);
if (nonExtant.length) {
updateNonExtant.push({
schemaName,
nonExtant,
});
}
}
return { update, updateNonExtant };
}
async #initializeSingles({ update, updateNonExtant }: UpdateSinglesConfig) {
for (const config of updateNonExtant) {
await this.#updateNonExtantSingleValues(config);
}
for (const schemaName of update) {
const fields = this.schemaMap[schemaName]!.fields;
if (fields.every((f) => f.default === undefined)) {
continue;
}
const defaultValues: FieldValueMap = fields.reduce((acc, f) => {
if (f.default !== undefined) {
acc[f.fieldname] = f.default;
@ -826,10 +900,12 @@ export default class DatabaseCore extends DatabaseBase {
}
}
async #updateNonExtantSingleValues(schemaName: string) {
const singleValues = await this.#getNonExtantSingleValues(schemaName);
for (const sv of singleValues) {
await this.#updateSingleValue(schemaName, sv.fieldname, sv.value!);
async #updateNonExtantSingleValues({
schemaName,
nonExtant,
}: NonExtantConfig) {
for (const { fieldname, value } of nonExtant) {
await this.#updateSingleValue(schemaName, fieldname, value);
}
}

View File

@ -1,9 +1,12 @@
import fs from 'fs/promises';
import BetterSQLite3 from 'better-sqlite3';
import fs from 'fs-extra';
import { DatabaseError } from 'fyo/utils/errors';
import path from 'path';
import { DatabaseDemuxBase, DatabaseMethod } from 'utils/db/types';
import { getMapFromList } from 'utils/index';
import { Version } from 'utils/version';
import { getSchemas } from '../../schemas';
import { checkFileAccess, databaseMethodSet, unlinkIfExists } from '../helpers';
import { databaseMethodSet, unlinkIfExists } from '../helpers';
import patches from '../patches';
import { BespokeQueries } from './bespoke';
import DatabaseCore from './core';
@ -55,66 +58,70 @@ export class DatabaseManager extends DatabaseDemuxBase {
await this.db!.migrate();
}
/**
* This needs to be supplimented with transactions
* TODO: Add transactions in core.ts
*/
const dbPath = this.db!.dbPath;
const copyPath = await this.#makeTempCopy();
try {
await this.#runPatchesAndMigrate();
} catch (error) {
await this.#handleFailedMigration(error, dbPath, copyPath);
} finally {
await unlinkIfExists(copyPath);
}
await this.#executeMigration();
}
async #handleFailedMigration(
error: unknown,
dbPath: string,
copyPath: string | null
) {
await this.db!.close();
async #executeMigration() {
const version = await this.#getAppVersion();
const patches = await this.#getPatchesToExecute(version);
if (copyPath && (await checkFileAccess(copyPath))) {
await fs.copyFile(copyPath, dbPath);
const hasPatches = !!patches.pre.length || !!patches.post.length;
if (hasPatches) {
await this.#createBackup();
}
if (error instanceof Error) {
error.message = `failed migration\n${error.message}`;
await runPatches(patches.pre, this, version);
await this.db!.migrate({
pre: async () => {
if (hasPatches) {
return;
}
throw error;
await this.#createBackup();
},
});
await runPatches(patches.post, this, version);
}
async #runPatchesAndMigrate() {
const patchesToExecute = await this.#getPatchesToExecute();
patchesToExecute.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
const preMigrationPatches = patchesToExecute.filter(
(p) => p.patch.beforeMigrate
);
const postMigrationPatches = patchesToExecute.filter(
(p) => !p.patch.beforeMigrate
);
await runPatches(preMigrationPatches, this);
await this.db!.migrate();
await runPatches(postMigrationPatches, this);
}
async #getPatchesToExecute(): Promise<Patch[]> {
async #getPatchesToExecute(
version: string
): Promise<{ pre: Patch[]; post: Patch[] }> {
if (this.db === undefined) {
return [];
return { pre: [], post: [] };
}
const query: { name: string }[] = await this.db.knex!('PatchRun').select(
'name'
);
const executedPatches = query.map((q) => q.name);
return patches.filter((p) => !executedPatches.includes(p.name));
const query = (await this.db.knex!('PatchRun').select()) as {
name: string;
version?: string;
failed?: boolean;
}[];
const runPatchesMap = getMapFromList(query, 'name');
/**
* A patch is run only if:
* - it hasn't run and was added in a future version
* i.e. app version is before patch added version
* - it ran but failed in some other version (i.e fixed)
*/
const filtered = patches
.filter((p) => {
const exec = runPatchesMap[p.name];
if (!exec && Version.lte(version, p.version)) {
return true;
}
if (exec?.failed && exec?.version !== version) {
return true;
}
return false;
})
.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
return {
pre: filtered.filter((p) => p.patch.beforeMigrate),
post: filtered.filter((p) => !p.patch.beforeMigrate),
};
}
async call(method: DatabaseMethod, ...args: unknown[]) {
@ -150,32 +157,72 @@ export class DatabaseManager extends DatabaseDemuxBase {
}
async #getIsFirstRun(): Promise<boolean> {
if (!this.#isInitialized) {
const knex = this.db?.knex;
if (!knex) {
return true;
}
const tableList: unknown[] = await this.db!.knex!.raw(
"SELECT name FROM sqlite_master WHERE type='table'"
);
return tableList.length === 0;
const query = await knex('sqlite_master').where({
type: 'table',
name: 'PatchRun',
});
return !query.length;
}
async #makeTempCopy() {
const src = this.db!.dbPath;
if (src === ':memory:') {
async #createBackup() {
const { dbPath } = this.db ?? {};
if (!dbPath || process.env.IS_TEST) {
return;
}
const backupPath = await this.#getBackupFilePath();
if (!backupPath) {
return;
}
const db = this.getDriver();
await db?.backup(backupPath).then(() => db.close());
}
async #getBackupFilePath() {
const { dbPath } = this.db ?? {};
if (dbPath === ':memory:' || !dbPath) {
return null;
}
const dir = path.parse(src).dir;
const dest = path.join(dir, '__premigratory_temp.db');
let fileName = path.parse(dbPath).name;
if (fileName.endsWith('.books')) {
fileName = fileName.slice(0, -6);
}
try {
await fs.copyFile(src, dest);
} catch (err) {
const backupFolder = path.join(path.dirname(dbPath), 'backups');
const date = new Date().toISOString().split('T')[0];
const version = await this.#getAppVersion();
const backupFile = `${fileName}-${version}-${date}.books.db`;
fs.ensureDirSync(backupFolder);
return path.join(backupFolder, backupFile);
}
async #getAppVersion(): Promise<string> {
const knex = this.db?.knex;
if (!knex) {
return '0.0.0';
}
const query = await knex('SingleValue')
.select('value')
.where({ fieldname: 'version', parent: 'SystemSettings' });
const value = (query[0] as undefined | { value: string })?.value;
return value || '0.0.0';
}
getDriver() {
const { dbPath } = this.db ?? {};
if (!dbPath) {
return null;
}
return dest;
return BetterSQLite3(dbPath, { readonly: true });
}
}

View File

@ -2,34 +2,50 @@ import { emitMainProcessError, getDefaultMetaFieldValueMap } from '../helpers';
import { DatabaseManager } from './manager';
import { FieldValueMap, Patch } from './types';
export async function runPatches(patches: Patch[], dm: DatabaseManager) {
export async function runPatches(
patches: Patch[],
dm: DatabaseManager,
version: string
) {
const list: { name: string; success: boolean }[] = [];
for (const patch of patches) {
const success = await runPatch(patch, dm);
const success = await runPatch(patch, dm, version);
list.push({ name: patch.name, success });
}
return list;
}
async function runPatch(patch: Patch, dm: DatabaseManager): Promise<boolean> {
async function runPatch(
patch: Patch,
dm: DatabaseManager,
version: string
): Promise<boolean> {
let failed = false;
try {
await patch.patch.execute(dm);
} catch (error) {
if (!(error instanceof Error)) {
return false;
}
failed = true;
if (error instanceof Error) {
error.message = `Patch Failed: ${patch.name}\n${error.message}`;
emitMainProcessError(error, { patchName: patch.name, notifyUser: false });
return false;
}
}
await makeEntry(patch.name, dm);
await makeEntry(patch.name, version, failed, dm);
return true;
}
async function makeEntry(patchName: string, dm: DatabaseManager) {
async function makeEntry(
patchName: string,
version: string,
failed: boolean,
dm: DatabaseManager
) {
const defaultFieldValueMap = getDefaultMetaFieldValueMap() as FieldValueMap;
defaultFieldValueMap.name = patchName;
defaultFieldValueMap.failed = failed;
defaultFieldValueMap.version = version;
await dm.db!.insert('PatchRun', defaultFieldValueMap);
}

View File

@ -16,6 +16,30 @@ export type FieldValueMap = Record<
RawValue | undefined | FieldValueMap[]
>;
export type AlterConfig = {
schemaName: string;
diff: ColumnDiff;
newForeignKeys: Field[];
};
export type NonExtantConfig = {
schemaName: string;
nonExtant: {
fieldname: string;
value: RawValue;
}[];
};
export type UpdateSinglesConfig = {
update: string[];
updateNonExtant: NonExtantConfig[];
};
export type MigrationConfig = {
pre?: () => Promise<void> | void;
post?: () => Promise<void> | void;
};
export interface Patch {
name: string;
version: string;
@ -50,6 +74,7 @@ export type BespokeFunction = (
db: DatabaseCore,
...args: unknown[]
) => Promise<unknown>;
export type SingleValue<T> = {
fieldname: string;
parent: string;

View File

@ -51,7 +51,7 @@ function updatePaths() {
async function buildMainProcessSource() {
const result = await esbuild.build({
...commonConfig,
outfile: path.join(buildDirPath, mainFileName),
outdir: path.join(buildDirPath),
});
if (result.errors.length) {

View File

@ -42,7 +42,7 @@ const viteProcess = $$`yarn vite`;
*/
const ctx = await esbuild.context({
...getMainProcessCommonConfig(root),
outfile: path.join(root, 'dist_electron', 'dev', 'main.js'),
outdir: path.join(root, 'dist_electron', 'dev'),
});
/**

View File

@ -10,7 +10,10 @@ import path from 'path';
*/
export function getMainProcessCommonConfig(root) {
return {
entryPoints: [path.join(root, 'main.ts')],
entryPoints: [
path.join(root, 'main.ts'),
path.join(root, 'main', 'preload.ts'),
],
bundle: true,
sourcemap: true,
sourcesContent: false,

View File

@ -1,7 +1,5 @@
import { AuthDemuxBase } from 'utils/auth/types';
import { IPC_ACTIONS } from 'utils/messages';
import { Creds } from 'utils/types';
const { ipcRenderer } = require('electron');
export class AuthDemux extends AuthDemuxBase {
#isElectron = false;
@ -12,7 +10,7 @@ export class AuthDemux extends AuthDemuxBase {
async getCreds(): Promise<Creds> {
if (this.#isElectron) {
return (await ipcRenderer.invoke(IPC_ACTIONS.GET_CREDS)) as Creds;
return await ipc.getCreds();
} else {
return { errorLogUrl: '', tokenString: '', telemetryUrl: '' };
}

View File

@ -1,26 +1,12 @@
import type Store from 'electron-store';
import { ConfigMap } from 'fyo/core/types';
import type { IPC } from 'main/preload';
export class Config {
config: Map<string, unknown> | Store;
config: Map<string, unknown> | IPC['store'];
constructor(isElectron: boolean) {
this.config = new Map();
if (isElectron) {
const Config = require('electron-store') as typeof Store;
this.config = new Config();
}
}
get store() {
if (this.config instanceof Map) {
const store: Record<string, unknown> = {};
for (const key of this.config.keys()) {
store[key] = this.config.get(key);
}
return store;
} else {
return this.config;
this.config = ipc.store;
}
}
@ -39,8 +25,4 @@ export class Config {
delete(key: keyof ConfigMap) {
this.config.delete(key);
}
clear() {
this.config.clear();
}
}

View File

@ -1,9 +1,7 @@
const { ipcRenderer } = require('electron');
import { DatabaseError, NotImplemented } from 'fyo/utils/errors';
import { SchemaMap } from 'schemas/types';
import { DatabaseDemuxBase, DatabaseMethod } from 'utils/db/types';
import { BackendResponse } from 'utils/ipc/types';
import { IPC_ACTIONS } from 'utils/messages';
export class DatabaseDemux extends DatabaseDemuxBase {
#isElectron = false;
@ -32,9 +30,7 @@ export class DatabaseDemux extends DatabaseDemuxBase {
}
return (await this.#handleDBCall(async () => {
return (await ipcRenderer.invoke(
IPC_ACTIONS.DB_SCHEMA
)) as BackendResponse;
return await ipc.db.getSchema();
})) as SchemaMap;
}
@ -47,11 +43,7 @@ export class DatabaseDemux extends DatabaseDemuxBase {
}
return (await this.#handleDBCall(async () => {
return (await ipcRenderer.invoke(
IPC_ACTIONS.DB_CREATE,
dbPath,
countryCode
)) as BackendResponse;
return ipc.db.create(dbPath, countryCode);
})) as string;
}
@ -64,11 +56,7 @@ export class DatabaseDemux extends DatabaseDemuxBase {
}
return (await this.#handleDBCall(async () => {
return (await ipcRenderer.invoke(
IPC_ACTIONS.DB_CONNECT,
dbPath,
countryCode
)) as BackendResponse;
return ipc.db.connect(dbPath, countryCode);
})) as string;
}
@ -78,11 +66,7 @@ export class DatabaseDemux extends DatabaseDemuxBase {
}
return await this.#handleDBCall(async () => {
return (await ipcRenderer.invoke(
IPC_ACTIONS.DB_CALL,
method,
...args
)) as BackendResponse;
return await ipc.db.call(method, ...args);
});
}
@ -92,11 +76,7 @@ export class DatabaseDemux extends DatabaseDemuxBase {
}
return await this.#handleDBCall(async () => {
return (await ipcRenderer.invoke(
IPC_ACTIONS.DB_BESPOKE,
method,
...args
)) as BackendResponse;
return await ipc.db.bespoke(method, ...args);
});
}
}

View File

@ -96,7 +96,7 @@ export class Fyo {
setIsElectron() {
try {
this.isElectron = Boolean(require('electron'));
this.isElectron = !!window?.ipc;
} catch {
this.isElectron = false;
}

12
main.ts
View File

@ -4,6 +4,7 @@ require('source-map-support').install({
environment: 'node',
});
import { emitMainProcessError } from 'backend/helpers';
import {
app,
BrowserWindow,
@ -12,7 +13,6 @@ import {
ProtocolRequest,
ProtocolResponse,
} from 'electron';
import Store from 'electron-store';
import { autoUpdater } from 'electron-updater';
import fs from 'fs';
import path from 'path';
@ -21,7 +21,6 @@ import registerAutoUpdaterListeners from './main/registerAutoUpdaterListeners';
import registerIpcMainActionListeners from './main/registerIpcMainActionListeners';
import registerIpcMainMessageListeners from './main/registerIpcMainMessageListeners';
import registerProcessListeners from './main/registerProcessListeners';
import { emitMainProcessError } from 'backend/helpers';
export class Main {
title = 'Frappe Books';
@ -54,8 +53,6 @@ export class Main {
'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0',
};
Store.initRenderer();
this.registerListeners();
if (this.isMac && this.isDevelopment) {
app.dock.setIcon(this.icon);
@ -87,6 +84,7 @@ export class Main {
}
getOptions(): BrowserWindowConstructorOptions {
const preload = path.join(__dirname, 'main', 'preload.js');
const options: BrowserWindowConstructorOptions = {
width: this.WIDTH,
height: this.HEIGHT,
@ -94,8 +92,10 @@ export class Main {
titleBarStyle: 'hidden',
trafficLightPosition: { x: 16, y: 16 },
webPreferences: {
contextIsolation: false, // TODO: Switch this off
nodeIntegration: true,
contextIsolation: true,
nodeIntegration: false,
sandbox: false,
preload,
},
autoHideMenuBar: true,
frame: !this.isMac,

211
main/preload.ts Normal file
View File

@ -0,0 +1,211 @@
import type {
OpenDialogOptions,
OpenDialogReturnValue,
SaveDialogOptions,
SaveDialogReturnValue,
} from 'electron';
import { contextBridge, ipcRenderer } from 'electron';
import type { ConfigMap } from 'fyo/core/types';
import config from 'utils/config';
import type { DatabaseMethod } from 'utils/db/types';
import type { BackendResponse } from 'utils/ipc/types';
import { IPC_ACTIONS, IPC_CHANNELS, IPC_MESSAGES } from 'utils/messages';
import type {
ConfigFilesWithModified,
Creds,
LanguageMap,
SelectFileOptions,
SelectFileReturn,
TemplateFile,
} from 'utils/types';
type IPCRendererListener = Parameters<typeof ipcRenderer.on>[1];
const ipc = {
desktop: true,
reloadWindow() {
return ipcRenderer.send(IPC_MESSAGES.RELOAD_MAIN_WINDOW);
},
async getCreds() {
return (await ipcRenderer.invoke(IPC_ACTIONS.GET_CREDS)) as Creds;
},
async getLanguageMap(code: string) {
return (await ipcRenderer.invoke(IPC_ACTIONS.GET_LANGUAGE_MAP, code)) as {
languageMap: LanguageMap;
success: boolean;
message: string;
};
},
async getTemplates(): Promise<TemplateFile[]> {
return (await ipcRenderer.invoke(
IPC_ACTIONS.GET_TEMPLATES
)) as TemplateFile[];
},
async selectFile(options: SelectFileOptions): Promise<SelectFileReturn> {
return (await ipcRenderer.invoke(
IPC_ACTIONS.SELECT_FILE,
options
)) as SelectFileReturn;
},
async getSaveFilePath(options: SaveDialogOptions) {
return (await ipcRenderer.invoke(
IPC_ACTIONS.GET_SAVE_FILEPATH,
options
)) as SaveDialogReturnValue;
},
async getOpenFilePath(options: OpenDialogOptions) {
return (await ipcRenderer.invoke(
IPC_ACTIONS.GET_OPEN_FILEPATH,
options
)) as OpenDialogReturnValue;
},
async checkDbAccess(filePath: string) {
return (await ipcRenderer.invoke(
IPC_ACTIONS.CHECK_DB_ACCESS,
filePath
)) as boolean;
},
async checkForUpdates() {
await ipcRenderer.invoke(IPC_ACTIONS.CHECK_FOR_UPDATES);
},
openLink(link: string) {
ipcRenderer.send(IPC_MESSAGES.OPEN_EXTERNAL, link);
},
async deleteFile(filePath: string) {
return (await ipcRenderer.invoke(
IPC_ACTIONS.DELETE_FILE,
filePath
)) as BackendResponse;
},
async saveData(data: string, savePath: string) {
await ipcRenderer.invoke(IPC_ACTIONS.SAVE_DATA, data, savePath);
},
showItemInFolder(filePath: string) {
ipcRenderer.send(IPC_MESSAGES.SHOW_ITEM_IN_FOLDER, filePath);
},
async makePDF(
html: string,
savePath: string,
width: number,
height: number
): Promise<boolean> {
return (await ipcRenderer.invoke(
IPC_ACTIONS.SAVE_HTML_AS_PDF,
html,
savePath,
width,
height
)) as boolean;
},
async getDbList() {
return (await ipcRenderer.invoke(
IPC_ACTIONS.GET_DB_LIST
)) as ConfigFilesWithModified[];
},
async getDbDefaultPath(companyName: string) {
return (await ipcRenderer.invoke(
IPC_ACTIONS.GET_DB_DEFAULT_PATH,
companyName
)) as string;
},
async getEnv() {
return (await ipcRenderer.invoke(IPC_ACTIONS.GET_ENV)) as {
isDevelopment: boolean;
platform: string;
version: string;
};
},
openExternalUrl(url: string) {
ipcRenderer.send(IPC_MESSAGES.OPEN_EXTERNAL, url);
},
async showError(title: string, content: string) {
await ipcRenderer.invoke(IPC_ACTIONS.SHOW_ERROR, { title, content });
},
async sendError(body: string) {
await ipcRenderer.invoke(IPC_ACTIONS.SEND_ERROR, body);
},
registerMainProcessErrorListener(listener: IPCRendererListener) {
ipcRenderer.on(IPC_CHANNELS.LOG_MAIN_PROCESS_ERROR, listener);
},
registerConsoleLogListener(listener: IPCRendererListener) {
ipcRenderer.on(IPC_CHANNELS.CONSOLE_LOG, listener);
},
db: {
async getSchema() {
return (await ipcRenderer.invoke(
IPC_ACTIONS.DB_SCHEMA
)) as BackendResponse;
},
async create(dbPath: string, countryCode?: string) {
return (await ipcRenderer.invoke(
IPC_ACTIONS.DB_CREATE,
dbPath,
countryCode
)) as BackendResponse;
},
async connect(dbPath: string, countryCode?: string) {
return (await ipcRenderer.invoke(
IPC_ACTIONS.DB_CONNECT,
dbPath,
countryCode
)) as BackendResponse;
},
async call(method: DatabaseMethod, ...args: unknown[]) {
return (await ipcRenderer.invoke(
IPC_ACTIONS.DB_CALL,
method,
...args
)) as BackendResponse;
},
async bespoke(method: string, ...args: unknown[]) {
return (await ipcRenderer.invoke(
IPC_ACTIONS.DB_BESPOKE,
method,
...args
)) as BackendResponse;
},
},
store: {
get<K extends keyof ConfigMap>(key: K) {
return config.get(key);
},
set<K extends keyof ConfigMap>(key: K, value: ConfigMap[K]) {
return config.set(key, value);
},
delete(key: keyof ConfigMap) {
return config.delete(key);
},
},
} as const;
contextBridge.exposeInMainWorld('ipc', ipc);
export type IPC = typeof ipc;

View File

@ -8,7 +8,7 @@ import {
} from 'electron';
import { autoUpdater } from 'electron-updater';
import { constants } from 'fs';
import fs from 'fs/promises';
import fs from 'fs-extra';
import path from 'path';
import { SelectFileOptions, SelectFileReturn } from 'utils/types';
import databaseManager from '../backend/database/manager';
@ -38,6 +38,22 @@ export default function registerIpcMainActionListeners(main: Main) {
return true;
});
ipcMain.handle(
IPC_ACTIONS.GET_DB_DEFAULT_PATH,
async (_, companyName: string) => {
let root = app.getPath('documents');
if (main.isDevelopment) {
root = 'dbs';
}
const dbsPath = path.join(root, 'Frappe Books');
const backupPath = path.join(dbsPath, 'backups');
await fs.ensureDir(backupPath);
return path.join(dbsPath, `${companyName}.books.db`);
}
);
ipcMain.handle(
IPC_ACTIONS.GET_OPEN_FILEPATH,
async (_, options: OpenDialogOptions) => {
@ -169,11 +185,17 @@ export default function registerIpcMainActionListeners(main: Main) {
return await getConfigFilesWithModified(files);
});
ipcMain.handle(IPC_ACTIONS.GET_ENV, () => {
ipcMain.handle(IPC_ACTIONS.GET_ENV, async () => {
let version = app.getVersion();
if (main.isDevelopment) {
const packageJson = await fs.readFile('package.json', 'utf-8');
version = (JSON.parse(packageJson) as { version: string }).version;
}
return {
isDevelopment: main.isDevelopment,
platform: process.platform,
version: app.getVersion(),
version,
};
});

View File

@ -43,6 +43,7 @@
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0",
"@types/assert": "^1.5.6",
"@types/better-sqlite3": "^7.6.4",
"@types/electron-devtools-installer": "^2.2.0",
"@types/lodash": "^4.14.179",
"@types/luxon": "^2.3.1",

View File

@ -5,11 +5,11 @@ import { ModelNameEnum } from 'models/types';
import { codeStateMap } from 'regional/in';
import { ExportExtention } from 'reports/types';
import { showDialog } from 'src/utils/interactive';
import { getSavePath } from 'src/utils/ipcCalls';
import { invertMap } from 'utils';
import { getCsvData, saveExportData } from '../commonExporter';
import { BaseGSTR } from './BaseGSTR';
import { TransferTypeEnum } from './types';
import { getSavePath } from 'src/utils/ui';
const GST = {
'GST-0': 0,

View File

@ -1,7 +1,7 @@
import { t } from 'fyo';
import { Action } from 'fyo/model/types';
import { Verb } from 'fyo/telemetry/types';
import { getSavePath, saveData, showExportInFolder } from 'src/utils/ipcCalls';
import { getSavePath, showExportInFolder } from 'src/utils/ui';
import { getIsNullOrUndef } from 'utils';
import { generateCSV } from 'utils/csvParser';
import { Report } from './Report';
@ -183,7 +183,7 @@ export async function saveExportData(
filePath: string,
message?: string
) {
await saveData(data, filePath);
await ipc.saveData(data, filePath);
message ??= t`Export Successful`;
showExportInFolder(message, filePath);
}

View File

@ -8,6 +8,18 @@
"fieldtype": "Data",
"label": "Name",
"required": true
},
{
"fieldname": "failed",
"fieldtype": "Check",
"label": "Failed",
"default": false
},
{
"fieldname": "version",
"fieldtype": "Data",
"label": "Version",
"default": "0.0.0"
}
]
}

View File

@ -60,6 +60,7 @@
"fieldname": "version",
"label": "Version",
"fieldtype": "Data",
"default": "0.0.0",
"readOnly": true,
"section": "Default"
},

View File

@ -5,4 +5,5 @@ if [ $# -eq 0 ]
TEST_PATH=./**/tests/**/*.spec.ts
fi
export IS_TEST=true
./scripts/runner.sh ./node_modules/.bin/tape $TEST_PATH | ./node_modules/.bin/tap-spec

View File

@ -19,6 +19,7 @@
<DatabaseSelector
v-if="activeScreen === 'DatabaseSelector'"
ref="databaseSelector"
@new-database="newDatabase"
@file-selected="fileSelected"
/>
<SetupWizard
@ -53,7 +54,7 @@ import { connectToDatabase, dbErrorActionSymbols } from './utils/db';
import { initializeInstance } from './utils/initialization';
import * as injectionKeys from './utils/injectionKeys';
import { showDialog } from './utils/interactive';
import { checkDbAccess, checkForUpdates } from './utils/ipcCalls';
import { setLanguageMap } from './utils/language';
import { updateConfigFiles } from './utils/misc';
import { updatePrintTemplates } from './utils/printTemplates';
import { Search } from './utils/search';
@ -136,7 +137,7 @@ export default defineComponent({
return;
}
await this.fileSelected(lastSelectedFilePath, false);
await this.fileSelected(lastSelectedFilePath);
},
async setSearcher(): Promise<void> {
this.searcher = new Search(fyo);
@ -146,7 +147,8 @@ export default defineComponent({
this.activeScreen = Screen.Desk;
await this.setDeskRoute();
await fyo.telemetry.start(true);
await checkForUpdates();
await ipc.checkForUpdates();
await setLanguageMap();
this.dbPath = filePath;
this.companyName = (await fyo.getValue(
ModelNameEnum.AccountingSettings,
@ -155,14 +157,12 @@ export default defineComponent({
await this.setSearcher();
updateConfigFiles(fyo);
},
async fileSelected(filePath: string, isNew?: boolean): Promise<void> {
fyo.config.set('lastSelectedFilePath', filePath);
if (isNew) {
newDatabase() {
this.activeScreen = Screen.SetupWizard;
return;
}
if (filePath !== ':memory:' && !(await checkDbAccess(filePath))) {
},
async fileSelected(filePath: string): Promise<void> {
fyo.config.set('lastSelectedFilePath', filePath);
if (filePath !== ':memory:' && !(await ipc.checkDbAccess(filePath))) {
await showDialog({
title: this.t`Cannot open file`,
type: 'error',
@ -182,12 +182,10 @@ export default defineComponent({
}
},
async setupComplete(setupWizardOptions: SetupWizardOptions): Promise<void> {
const filePath = fyo.config.get('lastSelectedFilePath');
if (typeof filePath !== 'string') {
return;
}
const companyName = setupWizardOptions.companyName;
const filePath = await ipc.getDbDefaultPath(companyName);
await setupInstance(filePath, setupWizardOptions, fyo);
fyo.config.set('lastSelectedFilePath', filePath);
await this.setDesk(filePath);
},
async showSetupWizardOrDesk(filePath: string): Promise<void> {

View File

@ -57,7 +57,6 @@
<script lang="ts">
import { Field } from 'schemas/types';
import { fyo } from 'src/initFyo';
import { selectFile } from 'src/utils/ipcCalls';
import { getDataURL } from 'src/utils/misc';
import { defineComponent, PropType } from 'vue';
import FeatherIcon from '../FeatherIcon.vue';
@ -105,7 +104,7 @@ export default defineComponent({
],
};
const { name, success, data } = await selectFile(options);
const { name, success, data } = await ipc.selectFile(options);
if (!success) {
return;

View File

@ -75,8 +75,8 @@ export default {
},
async openNewDoc() {
const schemaName = this.getTargetSchemaName();
if(!schemaName){
return
if (!schemaName) {
return;
}
const name =
this.linkValue || fyo.doc.getTemporaryName(fyo.schemaMap[schemaName]);

View File

@ -87,6 +87,7 @@
</template>
<script lang="ts">
import { t } from 'fyo';
import { Verb } from 'fyo/telemetry/types';
import { Field, FieldTypeEnum } from 'schemas/types';
import { fyo } from 'src/initFyo';
import {
@ -95,16 +96,15 @@ import {
getExportTableFields,
getJsonExportData,
} from 'src/utils/export';
import { getSavePath, saveData, showExportInFolder } from 'src/utils/ipcCalls';
import { ExportField, ExportFormat, ExportTableField } from 'src/utils/types';
import { getSavePath, showExportInFolder } from 'src/utils/ui';
import { QueryFilter } from 'utils/db/types';
import { defineComponent, PropType } from 'vue';
import { PropType, defineComponent } from 'vue';
import Button from './Button.vue';
import Check from './Controls/Check.vue';
import Int from './Controls/Int.vue';
import Select from './Controls/Select.vue';
import FormHeader from './FormHeader.vue';
import { Verb } from 'fyo/telemetry/types';
interface ExportWizardData {
useListFilters: boolean;
@ -257,7 +257,7 @@ export default defineComponent({
return;
}
await saveData(data, filePath);
await ipc.saveData(data, filePath);
this.fyo.telemetry.log(Verb.Exported, this.schemaName, {
extension: this.exportFormat,
});

View File

@ -9,7 +9,6 @@
</button>
</template>
<script>
import { openLink } from 'src/utils/ipcCalls';
import FeatherIcon from './FeatherIcon.vue';
export default {
@ -23,7 +22,7 @@ export default {
},
methods: {
openHelpLink() {
openLink(this.link);
ipc.openLink(this.link);
},
},
};

View File

@ -197,13 +197,12 @@
import { fyo } from 'src/initFyo';
import { getBgTextColorClass } from 'src/utils/colors';
import { searcherKey, shortcutsKey } from 'src/utils/injectionKeys';
import { openLink } from 'src/utils/ipcCalls';
import { docsPathMap } from 'src/utils/misc';
import {
getGroupLabelMap,
SearchGroup,
searchGroups,
SearchItems,
getGroupLabelMap,
searchGroups,
} from 'src/utils/search';
import { defineComponent, inject, nextTick } from 'vue';
import Button from './Button.vue';
@ -305,7 +304,7 @@ export default defineComponent({
},
methods: {
openDocs() {
openLink('https://docs.frappebooks.com/' + docsPathMap.Search);
ipc.openLink('https://docs.frappebooks.com/' + docsPathMap.Search);
},
getShortcuts() {
const ifOpen = (cb: Function) => () => this.openModal && cb();

View File

@ -182,7 +182,6 @@
import { reportIssue } from 'src/errorHandling';
import { fyo } from 'src/initFyo';
import { languageDirectionKey, shortcutsKey } from 'src/utils/injectionKeys';
import { openLink } from 'src/utils/ipcCalls';
import { docsPathRef } from 'src/utils/refs';
import { getSidebarConfig } from 'src/utils/sidebarConfig';
import { SidebarConfig, SidebarItem, SidebarRoot } from 'src/utils/types';
@ -255,7 +254,7 @@ export default defineComponent({
reportIssue,
toggleSidebar,
openDocumentation() {
openLink('https://docs.frappebooks.com/' + docsPathRef.value);
ipc.openLink('https://docs.frappebooks.com/' + docsPathRef.value);
},
setActiveGroup() {
const { fullPath } = this.$router.currentRoute.value;

View File

@ -1,15 +1,13 @@
import { t } from 'fyo';
import { Doc } from 'fyo/model/doc';
import type { Doc } from 'fyo/model/doc';
import { BaseError } from 'fyo/utils/errors';
import { ErrorLog } from 'fyo/utils/types';
import { truncate } from 'lodash';
import { showDialog } from 'src/utils/interactive';
import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages';
import { fyo } from './initFyo';
import router from './router';
import { getErrorMessage, stringifyCircular } from './utils';
import { DialogOptions, ToastOptions } from './utils/types';
const { ipcRenderer } = require('electron');
import type { DialogOptions, ToastOptions } from './utils/types';
function shouldNotStore(error: Error) {
const shouldLog = (error as BaseError).shouldStore ?? true;
@ -43,7 +41,7 @@ export async function sendError(errorLogObj: ErrorLog) {
console.log('sendError', body);
}
await ipcRenderer.invoke(IPC_ACTIONS.SEND_ERROR, JSON.stringify(body));
await ipc.sendError(JSON.stringify(body));
}
function getToastProps(errorLogObj: ErrorLog) {
@ -119,7 +117,7 @@ export async function handleErrorWithDialog(
};
if (reportError) {
options.detail = truncate(options.detail, { length: 128 });
options.detail = truncate(String(options.detail), { length: 128 });
options.buttons = [
{
label: t`Report`,
@ -154,8 +152,7 @@ export async function showErrorDialog(title?: string, content?: string) {
// To be used for show stopper errors
title ??= t`Error`;
content ??= t`Something has gone terribly wrong. Please check the console and raise an issue.`;
await ipcRenderer.invoke(IPC_ACTIONS.SHOW_ERROR, { title, content });
await ipc.showError(title, content);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -232,7 +229,7 @@ function getIssueUrlQuery(errorLogObj?: ErrorLog): string {
export function reportIssue(errorLogObj?: ErrorLog) {
const urlQuery = getIssueUrlQuery(errorLogObj);
ipcRenderer.send(IPC_MESSAGES.OPEN_EXTERNAL, urlQuery);
ipc.openExternalUrl(urlQuery);
}
function getErrorLabel(error: Error) {

View File

@ -246,12 +246,10 @@ import Loading from 'src/components/Loading.vue';
import Modal from 'src/components/Modal.vue';
import { fyo } from 'src/initFyo';
import { showDialog } from 'src/utils/interactive';
import { deleteDb, getSavePath, getSelectedFilePath } from 'src/utils/ipcCalls';
import { updateConfigFiles } from 'src/utils/misc';
import { IPC_ACTIONS } from 'utils/messages';
import { deleteDb, getSavePath, getSelectedFilePath } from 'src/utils/ui';
import type { ConfigFilesWithModified } from 'utils/types';
import { defineComponent } from 'vue';
const { ipcRenderer } = require('electron');
export default defineComponent({
name: 'DatabaseSelector',
@ -262,7 +260,7 @@ export default defineComponent({
Modal,
Button,
},
emits: ['file-selected'],
emits: ['file-selected', 'new-database'],
data() {
return {
openModal: false,
@ -360,25 +358,17 @@ export default defineComponent({
this.creatingDemo = false;
},
async setFiles() {
const dbList: ConfigFilesWithModified[] = await ipcRenderer.invoke(
IPC_ACTIONS.GET_DB_LIST
);
const dbList = await ipc.getDbList();
this.files = dbList?.sort(
(a, b) => Date.parse(b.modified) - Date.parse(a.modified)
);
},
async newDatabase() {
newDatabase() {
if (this.creatingDemo) {
return;
}
const { filePath, canceled } = await getSavePath('books', 'db');
if (canceled || !filePath) {
return;
}
this.emitFileSelected(filePath, true);
this.$emit('new-database');
},
async existingDatabase() {
if (this.creatingDemo) {
@ -395,17 +385,12 @@ export default defineComponent({
this.emitFileSelected(file.dbPath);
},
emitFileSelected(filePath: string, isNew?: boolean) {
emitFileSelected(filePath: string) {
if (!filePath) {
return;
}
if (isNew) {
this.$emit('file-selected', filePath, isNew);
return;
}
this.$emit('file-selected', filePath, !!isNew);
this.$emit('file-selected', filePath);
},
},
});

View File

@ -76,10 +76,8 @@ import Icon from 'src/components/Icon.vue';
import PageHeader from 'src/components/PageHeader.vue';
import { fyo } from 'src/initFyo';
import { getGetStartedConfig } from 'src/utils/getStartedConfig';
import { openLink } from 'src/utils/ipcCalls';
import { GetStartedConfigItem } from 'src/utils/types';
import { Component } from 'vue';
import { defineComponent, h } from 'vue';
import { Component, defineComponent, h } from 'vue';
type ListItem = GetStartedConfigItem['items'][number];
@ -96,9 +94,6 @@ export default defineComponent({
sections: getGetStartedConfig(),
};
},
mounted() {
// this.sections = getGetStartedConfig();
},
async activated() {
await fyo.doc.getDoc('GetStarted');
await this.checkForCompletedTasks();
@ -106,7 +101,7 @@ export default defineComponent({
methods: {
async handleDocumentation({ key, documentation }: ListItem) {
if (documentation) {
openLink(documentation);
ipc.openLink(documentation);
}
switch (key) {

View File

@ -388,10 +388,9 @@ import PageHeader from 'src/components/PageHeader.vue';
import { Importer, TemplateField, getColumnLabel } from 'src/importer';
import { fyo } from 'src/initFyo';
import { showDialog } from 'src/utils/interactive';
import { getSavePath, saveData } from 'src/utils/ipcCalls';
import { docsPathMap } from 'src/utils/misc';
import { docsPathRef } from 'src/utils/refs';
import { selectTextFile } from 'src/utils/ui';
import { getSavePath, selectTextFile } from 'src/utils/ui';
import { defineComponent } from 'vue';
import Loading from '../components/Loading.vue';
@ -795,7 +794,7 @@ export default defineComponent({
return;
}
await saveData(template, filePath);
await ipc.saveData(template, filePath);
},
async preImportValidations(): Promise<boolean> {
const title = this.t`Cannot Import`;

View File

@ -80,7 +80,6 @@ import { getErrorMessage } from 'src/utils';
import { evaluateHidden } from 'src/utils/doc';
import { shortcutsKey } from 'src/utils/injectionKeys';
import { showDialog } from 'src/utils/interactive';
import { reloadWindow } from 'src/utils/ipcCalls';
import { docsPathMap } from 'src/utils/misc';
import { docsPathRef } from 'src/utils/refs';
import { UIGroupedFields } from 'src/utils/types';
@ -222,7 +221,7 @@ export default defineComponent({
{
label: this.t`Yes`,
isPrimary: true,
action: reloadWindow,
action: ipc.reloadWindow.bind(ipc),
},
{
label: this.t`No`,

View File

@ -233,7 +233,6 @@ import ShortcutKeys from 'src/components/ShortcutKeys.vue';
import { handleErrorWithDialog } from 'src/errorHandling';
import { shortcutsKey } from 'src/utils/injectionKeys';
import { showDialog, showToast } from 'src/utils/interactive';
import { getSavePath } from 'src/utils/ipcCalls';
import { docsPathMap } from 'src/utils/misc';
import {
PrintTemplateHint,
@ -248,6 +247,7 @@ import {
focusOrSelectFormControl,
getActionsForDoc,
getDocFromNameIfExistsElseNew,
getSavePath,
openSettings,
selectTextFile,
} from 'src/utils/ui';

View File

@ -1,17 +1,10 @@
const { ipcRenderer } = require('electron');
import { DateTime } from 'luxon';
import { CUSTOM_EVENTS, IPC_ACTIONS } from 'utils/messages';
import { CUSTOM_EVENTS } from 'utils/messages';
import { UnexpectedLogObject } from 'utils/types';
import { App as VueApp, createApp } from 'vue';
import App from './App.vue';
import Badge from './components/Badge.vue';
import FeatherIcon from './components/FeatherIcon.vue';
import {
getErrorHandled,
getErrorHandledSync,
handleError,
sendError,
} from './errorHandling';
import { handleError, sendError } from './errorHandling';
import { fyo } from './initFyo';
import { outsideClickDirective } from './renderer/helpers';
import registerIpcRendererListeners from './renderer/registerIpcRendererListeners';
@ -27,13 +20,8 @@ import { setLanguageMap } from './utils/language';
}
fyo.store.language = language || 'English';
ipcRenderer.send = getErrorHandledSync(ipcRenderer.send.bind(ipcRenderer));
ipcRenderer.invoke = getErrorHandled(ipcRenderer.invoke.bind(ipcRenderer));
registerIpcRendererListeners();
const { isDevelopment, platform, version } = (await ipcRenderer.invoke(
IPC_ACTIONS.GET_ENV
)) as { isDevelopment: boolean; platform: string; version: string };
const { isDevelopment, platform, version } = await ipc.getEnv();
fyo.store.isDevelopment = isDevelopment;
fyo.store.appVersion = version;
@ -125,10 +113,6 @@ function setOnWindow(isDevelopment: boolean) {
window.router = router;
// @ts-ignore
window.fyo = fyo;
// @ts-ignore
window.DateTime = DateTime;
// @ts-ignore
window.ipcRenderer = ipcRenderer;
}
function getPlatformName(platform: string) {

View File

@ -1,11 +1,8 @@
const { ipcRenderer } = require('electron');
import { handleError } from 'src/errorHandling';
import { fyo } from 'src/initFyo';
import { IPC_CHANNELS } from 'utils/messages';
export default function registerIpcRendererListeners() {
ipcRenderer.on(
IPC_CHANNELS.LOG_MAIN_PROCESS_ERROR,
ipc.registerMainProcessErrorListener(
(_, error: unknown, more?: Record<string, unknown>) => {
if (!(error instanceof Error)) {
throw error;
@ -27,7 +24,7 @@ export default function registerIpcRendererListeners() {
}
);
ipcRenderer.on(IPC_CHANNELS.CONSOLE_LOG, (_, ...stuff: unknown[]) => {
ipc.registerConsoleLogListener((_, ...stuff: unknown[]) => {
if (!fyo.store.isDevelopment) {
return;
}

6
src/shims-tsx.d.ts vendored
View File

@ -1,6 +1,8 @@
import type { IPC } from 'main/preload';
import Vue, { VNode } from 'vue';
declare global {
const ipc: IPC;
namespace JSX {
type Element = VNode;
type ElementClass = Vue;
@ -9,4 +11,8 @@ declare global {
[elem: string]: any;
}
}
interface Window {
ipc: IPC;
}
}

View File

@ -1,156 +0,0 @@
/**
* Utils that make ipcRenderer calls.
*/
const { ipcRenderer } = require('electron');
import { t } from 'fyo';
import { BaseError } from 'fyo/utils/errors';
import type { BackendResponse } from 'utils/ipc/types';
import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages';
import type {
LanguageMap,
SelectFileOptions,
SelectFileReturn,
TemplateFile,
} from 'utils/types';
import { showDialog, showToast } from './interactive';
import { setLanguageMap } from './language';
import type { OpenDialogReturnValue } from 'electron';
export function reloadWindow() {
return ipcRenderer.send(IPC_MESSAGES.RELOAD_MAIN_WINDOW);
}
export async function getLanguageMap(code: string) {
return (await ipcRenderer.invoke(IPC_ACTIONS.GET_LANGUAGE_MAP, code)) as {
languageMap: LanguageMap;
success: boolean;
message: string;
};
}
export async function getSelectedFilePath(): Promise<OpenDialogReturnValue> {
return (await ipcRenderer.invoke(IPC_ACTIONS.GET_OPEN_FILEPATH, {
title: t`Select file`,
properties: ['openFile'],
filters: [{ name: 'SQLite DB File', extensions: ['db'] }],
})) as OpenDialogReturnValue;
}
export async function getTemplates(): Promise<TemplateFile[]> {
return (await ipcRenderer.invoke(
IPC_ACTIONS.GET_TEMPLATES
)) as TemplateFile[];
}
export async function selectFile(
options: SelectFileOptions
): Promise<SelectFileReturn> {
return (await ipcRenderer.invoke(
IPC_ACTIONS.SELECT_FILE,
options
)) as SelectFileReturn;
}
export async function checkDbAccess(filePath: string) {
return (await ipcRenderer.invoke(
IPC_ACTIONS.CHECK_DB_ACCESS,
filePath
)) as boolean;
}
export async function checkForUpdates() {
await ipcRenderer.invoke(IPC_ACTIONS.CHECK_FOR_UPDATES);
await setLanguageMap();
}
export function openLink(link: string) {
ipcRenderer.send(IPC_MESSAGES.OPEN_EXTERNAL, link);
}
export async function deleteDb(filePath: string) {
const { error } = (await ipcRenderer.invoke(
IPC_ACTIONS.DELETE_FILE,
filePath
)) as BackendResponse;
if (error?.code === 'EBUSY') {
await showDialog({
title: t`Delete Failed`,
detail: t`Please restart and try again.`,
type: 'error',
});
} else if (error?.code === 'ENOENT') {
await showDialog({
title: t`Delete Failed`,
detail: t`File ${filePath} does not exist.`,
type: 'error',
});
} else if (error?.code === 'EPERM') {
await showDialog({
title: t`Cannot Delete`,
detail: t`Close Frappe Books and try manually.`,
type: 'error',
});
} else if (error) {
const err = new BaseError(500, error.message);
err.name = error.name;
err.stack = error.stack;
throw err;
}
}
export async function saveData(data: string, savePath: string) {
await ipcRenderer.invoke(IPC_ACTIONS.SAVE_DATA, data, savePath);
}
export function showItemInFolder(filePath: string) {
ipcRenderer.send(IPC_MESSAGES.SHOW_ITEM_IN_FOLDER, filePath);
}
export async function makePDF(
html: string,
savePath: string,
width: number,
height: number
): Promise<void> {
const success = (await ipcRenderer.invoke(
IPC_ACTIONS.SAVE_HTML_AS_PDF,
html,
savePath,
width,
height
)) as boolean;
if (success) {
showExportInFolder(t`Save as PDF Successful`, savePath);
} else {
showToast({ message: t`Export Failed`, type: 'error' });
}
}
export function showExportInFolder(message: string, filePath: string) {
showToast({
message,
actionText: t`Open Folder`,
type: 'success',
action: () => {
showItemInFolder(filePath);
},
});
}
export async function getSavePath(name: string, extention: string) {
const response = (await ipcRenderer.invoke(IPC_ACTIONS.GET_SAVE_FILEPATH, {
title: t`Select Folder`,
defaultPath: `${name}.${extention}`,
})) as { canceled: boolean; filePath?: string };
const canceled = response.canceled;
let filePath = response.filePath;
if (filePath && !filePath.endsWith(extention) && filePath !== ':memory:') {
filePath = `${filePath}.${extention}`;
}
return { canceled, filePath };
}

View File

@ -1,7 +1,6 @@
import { DEFAULT_LANGUAGE } from 'fyo/utils/consts';
import { setLanguageMapOnTranslationString } from 'fyo/utils/translation';
import { fyo } from 'src/initFyo';
import { getLanguageMap, reloadWindow } from './ipcCalls';
import { systemLanguageRef } from './refs';
// Language: Language Code in books/translations
@ -45,7 +44,7 @@ export async function setLanguageMap(
}
if (!dontReload && success && initLanguage !== oldLanguage) {
reloadWindow();
ipc.reloadWindow();
}
return success;
}
@ -63,7 +62,7 @@ function getLanguageCode(initLanguage: string, oldLanguage: string) {
}
async function fetchAndSetLanguageMap(code: string) {
const { success, message, languageMap } = await getLanguageMap(code);
const { success, message, languageMap } = await ipc.getLanguageMap(code);
if (!success) {
const { showToast } = await import('src/utils/interactive');

View File

@ -1,13 +1,17 @@
import { Fyo } from 'fyo';
import { Fyo, t } from 'fyo';
import { Doc } from 'fyo/model/doc';
import { Invoice } from 'models/baseModels/Invoice/Invoice';
import { ModelNameEnum } from 'models/types';
import { FieldTypeEnum, Schema, TargetField } from 'schemas/types';
import { getValueMapFromList } from 'utils/index';
import { TemplateFile } from 'utils/types';
import { getSavePath, getTemplates, makePDF } from './ipcCalls';
import { showToast } from './interactive';
import { PrintValues } from './types';
import { getDocFromNameIfExistsElseNew } from './ui';
import {
getDocFromNameIfExistsElseNew,
getSavePath,
showExportInFolder,
} from './ui';
export type PrintTemplateHint = {
[key: string]: string | PrintTemplateHint | PrintTemplateHint[];
@ -223,13 +227,18 @@ export async function getPathAndMakePDF(
width: number,
height: number
) {
const { filePath } = await getSavePath(name, 'pdf');
if (!filePath) {
const { filePath: savePath } = await getSavePath(name, 'pdf');
if (!savePath) {
return;
}
const html = constructPrintDocument(innerHTML);
await makePDF(html, filePath, width, height);
const success = await ipc.makePDF(html, savePath, width, height);
if (success) {
showExportInFolder(t`Save as PDF Successful`, savePath);
} else {
showToast({ message: t`Export Failed`, type: 'error' });
}
}
function constructPrintDocument(innerHTML: string) {
@ -267,7 +276,7 @@ function getAllCSSAsStyleElem() {
}
export async function updatePrintTemplates(fyo: Fyo) {
const templateFiles = await getTemplates();
const templateFiles = await ipc.getTemplates();
const existingTemplates = (await fyo.db.getAll(ModelNameEnum.PrintTemplate, {
fields: ['name', 'modified'],
filters: { isCustom: false },

View File

@ -6,7 +6,12 @@ import { t } from 'fyo';
import type { Doc } from 'fyo/model/doc';
import { Action } from 'fyo/model/types';
import { getActions } from 'fyo/utils';
import { getDbError, LinkValidationError, ValueError } from 'fyo/utils/errors';
import {
BaseError,
getDbError,
LinkValidationError,
ValueError,
} from 'fyo/utils/errors';
import { Invoice } from 'models/baseModels/Invoice/Invoice';
import { PurchaseInvoice } from 'models/baseModels/PurchaseInvoice/PurchaseInvoice';
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
@ -18,11 +23,11 @@ import { Schema } from 'schemas/types';
import { handleErrorWithDialog } from 'src/errorHandling';
import { fyo } from 'src/initFyo';
import router from 'src/router';
import { assertIsType } from 'utils/index';
import { SelectFileOptions } from 'utils/types';
import { RouteLocationRaw } from 'vue-router';
import { evaluateHidden } from './doc';
import { showDialog, showToast } from './interactive';
import { selectFile } from './ipcCalls';
import { showSidebar } from './refs';
import {
ActionGroup,
@ -31,7 +36,6 @@ import {
ToastOptions,
UIGroupedFields,
} from './types';
import { assertIsType } from 'utils/index';
export const toastDurationMap = { short: 2_500, long: 5_000 } as const;
@ -434,7 +438,9 @@ export async function selectTextFile(filters?: SelectFileOptions['filters']) {
title: t`Select File`,
filters,
};
const { success, canceled, filePath, data, name } = await selectFile(options);
const { success, canceled, filePath, data, name } = await ipc.selectFile(
options
);
if (canceled || !success) {
showToast({
@ -953,3 +959,67 @@ export const paperSizeMap: Record<
height: -1,
},
};
export function showExportInFolder(message: string, filePath: string) {
showToast({
message,
actionText: t`Open Folder`,
type: 'success',
action: () => {
ipc.showItemInFolder(filePath);
},
});
}
export async function deleteDb(filePath: string) {
const { error } = await ipc.deleteFile(filePath);
if (error?.code === 'EBUSY') {
await showDialog({
title: t`Delete Failed`,
detail: t`Please restart and try again.`,
type: 'error',
});
} else if (error?.code === 'ENOENT') {
await showDialog({
title: t`Delete Failed`,
detail: t`File ${filePath} does not exist.`,
type: 'error',
});
} else if (error?.code === 'EPERM') {
await showDialog({
title: t`Cannot Delete`,
detail: t`Close Frappe Books and try manually.`,
type: 'error',
});
} else if (error) {
const err = new BaseError(500, error.message);
err.name = error.name;
err.stack = error.stack;
throw err;
}
}
export async function getSelectedFilePath() {
return ipc.getOpenFilePath({
title: t`Select file`,
properties: ['openFile'],
filters: [{ name: 'SQLite DB File', extensions: ['db'] }],
});
}
export async function getSavePath(name: string, extention: string) {
const response = await ipc.getSaveFilePath({
title: t`Select folder`,
defaultPath: `${name}.${extention}`,
});
const canceled = response.canceled;
let filePath = response.filePath;
if (filePath && !filePath.endsWith(extention) && filePath !== ':memory:') {
filePath = `${filePath}.${extention}`;
}
return { canceled, filePath };
}

View File

@ -44,10 +44,6 @@ const appSourcePath = path.join(root, 'dist_electron', 'build', 'main.js');
});
test('fill setup form', async (t) => {
await electronApp.evaluate(({ dialog }, filePath) => {
dialog.showSaveDialog = () =>
Promise.resolve({ canceled: false, filePath });
}, ':memory:');
await window.getByTestId('create-new-file').click();
await window.getByTestId('submit-button').waitFor();

View File

@ -25,6 +25,7 @@ export enum IPC_ACTIONS {
GET_DB_LIST = 'get-db-list',
GET_TEMPLATES = 'get-templates',
DELETE_FILE = 'delete-file',
GET_DB_DEFAULT_PATH = 'get-db-default-path',
// Database messages
DB_CREATE = 'db-create',
DB_CONNECT = 'db-connect',

View File

@ -585,6 +585,13 @@
resolved "https://registry.yarnpkg.com/@types/assert/-/assert-1.5.6.tgz#a8b5a94ce5fb8f4ba65fdc37fc9507609114189e"
integrity sha512-Y7gDJiIqb9qKUHfBQYOWGngUpLORtirAVPuj/CWJrU2C6ZM4/y3XLwuwfGMF8s7QzW746LQZx23m0+1FSgjfug==
"@types/better-sqlite3@^7.6.4":
version "7.6.4"
resolved "https://registry.yarnpkg.com/@types/better-sqlite3/-/better-sqlite3-7.6.4.tgz#102462611e67aadf950d3ccca10292de91e6f35b"
integrity sha512-dzrRZCYPXIXfSR1/surNbJ/grU3scTaygS0OMzjlGf71i9sc2fGyHPXXiXmEvNIoE0cGwsanEFMVJxPXmco9Eg==
dependencies:
"@types/node" "*"
"@types/cacheable-request@^6.0.1":
version "6.0.2"
resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.2.tgz#c324da0197de0a98a2312156536ae262429ff6b9"