2022-04-18 11:29:20 +00:00
|
|
|
import { SingleValue } from 'backend/database/types';
|
2022-04-19 05:59:36 +00:00
|
|
|
import { Fyo } from 'fyo';
|
|
|
|
import { DatabaseDemux } from 'fyo/demux/db';
|
2022-05-11 10:44:31 +00:00
|
|
|
import { ValueError } from 'fyo/utils/errors';
|
2022-05-04 08:19:40 +00:00
|
|
|
import Observable from 'fyo/utils/observable';
|
2022-04-18 05:07:36 +00:00
|
|
|
import { Field, RawValue, SchemaMap } from 'schemas/types';
|
|
|
|
import { getMapFromList } from 'utils';
|
2022-04-07 13:38:17 +00:00
|
|
|
import { DatabaseBase, DatabaseDemuxBase, GetAllOptions } from 'utils/db/types';
|
2022-04-18 05:07:36 +00:00
|
|
|
import { Converter } from './converter';
|
2022-04-07 13:38:17 +00:00
|
|
|
import {
|
|
|
|
DatabaseDemuxConstructor,
|
|
|
|
DocValue,
|
|
|
|
DocValueMap,
|
|
|
|
RawValueMap,
|
|
|
|
} from './types';
|
2022-03-22 09:28:36 +00:00
|
|
|
|
2022-04-11 07:15:35 +00:00
|
|
|
// Return types of Bespoke Queries
|
2022-04-11 06:04:55 +00:00
|
|
|
type TopExpenses = { account: string; total: number }[];
|
|
|
|
type TotalOutstanding = { total: number; outstanding: number };
|
|
|
|
type Cashflow = { inflow: number; outflow: number; 'month-year': string }[];
|
|
|
|
|
2022-03-31 09:04:30 +00:00
|
|
|
export class DatabaseHandler extends DatabaseBase {
|
2022-04-19 05:59:36 +00:00
|
|
|
#fyo: Fyo;
|
2022-04-18 05:07:36 +00:00
|
|
|
converter: Converter;
|
2022-04-07 13:38:17 +00:00
|
|
|
#demux: DatabaseDemuxBase;
|
2022-04-20 10:41:27 +00:00
|
|
|
dbPath?: string;
|
2022-03-31 12:04:30 +00:00
|
|
|
schemaMap: Readonly<SchemaMap> = {};
|
2022-05-04 08:19:40 +00:00
|
|
|
observer: Observable<never> = new Observable();
|
2022-04-18 05:07:36 +00:00
|
|
|
fieldValueMap: Record<string, Record<string, Field>> = {};
|
2022-03-22 09:28:36 +00:00
|
|
|
|
2022-04-19 05:59:36 +00:00
|
|
|
constructor(fyo: Fyo, Demux?: DatabaseDemuxConstructor) {
|
2022-03-31 09:04:30 +00:00
|
|
|
super();
|
2022-04-19 05:59:36 +00:00
|
|
|
this.#fyo = fyo;
|
|
|
|
this.converter = new Converter(this, this.#fyo);
|
2022-04-07 13:38:17 +00:00
|
|
|
|
|
|
|
if (Demux !== undefined) {
|
2022-04-19 05:59:36 +00:00
|
|
|
this.#demux = new Demux(fyo.isElectron);
|
2022-04-07 13:38:17 +00:00
|
|
|
} else {
|
2022-04-19 05:59:36 +00:00
|
|
|
this.#demux = new DatabaseDemux(fyo.isElectron);
|
2022-04-07 13:38:17 +00:00
|
|
|
}
|
2022-03-22 09:28:36 +00:00
|
|
|
}
|
|
|
|
|
2022-04-22 11:02:03 +00:00
|
|
|
get isConnected() {
|
|
|
|
return !!this.dbPath;
|
|
|
|
}
|
|
|
|
|
2022-04-18 06:42:56 +00:00
|
|
|
async createNewDatabase(dbPath: string, countryCode: string) {
|
|
|
|
countryCode = await this.#demux.createNewDatabase(dbPath, countryCode);
|
|
|
|
await this.init();
|
2022-04-20 10:41:27 +00:00
|
|
|
this.dbPath = dbPath;
|
2022-04-18 06:42:56 +00:00
|
|
|
return countryCode;
|
2022-03-22 09:28:36 +00:00
|
|
|
}
|
|
|
|
|
2022-03-31 09:04:30 +00:00
|
|
|
async connectToDatabase(dbPath: string, countryCode?: string) {
|
2022-04-18 06:42:56 +00:00
|
|
|
countryCode = await this.#demux.connectToDatabase(dbPath, countryCode);
|
|
|
|
await this.init();
|
2022-04-20 10:41:27 +00:00
|
|
|
this.dbPath = dbPath;
|
2022-04-18 06:42:56 +00:00
|
|
|
return countryCode;
|
2022-03-22 09:28:36 +00:00
|
|
|
}
|
2022-03-31 09:04:30 +00:00
|
|
|
|
2022-03-31 12:04:30 +00:00
|
|
|
async init() {
|
|
|
|
this.schemaMap = (await this.#demux.getSchemaMap()) as Readonly<SchemaMap>;
|
2022-04-18 05:07:36 +00:00
|
|
|
|
|
|
|
for (const schemaName in this.schemaMap) {
|
|
|
|
const fields = this.schemaMap[schemaName]!.fields!;
|
|
|
|
this.fieldValueMap[schemaName] = getMapFromList(fields, 'fieldname');
|
|
|
|
}
|
2022-05-04 08:19:40 +00:00
|
|
|
this.observer = new Observable();
|
2022-03-31 09:04:30 +00:00
|
|
|
}
|
|
|
|
|
2022-04-22 11:02:03 +00:00
|
|
|
purgeCache() {
|
|
|
|
this.dbPath = undefined;
|
|
|
|
this.schemaMap = {};
|
|
|
|
this.fieldValueMap = {};
|
|
|
|
}
|
|
|
|
|
2022-03-31 09:04:30 +00:00
|
|
|
async insert(
|
|
|
|
schemaName: string,
|
|
|
|
docValueMap: DocValueMap
|
|
|
|
): Promise<DocValueMap> {
|
2022-04-18 05:07:36 +00:00
|
|
|
let rawValueMap = this.converter.toRawValueMap(
|
2022-03-31 09:04:30 +00:00
|
|
|
schemaName,
|
|
|
|
docValueMap
|
|
|
|
) as RawValueMap;
|
|
|
|
rawValueMap = (await this.#demux.call(
|
|
|
|
'insert',
|
|
|
|
schemaName,
|
|
|
|
rawValueMap
|
|
|
|
)) as RawValueMap;
|
2022-05-04 08:19:40 +00:00
|
|
|
this.observer.trigger(`insert:${schemaName}`, docValueMap);
|
2022-04-18 05:07:36 +00:00
|
|
|
return this.converter.toDocValueMap(schemaName, rawValueMap) as DocValueMap;
|
2022-03-31 09:04:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Read
|
|
|
|
async get(
|
|
|
|
schemaName: string,
|
|
|
|
name: string,
|
|
|
|
fields?: string | string[]
|
|
|
|
): Promise<DocValueMap> {
|
|
|
|
const rawValueMap = (await this.#demux.call(
|
|
|
|
'get',
|
|
|
|
schemaName,
|
|
|
|
name,
|
|
|
|
fields
|
|
|
|
)) as RawValueMap;
|
2022-05-04 08:19:40 +00:00
|
|
|
this.observer.trigger(`get:${schemaName}`, { name, fields });
|
2022-04-18 05:07:36 +00:00
|
|
|
return this.converter.toDocValueMap(schemaName, rawValueMap) as DocValueMap;
|
2022-03-31 09:04:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async getAll(
|
|
|
|
schemaName: string,
|
2022-04-08 06:10:04 +00:00
|
|
|
options: GetAllOptions = {}
|
2022-03-31 09:04:30 +00:00
|
|
|
): Promise<DocValueMap[]> {
|
2022-04-11 06:04:55 +00:00
|
|
|
const rawValueMap = await this.#getAll(schemaName, options);
|
2022-05-04 08:19:40 +00:00
|
|
|
this.observer.trigger(`getAll:${schemaName}`, options);
|
2022-04-18 05:07:36 +00:00
|
|
|
return this.converter.toDocValueMap(
|
|
|
|
schemaName,
|
|
|
|
rawValueMap
|
|
|
|
) as DocValueMap[];
|
2022-04-11 06:04:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async getAllRaw(
|
|
|
|
schemaName: string,
|
|
|
|
options: GetAllOptions = {}
|
|
|
|
): Promise<DocValueMap[]> {
|
2022-05-04 08:19:40 +00:00
|
|
|
const all = await this.#getAll(schemaName, options);
|
|
|
|
this.observer.trigger(`getAllRaw:${schemaName}`, options);
|
|
|
|
return all;
|
2022-04-11 06:04:55 +00:00
|
|
|
}
|
|
|
|
|
2022-03-31 09:04:30 +00:00
|
|
|
async getSingleValues(
|
|
|
|
...fieldnames: ({ fieldname: string; parent?: string } | string)[]
|
2022-03-31 11:43:20 +00:00
|
|
|
): Promise<SingleValue<DocValue>> {
|
|
|
|
const rawSingleValue = (await this.#demux.call(
|
|
|
|
'getSingleValues',
|
|
|
|
...fieldnames
|
|
|
|
)) as SingleValue<RawValue>;
|
2022-03-31 12:04:30 +00:00
|
|
|
|
2022-04-18 05:07:36 +00:00
|
|
|
const docSingleValue: SingleValue<DocValue> = [];
|
|
|
|
for (const sv of rawSingleValue) {
|
2022-04-23 09:23:44 +00:00
|
|
|
const field = this.fieldValueMap[sv.parent][sv.fieldname];
|
|
|
|
const value = Converter.toDocValue(sv.value, field, this.#fyo);
|
2022-04-18 05:07:36 +00:00
|
|
|
|
|
|
|
docSingleValue.push({
|
|
|
|
value,
|
|
|
|
parent: sv.parent,
|
|
|
|
fieldname: sv.fieldname,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-05-04 08:19:40 +00:00
|
|
|
this.observer.trigger(`getSingleValues`, fieldnames);
|
2022-04-18 05:07:36 +00:00
|
|
|
return docSingleValue;
|
|
|
|
}
|
|
|
|
|
|
|
|
async count(
|
|
|
|
schemaName: string,
|
|
|
|
options: GetAllOptions = {}
|
|
|
|
): Promise<number> {
|
|
|
|
const rawValueMap = await this.#getAll(schemaName, options);
|
2022-05-04 08:19:40 +00:00
|
|
|
const count = rawValueMap.length;
|
|
|
|
this.observer.trigger(`count:${schemaName}`, options);
|
|
|
|
return count;
|
2022-03-31 09:04:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Update
|
|
|
|
async rename(
|
|
|
|
schemaName: string,
|
|
|
|
oldName: string,
|
|
|
|
newName: string
|
|
|
|
): Promise<void> {
|
|
|
|
await this.#demux.call('rename', schemaName, oldName, newName);
|
2022-05-04 08:19:40 +00:00
|
|
|
this.observer.trigger(`rename:${schemaName}`, { oldName, newName });
|
2022-03-31 09:04:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async update(schemaName: string, docValueMap: DocValueMap): Promise<void> {
|
2022-04-18 05:07:36 +00:00
|
|
|
const rawValueMap = this.converter.toRawValueMap(schemaName, docValueMap);
|
2022-03-31 09:04:30 +00:00
|
|
|
await this.#demux.call('update', schemaName, rawValueMap);
|
2022-05-04 08:19:40 +00:00
|
|
|
this.observer.trigger(`update:${schemaName}`, docValueMap);
|
2022-03-31 09:04:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Delete
|
|
|
|
async delete(schemaName: string, name: string): Promise<void> {
|
|
|
|
await this.#demux.call('delete', schemaName, name);
|
2022-05-04 08:19:40 +00:00
|
|
|
this.observer.trigger(`delete:${schemaName}`, name);
|
2022-03-31 09:04:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Other
|
2022-05-04 08:19:40 +00:00
|
|
|
async exists(schemaName: string, name?: string): Promise<boolean> {
|
|
|
|
const doesExist = (await this.#demux.call(
|
|
|
|
'exists',
|
|
|
|
schemaName,
|
|
|
|
name
|
|
|
|
)) as boolean;
|
|
|
|
this.observer.trigger(`exists:${schemaName}`, name);
|
|
|
|
return doesExist;
|
|
|
|
}
|
|
|
|
|
2022-03-31 09:04:30 +00:00
|
|
|
async close(): Promise<void> {
|
|
|
|
await this.#demux.call('close');
|
2022-04-22 11:02:03 +00:00
|
|
|
this.purgeCache();
|
2022-03-31 09:04:30 +00:00
|
|
|
}
|
|
|
|
|
2022-04-11 06:04:55 +00:00
|
|
|
/**
|
|
|
|
* Bespoke function
|
|
|
|
*
|
|
|
|
* These are functions to run custom queries that are too complex for
|
2022-04-18 05:07:36 +00:00
|
|
|
* DatabaseCore and require use of knex or raw queries. The output
|
|
|
|
* of these is not converted to DocValue and is used as is (RawValue).
|
2022-04-11 06:04:55 +00:00
|
|
|
*
|
|
|
|
* The query logic for these is in backend/database/bespoke.ts
|
|
|
|
*/
|
2022-05-11 10:44:31 +00:00
|
|
|
|
|
|
|
async getLastInserted(schemaName: string): Promise<number> {
|
|
|
|
if (this.schemaMap[schemaName]?.naming !== 'autoincrement') {
|
|
|
|
throw new ValueError(
|
|
|
|
`invalid schema, ${schemaName} does not have autoincrement naming`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (await this.#demux.callBespoke(
|
|
|
|
'getLastInserted',
|
|
|
|
schemaName
|
|
|
|
)) as number;
|
|
|
|
}
|
|
|
|
|
2022-04-11 06:04:55 +00:00
|
|
|
async getTopExpenses(fromDate: string, toDate: string): Promise<TopExpenses> {
|
|
|
|
return (await this.#demux.callBespoke(
|
|
|
|
'getTopExpenses',
|
|
|
|
fromDate,
|
|
|
|
toDate
|
|
|
|
)) as TopExpenses;
|
|
|
|
}
|
|
|
|
|
|
|
|
async getTotalOutstanding(
|
|
|
|
schemaName: string,
|
|
|
|
fromDate: string,
|
|
|
|
toDate: string
|
|
|
|
): Promise<TotalOutstanding> {
|
|
|
|
return (await this.#demux.callBespoke(
|
|
|
|
'getTotalOutstanding',
|
|
|
|
schemaName,
|
|
|
|
fromDate,
|
|
|
|
toDate
|
|
|
|
)) as TotalOutstanding;
|
|
|
|
}
|
|
|
|
|
|
|
|
async getCashflow(fromDate: string, toDate: string): Promise<Cashflow> {
|
|
|
|
return (await this.#demux.callBespoke(
|
|
|
|
'getCashflow',
|
|
|
|
fromDate,
|
|
|
|
toDate
|
|
|
|
)) as Cashflow;
|
|
|
|
}
|
|
|
|
|
2022-04-18 05:07:36 +00:00
|
|
|
/**
|
|
|
|
* Internal methods
|
|
|
|
*/
|
|
|
|
async #getAll(
|
2022-03-31 09:04:30 +00:00
|
|
|
schemaName: string,
|
2022-04-18 05:07:36 +00:00
|
|
|
options: GetAllOptions = {}
|
|
|
|
): Promise<RawValueMap[]> {
|
|
|
|
return (await this.#demux.call(
|
|
|
|
'getAll',
|
|
|
|
schemaName,
|
|
|
|
options
|
|
|
|
)) as RawValueMap[];
|
2022-03-31 11:43:20 +00:00
|
|
|
}
|
2022-03-22 09:28:36 +00:00
|
|
|
}
|