2
0
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:
18alantom 2022-04-18 12:12:56 +05:30
parent a39d7ed555
commit 9ec004184c
14 changed files with 173 additions and 74 deletions

View File

@ -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
View 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.

View File

@ -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() {

View File

@ -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;
}
}
}

View File

@ -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> {

View File

@ -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,

View File

@ -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;

View File

@ -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,

View File

@ -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-',

View File

@ -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
);

View File

@ -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"
]

View File

@ -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"
],

View File

@ -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';

View File

@ -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>;