mirror of
https://github.com/frappe/books.git
synced 2024-12-22 10:58:59 +00:00
refactor: simplify init flow
- add README.md - move currency to systemsettings
This commit is contained in:
parent
a39d7ed555
commit
9ec004184c
@ -19,9 +19,10 @@ export class DatabaseManager extends DatabaseDemuxBase {
|
||||
return this.db?.schemaMap ?? {};
|
||||
}
|
||||
|
||||
async createNewDatabase(dbPath: string, countryCode?: string) {
|
||||
async createNewDatabase(dbPath: string, countryCode: string) {
|
||||
await this.#unlinkIfExists(dbPath);
|
||||
await this.connectToDatabase(dbPath, countryCode);
|
||||
return countryCode;
|
||||
}
|
||||
|
||||
async connectToDatabase(dbPath: string, countryCode?: string) {
|
||||
@ -34,6 +35,7 @@ export class DatabaseManager extends DatabaseDemuxBase {
|
||||
this.db.setSchemaMap(schemaMap);
|
||||
|
||||
await this.#migrate();
|
||||
return countryCode;
|
||||
}
|
||||
|
||||
async call(method: DatabaseMethod, ...args: unknown[]) {
|
||||
|
91
frappe/README.md
Normal file
91
frappe/README.md
Normal file
@ -0,0 +1,91 @@
|
||||
# Fyo
|
||||
|
||||
This is the underlying framework that runs **Books**, at some point it may be
|
||||
removed into a separate repo, but as of now it's in gestation.
|
||||
|
||||
The reason for maintaining a framework is to allow for varied backends.
|
||||
Currently Books runs on the electron renderer process and all db stuff happens
|
||||
on the electron main process which has access to node libs. As the development
|
||||
of `Fyo` progresses it will allow for a browser frontend and a node server
|
||||
backend.
|
||||
|
||||
This platform variablity will be handled by code in the `fyo/demux` subdirectory.
|
||||
|
||||
## Pre Req
|
||||
|
||||
**Singleton**: The `Fyo` class is used as a singleton throughout Books, this
|
||||
allows for a single source of truth and a common interface to access different
|
||||
modules such as `db`, `doc` an `auth`.
|
||||
|
||||
**Localization**: Since Books' functionality changes depending on region,
|
||||
regional information is required in the initialization process.
|
||||
|
||||
**Doc**: This is `fyo`'s abstraction for an ORM, the associated files are
|
||||
located in `model`, all classes exported from `books/models` extend this.
|
||||
|
||||
### Terminology
|
||||
|
||||
- **Schema**: object that defines shape of the data in the database
|
||||
- **Model**: the controller class that extends the `Doc` class or the `Doc`
|
||||
class itself (if a controller doesn't exist).
|
||||
- **Doc**: instance of a Model, i.e. what has the data.
|
||||
|
||||
## Initialization
|
||||
|
||||
There are a set of core models which are maintained in the `fyo/models`
|
||||
subdirectory, from this the _SystemSettings_ field `countryCode` is used to
|
||||
config regional information.
|
||||
|
||||
A few things have to be done on initialization:
|
||||
|
||||
#### 1. Connect To DB
|
||||
|
||||
If creating a new instance then `fyo.db.createNewDatabase` or if loading an
|
||||
instance `fyo.db.connectToDatabase`.
|
||||
|
||||
Both of them take `countryCode` as an argument, `fyo.db.createNewDatabase`
|
||||
should be passed the `countryCode` as the schemas are built on the basis of
|
||||
this.
|
||||
|
||||
#### 2. Initialize and Register
|
||||
|
||||
Done using `fyo.initializeAndRegister` after a database is connected, this should be
|
||||
passed the models and regional models.
|
||||
|
||||
This sets the schemas and associated models on the `fyo` object along with a few
|
||||
other things.
|
||||
|
||||
### Sequence
|
||||
|
||||
**First Load**: i.e. registering or creating a new instance.
|
||||
|
||||
- Get `countryCode` from the setup wizard.
|
||||
- Create a new DB using `fyo.db.createNewDatabase` with the `countryCode`.
|
||||
- Get models and `regionalModels` using `countryCode` from `models/index.ts/getRegionalModels`.
|
||||
- Call `fyo.initializeAndRegister` with the all models.
|
||||
|
||||
**Next Load**: i.e. logging in or opening an existing instance.
|
||||
|
||||
- Connect to DB using `fyo.db.connectToDatabase` and get `countryCode` from the return.
|
||||
- Get models and `regionalModels` using `countryCode` from `models/index.ts/getRegionalModels`.
|
||||
- Call `fyo.initializeAndRegister` with the all models.
|
||||
|
||||
## Testing
|
||||
|
||||
For testing the `fyo` class, `mocha` is used (`node` side). So for this the
|
||||
demux classes are directly replaced by `node` side managers such as
|
||||
`DatabaseManager`.
|
||||
|
||||
For this to work the class signatures of the demux class and the manager have to
|
||||
be the same.
|
||||
|
||||
## Translations
|
||||
|
||||
All translations take place during runtime, for translations to work, a
|
||||
`LanguageMap` (for def check `utils/types.ts`) has to be set.
|
||||
|
||||
This can be done using `fyo/utils/translation.ts/setLanguageMapOnTranslationString`.
|
||||
|
||||
Since translations are runtime, if the code is evaluated before the language map
|
||||
is loaded, translations won't work. To prevent this, don't maintain translation
|
||||
strings globally.
|
@ -36,12 +36,16 @@ export class DatabaseHandler extends DatabaseBase {
|
||||
}
|
||||
}
|
||||
|
||||
async createNewDatabase(dbPath: string, countryCode?: string) {
|
||||
await this.#demux.createNewDatabase(dbPath, countryCode);
|
||||
async createNewDatabase(dbPath: string, countryCode: string) {
|
||||
countryCode = await this.#demux.createNewDatabase(dbPath, countryCode);
|
||||
await this.init();
|
||||
return countryCode;
|
||||
}
|
||||
|
||||
async connectToDatabase(dbPath: string, countryCode?: string) {
|
||||
await this.#demux.connectToDatabase(dbPath, countryCode);
|
||||
countryCode = await this.#demux.connectToDatabase(dbPath, countryCode);
|
||||
await this.init();
|
||||
return countryCode;
|
||||
}
|
||||
|
||||
async init() {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Doc from 'frappe/model/doc';
|
||||
import { DocMap, ModelMap } from 'frappe/model/types';
|
||||
import { coreModels } from 'frappe/models';
|
||||
import { getRandomString } from 'frappe/utils';
|
||||
import Observable from 'frappe/utils/observable';
|
||||
import { Frappe } from '..';
|
||||
@ -20,9 +21,17 @@ export class DocHandler {
|
||||
this.docs = new Observable();
|
||||
}
|
||||
|
||||
registerModels(models: ModelMap) {
|
||||
for (const schemaName in models) {
|
||||
this.models[schemaName] = models[schemaName];
|
||||
registerModels(models: ModelMap, regionalModels: ModelMap = {}) {
|
||||
for (const schemaName in this.frappe.db.schemaMap) {
|
||||
if (coreModels[schemaName] !== undefined) {
|
||||
this.models[schemaName] = coreModels[schemaName];
|
||||
} else if (regionalModels[schemaName] !== undefined) {
|
||||
this.models[schemaName] = regionalModels[schemaName];
|
||||
} else if (models[schemaName] !== undefined) {
|
||||
this.models[schemaName] = models[schemaName];
|
||||
} else {
|
||||
this.models[schemaName] = Doc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { DEFAULT_COUNTRY_CODE } from 'frappe/utils/consts';
|
||||
import { SchemaMap } from 'schemas/types';
|
||||
import { DatabaseDemuxBase, DatabaseMethod } from 'utils/db/types';
|
||||
import { DatabaseResponse } from 'utils/ipc/types';
|
||||
@ -27,7 +28,10 @@ export class DatabaseDemux extends DatabaseDemuxBase {
|
||||
return response.data as SchemaMap;
|
||||
}
|
||||
|
||||
async createNewDatabase(dbPath: string, countryCode?: string): Promise<void> {
|
||||
async createNewDatabase(
|
||||
dbPath: string,
|
||||
countryCode?: string
|
||||
): Promise<string> {
|
||||
let response: DatabaseResponse;
|
||||
if (this.#isElectron) {
|
||||
response = await ipcRenderer.invoke(
|
||||
@ -43,9 +47,14 @@ export class DatabaseDemux extends DatabaseDemuxBase {
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
return (response.data ?? DEFAULT_COUNTRY_CODE) as string;
|
||||
}
|
||||
|
||||
async connectToDatabase(dbPath: string, countryCode?: string): Promise<void> {
|
||||
async connectToDatabase(
|
||||
dbPath: string,
|
||||
countryCode?: string
|
||||
): Promise<string> {
|
||||
let response: DatabaseResponse;
|
||||
if (this.#isElectron) {
|
||||
response = await ipcRenderer.invoke(
|
||||
@ -61,6 +70,8 @@ export class DatabaseDemux extends DatabaseDemuxBase {
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
return (response.data ?? DEFAULT_COUNTRY_CODE) as string;
|
||||
}
|
||||
|
||||
async call(method: DatabaseMethod, ...args: unknown[]): Promise<unknown> {
|
||||
|
@ -5,8 +5,8 @@ import { DatabaseHandler } from './core/dbHandler';
|
||||
import { DocHandler } from './core/docHandler';
|
||||
import { DatabaseDemuxConstructor } from './core/types';
|
||||
import { ModelMap } from './model/types';
|
||||
import coreModels from './models';
|
||||
import {
|
||||
DEFAULT_CURRENCY,
|
||||
DEFAULT_DISPLAY_PRECISION,
|
||||
DEFAULT_INTERNAL_PRECISION,
|
||||
} from './utils/consts';
|
||||
@ -15,13 +15,6 @@ import { format } from './utils/format';
|
||||
import { t, T } from './utils/translation';
|
||||
import { ErrorLog } from './utils/types';
|
||||
|
||||
/**
|
||||
* Terminology
|
||||
* - Schema: object that defines shape of the data in the database
|
||||
* - Model: the controller class that extends the Doc class or the Doc
|
||||
* class itself.
|
||||
* - Doc: instance of a Model, i.e. what has the data.
|
||||
*/
|
||||
|
||||
export class Frappe {
|
||||
t = t;
|
||||
@ -81,18 +74,20 @@ export class Frappe {
|
||||
}
|
||||
|
||||
async initializeAndRegister(
|
||||
customModels: ModelMap = {},
|
||||
models: ModelMap = {},
|
||||
regionalModels: ModelMap = {},
|
||||
force: boolean = false
|
||||
) {
|
||||
await this.init(force);
|
||||
|
||||
this.doc.registerModels(coreModels as ModelMap);
|
||||
this.doc.registerModels(customModels);
|
||||
}
|
||||
|
||||
async init(force?: boolean) {
|
||||
if (this._initialized && !force) return;
|
||||
|
||||
await this.#initializeModules();
|
||||
await this.#initializeMoneyMaker();
|
||||
|
||||
this.doc.registerModels(models, regionalModels);
|
||||
this._initialized = true;
|
||||
}
|
||||
|
||||
async #initializeModules() {
|
||||
this.methods = {};
|
||||
this.errorLog = [];
|
||||
|
||||
@ -102,11 +97,9 @@ export class Frappe {
|
||||
await this.doc.init();
|
||||
await this.auth.init();
|
||||
await this.db.init();
|
||||
this._initialized = true;
|
||||
}
|
||||
|
||||
async initializeMoneyMaker(currency: string = 'XXX') {
|
||||
// to be called after db initialization
|
||||
async #initializeMoneyMaker() {
|
||||
const values =
|
||||
(await this.db?.getSingleValues(
|
||||
{
|
||||
@ -116,6 +109,10 @@ export class Frappe {
|
||||
{
|
||||
fieldname: 'displayPrecision',
|
||||
parent: 'SystemSettings',
|
||||
},
|
||||
{
|
||||
fieldname: 'currency',
|
||||
parent: 'SystemSettings',
|
||||
}
|
||||
)) ?? [];
|
||||
|
||||
@ -124,18 +121,11 @@ export class Frappe {
|
||||
return acc;
|
||||
}, {} as Record<string, string | number | undefined>);
|
||||
|
||||
let precision: string | number =
|
||||
acc.internalPrecision ?? DEFAULT_INTERNAL_PRECISION;
|
||||
let display: string | number =
|
||||
acc.displayPrecision ?? DEFAULT_DISPLAY_PRECISION;
|
||||
|
||||
if (typeof precision === 'string') {
|
||||
precision = parseInt(precision);
|
||||
}
|
||||
|
||||
if (typeof display === 'string') {
|
||||
display = parseInt(display);
|
||||
}
|
||||
const precision: number =
|
||||
(acc.internalPrecision as number) ?? DEFAULT_INTERNAL_PRECISION;
|
||||
const display: number =
|
||||
(acc.displayPrecision as number) ?? DEFAULT_DISPLAY_PRECISION;
|
||||
const currency: string = (acc.currency as string) ?? DEFAULT_CURRENCY;
|
||||
|
||||
this.pesa = getMoneyMaker({
|
||||
currency,
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { ModelMap } from 'frappe/model/types';
|
||||
import NumberSeries from './NumberSeries';
|
||||
import SystemSettings from './SystemSettings';
|
||||
|
||||
export default {
|
||||
export const coreModels = {
|
||||
NumberSeries,
|
||||
SystemSettings,
|
||||
};
|
||||
} as ModelMap;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as assert from 'assert';
|
||||
import 'mocha';
|
||||
import { DatabaseManager } from '../../backend/database/manager';
|
||||
import { Frappe } from '..';
|
||||
import { DatabaseManager } from '../../backend/database/manager';
|
||||
|
||||
describe('Frappe', function () {
|
||||
const frappe = new Frappe(DatabaseManager);
|
||||
@ -12,15 +12,15 @@ describe('Frappe', function () {
|
||||
0,
|
||||
'zero schemas one'
|
||||
);
|
||||
await frappe.init();
|
||||
await frappe.initializeAndRegister();
|
||||
assert.strictEqual(
|
||||
Object.keys(frappe.schemaMap).length,
|
||||
0,
|
||||
'zero schemas two'
|
||||
);
|
||||
|
||||
await frappe.db.createNewDatabase(':memory:');
|
||||
await frappe.initializeAndRegister({}, true);
|
||||
await frappe.db.createNewDatabase(':memory:', 'in');
|
||||
await frappe.initializeAndRegister({}, {}, true);
|
||||
assert.strictEqual(
|
||||
Object.keys(frappe.schemaMap).length > 0,
|
||||
true,
|
||||
|
@ -1,6 +1,8 @@
|
||||
export const DEFAULT_INTERNAL_PRECISION = 11;
|
||||
export const DEFAULT_DISPLAY_PRECISION = 2;
|
||||
export const DEFAULT_LOCALE = 'en-IN';
|
||||
export const DEFAULT_COUNTRY_CODE = 'in';
|
||||
export const DEFAULT_CURRENCY = 'XXX';
|
||||
export const DEFAULT_LANGUAGE = 'English';
|
||||
export const DEFAULT_NUMBER_SERIES = {
|
||||
SalesInvoice: 'SINV-',
|
||||
|
@ -126,7 +126,7 @@ export default function registerIpcMainActionListeners(main: Main) {
|
||||
|
||||
ipcMain.handle(
|
||||
IPC_ACTIONS.DB_CREATE,
|
||||
async (_, dbPath: string, countryCode?: string) => {
|
||||
async (_, dbPath: string, countryCode: string) => {
|
||||
const response: DatabaseResponse = { error: '', data: undefined };
|
||||
try {
|
||||
response.data = await databaseManager.createNewDatabase(
|
||||
@ -146,7 +146,7 @@ export default function registerIpcMainActionListeners(main: Main) {
|
||||
async (_, dbPath: string, countryCode?: string) => {
|
||||
const response: DatabaseResponse = { error: '', data: undefined };
|
||||
try {
|
||||
response.data = await databaseManager.createNewDatabase(
|
||||
response.data = await databaseManager.connectToDatabase(
|
||||
dbPath,
|
||||
countryCode
|
||||
);
|
||||
|
@ -33,13 +33,6 @@
|
||||
"readOnly": true,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"label": "Currency",
|
||||
"fieldtype": "Data",
|
||||
"readOnly": true,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"fieldname": "fullname",
|
||||
"label": "Name",
|
||||
@ -82,7 +75,6 @@
|
||||
"email",
|
||||
"companyName",
|
||||
"country",
|
||||
"currency",
|
||||
"fiscalYearStart",
|
||||
"fiscalYearEnd"
|
||||
]
|
||||
|
@ -79,11 +79,19 @@
|
||||
"label": "Country Code",
|
||||
"fieldtype": "Data",
|
||||
"description": "Country code used to initialize regional settings."
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"label": "Currency",
|
||||
"fieldtype": "Data",
|
||||
"readOnly": true,
|
||||
"required": false
|
||||
}
|
||||
],
|
||||
"quickEditFields": [
|
||||
"locale",
|
||||
"dateFormat",
|
||||
"currency",
|
||||
"displayPrecision",
|
||||
"hideGetStarted"
|
||||
],
|
||||
|
@ -1,14 +1,3 @@
|
||||
import FormControl from '@/components/Controls/FormControl'; import
|
||||
LanguageSelector from '@/components/Controls/LanguageSelector.vue'; import
|
||||
Popover from '@/components/Popover'; import TwoColumnForm from
|
||||
'@/components/TwoColumnForm'; import config from '@/config'; import {
|
||||
connectToLocalDatabase, purgeCache } from '@/initialization'; import {
|
||||
IPC_MESSAGES } from '@/messages'; import { setLanguageMap, showMessageDialog }
|
||||
from '@/utils'; import { ipcRenderer } from 'electron'; import frappe from
|
||||
'frappe'; import fs from 'fs'; import path from 'path'; import {
|
||||
getErrorMessage, handleErrorWithDialog, showErrorDialog } from
|
||||
'../../errorHandling'; import setupCompany from './setupCompany'; import Slide
|
||||
from './Slide.vue';
|
||||
<template>
|
||||
<div>
|
||||
<Slide
|
||||
@ -106,16 +95,16 @@ import Popover from '@/components/Popover';
|
||||
import TwoColumnForm from '@/components/TwoColumnForm';
|
||||
import config from '@/config';
|
||||
import { connectToLocalDatabase, purgeCache } from '@/initialization';
|
||||
import { IPC_MESSAGES } from 'utils/messages';
|
||||
import { setLanguageMap, showMessageDialog } from '@/utils';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import frappe from 'frappe';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { IPC_MESSAGES } from 'utils/messages';
|
||||
import {
|
||||
getErrorMessage,
|
||||
handleErrorWithDialog,
|
||||
showErrorDialog,
|
||||
getErrorMessage,
|
||||
handleErrorWithDialog,
|
||||
showErrorDialog
|
||||
} from '../../errorHandling';
|
||||
import setupCompany from './setupCompany';
|
||||
import Slide from './Slide.vue';
|
||||
|
@ -79,13 +79,13 @@ export abstract class DatabaseDemuxBase {
|
||||
|
||||
abstract createNewDatabase(
|
||||
dbPath: string,
|
||||
countryCode?: string
|
||||
): Promise<void>;
|
||||
countryCode: string
|
||||
): Promise<string>;
|
||||
|
||||
abstract connectToDatabase(
|
||||
dbPath: string,
|
||||
countryCode?: string
|
||||
): Promise<void>;
|
||||
): Promise<string>;
|
||||
|
||||
abstract call(method: DatabaseMethod, ...args: unknown[]): Promise<unknown>;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user