mirror of
https://github.com/frappe/books.git
synced 2024-11-08 14:50:56 +00:00
incr: database demux base
- remove a few redundant files
This commit is contained in:
parent
0efd2b4c33
commit
a66e2bcc45
@ -1,5 +1,5 @@
|
||||
import fs from 'fs/promises';
|
||||
import { DatabaseMethod } from 'utils/db/types';
|
||||
import { DatabaseDemuxBase, DatabaseMethod } from 'utils/db/types';
|
||||
import { getSchemas } from '../../schemas';
|
||||
import { databaseMethodSet } from '../helpers';
|
||||
import patches from '../patches';
|
||||
@ -7,7 +7,7 @@ import DatabaseCore from './core';
|
||||
import { runPatches } from './runPatch';
|
||||
import { Patch } from './types';
|
||||
|
||||
export class DatabaseManager {
|
||||
export class DatabaseManager extends DatabaseDemuxBase {
|
||||
db?: DatabaseCore;
|
||||
|
||||
get #isInitialized(): boolean {
|
||||
|
@ -2,19 +2,30 @@ import { DatabaseDemux } from '@/demux/db';
|
||||
import { Frappe } from 'frappe';
|
||||
import Money from 'pesa/dist/types/src/money';
|
||||
import { FieldType, FieldTypeEnum, RawValue, SchemaMap } from 'schemas/types';
|
||||
import { DatabaseBase, GetAllOptions } from 'utils/db/types';
|
||||
import { DocValue, DocValueMap, RawValueMap, SingleValue } from './types';
|
||||
import { DatabaseBase, DatabaseDemuxBase, GetAllOptions } from 'utils/db/types';
|
||||
import {
|
||||
DatabaseDemuxConstructor,
|
||||
DocValue,
|
||||
DocValueMap,
|
||||
RawValueMap,
|
||||
SingleValue,
|
||||
} from './types';
|
||||
|
||||
export class DatabaseHandler extends DatabaseBase {
|
||||
#frappe: Frappe;
|
||||
#demux: DatabaseDemux;
|
||||
#demux: DatabaseDemuxBase;
|
||||
schemaMap: Readonly<SchemaMap> = {};
|
||||
|
||||
constructor(frappe: Frappe) {
|
||||
constructor(frappe: Frappe, Demux?: DatabaseDemuxConstructor) {
|
||||
super();
|
||||
this.#frappe = frappe;
|
||||
|
||||
if (Demux !== undefined) {
|
||||
this.#demux = new Demux(frappe.isElectron);
|
||||
} else {
|
||||
this.#demux = new DatabaseDemux(frappe.isElectron);
|
||||
}
|
||||
}
|
||||
|
||||
async createNewDatabase(dbPath: string, countryCode?: string) {
|
||||
await this.#demux.createNewDatabase(dbPath, countryCode);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import Doc from 'frappe/model/doc';
|
||||
import Money from 'pesa/dist/types/src/money';
|
||||
import { RawValue } from 'schemas/types';
|
||||
import { DatabaseDemuxBase } from 'utils/db/types';
|
||||
|
||||
export type DocValue = string | number | boolean | Date | Money | null;
|
||||
export type DocValueMap = Record<string, DocValue | Doc[] | DocValueMap[]>;
|
||||
@ -11,3 +12,10 @@ export type SingleValue<T> = {
|
||||
parent: string;
|
||||
value: T;
|
||||
}[];
|
||||
|
||||
/**
|
||||
* DatabaseDemuxConstructor: type for a constructor that returns a DatabaseDemuxBase
|
||||
* it's typed this way because `typeof AbstractClass` is invalid as abstract classes
|
||||
* can't be initialized using `new`.
|
||||
*/
|
||||
export type DatabaseDemuxConstructor = new (isElectron?: boolean)=> DatabaseDemuxBase
|
@ -4,6 +4,7 @@ import { markRaw } from 'vue';
|
||||
import { AuthHandler } from './core/authHandler';
|
||||
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 {
|
||||
@ -42,9 +43,13 @@ export class Frappe {
|
||||
methods?: Record<string, Function>;
|
||||
temp?: Record<string, unknown>;
|
||||
|
||||
constructor() {
|
||||
constructor(DatabaseDemux?: DatabaseDemuxConstructor) {
|
||||
/**
|
||||
* `DatabaseManager` can be passed as the `DatabaseDemux` for
|
||||
* testing this class without API or IPC calls.
|
||||
*/
|
||||
this.auth = new AuthHandler(this);
|
||||
this.db = new DatabaseHandler(this);
|
||||
this.db = new DatabaseHandler(this, DatabaseDemux);
|
||||
this.doc = new DocHandler(this);
|
||||
|
||||
this.pesa = getMoneyMaker({
|
||||
@ -81,7 +86,7 @@ export class Frappe {
|
||||
this.doc.registerModels(customModels);
|
||||
}
|
||||
|
||||
async init(force: boolean) {
|
||||
async init(force?: boolean) {
|
||||
if (this._initialized && !force) return;
|
||||
|
||||
this.methods = {};
|
||||
|
@ -1,762 +0,0 @@
|
||||
import telemetry from '@/telemetry/telemetry';
|
||||
import { Verb } from '@/telemetry/types';
|
||||
import frappe from 'frappe';
|
||||
import Observable from 'frappe/utils/observable';
|
||||
import { DEFAULT_INTERNAL_PRECISION } from '../utils/consts';
|
||||
import { getRandomString, isPesa } from '../utils/index';
|
||||
import { setName } from './naming';
|
||||
|
||||
export default class Document extends Observable {
|
||||
constructor(data) {
|
||||
super();
|
||||
this.fetchValuesCache = {};
|
||||
this.flags = {};
|
||||
this.setup();
|
||||
this.setValues(data);
|
||||
}
|
||||
|
||||
setup() {
|
||||
// add listeners
|
||||
}
|
||||
|
||||
setValues(data) {
|
||||
for (let fieldname in data) {
|
||||
let value = data[fieldname];
|
||||
if (fieldname.startsWith('_')) {
|
||||
// private property
|
||||
this[fieldname] = value;
|
||||
} else if (Array.isArray(value)) {
|
||||
for (let row of value) {
|
||||
this.push(fieldname, row);
|
||||
}
|
||||
} else {
|
||||
this[fieldname] = value;
|
||||
}
|
||||
}
|
||||
// set unset fields as null
|
||||
for (let field of this.meta.getValidFields()) {
|
||||
// check for null or undefined
|
||||
if (this[field.fieldname] == null) {
|
||||
this[field.fieldname] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get meta() {
|
||||
if (this.isCustom) {
|
||||
this._meta = frappe.createMeta(this.fields);
|
||||
}
|
||||
if (!this._meta) {
|
||||
this._meta = frappe.getMeta(this.doctype);
|
||||
}
|
||||
return this._meta;
|
||||
}
|
||||
|
||||
async getSettings() {
|
||||
if (!this._settings) {
|
||||
this._settings = await frappe.getSingle(this.meta.settings);
|
||||
}
|
||||
return this._settings;
|
||||
}
|
||||
|
||||
// set value and trigger change
|
||||
async set(fieldname, value) {
|
||||
if (typeof fieldname === 'object') {
|
||||
const valueDict = fieldname;
|
||||
for (let fieldname in valueDict) {
|
||||
await this.set(fieldname, valueDict[fieldname]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (fieldname === 'numberSeries' && !this._notInserted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this[fieldname] !== value) {
|
||||
this._dirty = true;
|
||||
// if child is dirty, parent is dirty too
|
||||
if (this.meta.isChild && this.parentdoc) {
|
||||
this.parentdoc._dirty = true;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
this[fieldname] = [];
|
||||
value.forEach((row, i) => {
|
||||
this.append(fieldname, row);
|
||||
row.idx = i;
|
||||
});
|
||||
} else {
|
||||
await this.validateField(fieldname, value);
|
||||
this[fieldname] = value;
|
||||
}
|
||||
|
||||
// always run applyChange from the parentdoc
|
||||
if (this.meta.isChild && this.parentdoc) {
|
||||
await this.applyChange(fieldname);
|
||||
await this.parentdoc.applyChange(this.parentfield);
|
||||
} else {
|
||||
await this.applyChange(fieldname);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async applyChange(fieldname) {
|
||||
await this.applyFormula(fieldname);
|
||||
this.roundFloats();
|
||||
await this.trigger('change', {
|
||||
doc: this,
|
||||
changed: fieldname,
|
||||
});
|
||||
}
|
||||
|
||||
setDefaults() {
|
||||
for (let field of this.meta.fields) {
|
||||
if (this[field.fieldname] == null) {
|
||||
let defaultValue = getPreDefaultValues(field.fieldtype);
|
||||
|
||||
if (typeof field.default === 'function') {
|
||||
defaultValue = field.default(this);
|
||||
} else if (field.default !== undefined) {
|
||||
defaultValue = field.default;
|
||||
}
|
||||
|
||||
if (field.fieldtype === 'Currency' && !isPesa(defaultValue)) {
|
||||
defaultValue = frappe.pesa(defaultValue);
|
||||
}
|
||||
|
||||
this[field.fieldname] = defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.meta.basedOn && this.meta.filters) {
|
||||
this.setValues(this.meta.filters);
|
||||
}
|
||||
}
|
||||
|
||||
castValues() {
|
||||
for (let field of this.meta.fields) {
|
||||
let value = this[field.fieldname];
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
if (['Int', 'Check'].includes(field.fieldtype)) {
|
||||
value = parseInt(value, 10);
|
||||
} else if (field.fieldtype === 'Float') {
|
||||
value = parseFloat(value);
|
||||
} else if (field.fieldtype === 'Currency' && !isPesa(value)) {
|
||||
value = frappe.pesa(value);
|
||||
}
|
||||
this[field.fieldname] = value;
|
||||
}
|
||||
}
|
||||
|
||||
setKeywords() {
|
||||
let keywords = [];
|
||||
for (let fieldname of this.meta.getKeywordFields()) {
|
||||
keywords.push(this[fieldname]);
|
||||
}
|
||||
this.keywords = keywords.join(', ');
|
||||
}
|
||||
|
||||
append(key, document = {}) {
|
||||
// push child row and trigger change
|
||||
this.push(key, document);
|
||||
this._dirty = true;
|
||||
this.applyChange(key);
|
||||
}
|
||||
|
||||
push(key, document = {}) {
|
||||
// push child row without triggering change
|
||||
if (!this[key]) {
|
||||
this[key] = [];
|
||||
}
|
||||
this[key].push(this._initChild(document, key));
|
||||
}
|
||||
|
||||
_initChild(data, key) {
|
||||
if (data instanceof Document) {
|
||||
return data;
|
||||
}
|
||||
|
||||
data.doctype = this.meta.getField(key).childtype;
|
||||
data.parent = this.name;
|
||||
data.parenttype = this.doctype;
|
||||
data.parentfield = key;
|
||||
data.parentdoc = this;
|
||||
|
||||
if (!data.idx) {
|
||||
data.idx = (this[key] || []).length;
|
||||
}
|
||||
|
||||
if (!data.name) {
|
||||
data.name = getRandomString();
|
||||
}
|
||||
|
||||
const childDoc = new Document(data);
|
||||
childDoc.setDefaults();
|
||||
return childDoc;
|
||||
}
|
||||
|
||||
async validateInsert() {
|
||||
this.validateMandatory();
|
||||
await this.validateFields();
|
||||
}
|
||||
|
||||
validateMandatory() {
|
||||
let checkForMandatory = [this];
|
||||
let tableFields = this.meta.fields.filter((df) => df.fieldtype === 'Table');
|
||||
tableFields.map((df) => {
|
||||
let rows = this[df.fieldname];
|
||||
checkForMandatory = [...checkForMandatory, ...rows];
|
||||
});
|
||||
|
||||
let missingMandatory = checkForMandatory
|
||||
.map((doc) => getMissingMandatory(doc))
|
||||
.filter(Boolean);
|
||||
|
||||
if (missingMandatory.length > 0) {
|
||||
let fields = missingMandatory.join('\n');
|
||||
let message = frappe.t`Value missing for ${fields}`;
|
||||
throw new frappe.errors.MandatoryError(message);
|
||||
}
|
||||
|
||||
function getMissingMandatory(doc) {
|
||||
let mandatoryFields = doc.meta.fields.filter((df) => {
|
||||
if (df.required instanceof Function) {
|
||||
return df.required(doc);
|
||||
}
|
||||
return df.required;
|
||||
});
|
||||
let message = mandatoryFields
|
||||
.filter((df) => {
|
||||
let value = doc[df.fieldname];
|
||||
if (df.fieldtype === 'Table') {
|
||||
return value == null || value.length === 0;
|
||||
}
|
||||
return value == null || value === '';
|
||||
})
|
||||
.map((df) => {
|
||||
return `"${df.label}"`;
|
||||
})
|
||||
.join(', ');
|
||||
|
||||
if (message && doc.meta.isChild) {
|
||||
let parentfield = doc.parentdoc.meta.getField(doc.parentfield);
|
||||
message = `${parentfield.label} Row ${doc.idx + 1}: ${message}`;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
async validateFields() {
|
||||
let fields = this.meta.fields;
|
||||
for (let field of fields) {
|
||||
await this.validateField(field.fieldname, this.get(field.fieldname));
|
||||
}
|
||||
}
|
||||
|
||||
async validateField(key, value) {
|
||||
let field = this.meta.getField(key);
|
||||
if (!field) {
|
||||
throw new frappe.errors.InvalidFieldError(`Invalid field ${key}`);
|
||||
}
|
||||
if (field.fieldtype == 'Select') {
|
||||
this.meta.validateSelect(field, value);
|
||||
}
|
||||
if (field.validate && value != null) {
|
||||
let validator = null;
|
||||
if (typeof field.validate === 'object') {
|
||||
validator = this.getValidateFunction(field.validate);
|
||||
}
|
||||
if (typeof field.validate === 'function') {
|
||||
validator = field.validate;
|
||||
}
|
||||
if (validator) {
|
||||
await validator(value, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getValidateFunction(validator) {
|
||||
let functions = {
|
||||
email(value) {
|
||||
let isValid = /(.+)@(.+){2,}\.(.+){2,}/.test(value);
|
||||
if (!isValid) {
|
||||
throw new frappe.errors.ValidationError(`Invalid email: ${value}`);
|
||||
}
|
||||
},
|
||||
phone(value) {
|
||||
let isValid = /[+]{0,1}[\d ]+/.test(value);
|
||||
if (!isValid) {
|
||||
throw new frappe.errors.ValidationError(`Invalid phone: ${value}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return functions[validator.type];
|
||||
}
|
||||
|
||||
getValidDict() {
|
||||
let data = {};
|
||||
for (let field of this.meta.getValidFields()) {
|
||||
let value = this[field.fieldname];
|
||||
if (Array.isArray(value)) {
|
||||
value = value.map((doc) =>
|
||||
doc.getValidDict ? doc.getValidDict() : doc
|
||||
);
|
||||
}
|
||||
data[field.fieldname] = value;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
setStandardValues() {
|
||||
// set standard values on server-side only
|
||||
if (frappe.isServer) {
|
||||
if (this.isSubmittable && this.submitted == null) {
|
||||
this.submitted = 0;
|
||||
}
|
||||
|
||||
let now = new Date().toISOString();
|
||||
if (!this.owner) {
|
||||
this.owner = frappe.auth.session.user;
|
||||
}
|
||||
|
||||
if (!this.creation) {
|
||||
this.creation = now;
|
||||
}
|
||||
|
||||
this.updateModified();
|
||||
}
|
||||
}
|
||||
|
||||
updateModified() {
|
||||
if (frappe.isServer) {
|
||||
let now = new Date().toISOString();
|
||||
this.modifiedBy = frappe.auth.session.user;
|
||||
this.modified = now;
|
||||
}
|
||||
}
|
||||
|
||||
async load() {
|
||||
let data = await frappe.db.get(this.doctype, this.name);
|
||||
if (data && data.name) {
|
||||
this.syncValues(data);
|
||||
if (this.meta.isSingle) {
|
||||
this.setDefaults();
|
||||
this.castValues();
|
||||
}
|
||||
await this.loadLinks();
|
||||
} else {
|
||||
throw new frappe.errors.NotFoundError(
|
||||
`Not Found: ${this.doctype} ${this.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async loadLinks() {
|
||||
this._links = {};
|
||||
let inlineLinks = this.meta.fields.filter((df) => df.inline);
|
||||
for (let df of inlineLinks) {
|
||||
await this.loadLink(df.fieldname);
|
||||
}
|
||||
}
|
||||
|
||||
async loadLink(fieldname) {
|
||||
this._links = this._links || {};
|
||||
let df = this.meta.getField(fieldname);
|
||||
if (this[df.fieldname]) {
|
||||
this._links[df.fieldname] = await frappe.getDoc(
|
||||
df.target,
|
||||
this[df.fieldname]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getLink(fieldname) {
|
||||
return this._links ? this._links[fieldname] : null;
|
||||
}
|
||||
|
||||
syncValues(data) {
|
||||
this.clearValues();
|
||||
this.setValues(data);
|
||||
this._dirty = false;
|
||||
this.trigger('change', {
|
||||
doc: this,
|
||||
});
|
||||
}
|
||||
|
||||
clearValues() {
|
||||
let toClear = ['_dirty', '_notInserted'].concat(
|
||||
this.meta.getValidFields().map((df) => df.fieldname)
|
||||
);
|
||||
for (let key of toClear) {
|
||||
this[key] = null;
|
||||
}
|
||||
}
|
||||
|
||||
setChildIdx() {
|
||||
// renumber children
|
||||
for (let field of this.meta.getValidFields()) {
|
||||
if (field.fieldtype === 'Table') {
|
||||
for (let i = 0; i < (this[field.fieldname] || []).length; i++) {
|
||||
this[field.fieldname][i].idx = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async compareWithCurrentDoc() {
|
||||
if (frappe.isServer && !this.isNew()) {
|
||||
let currentDoc = await frappe.db.get(this.doctype, this.name);
|
||||
|
||||
// check for conflict
|
||||
if (currentDoc && this.modified != currentDoc.modified) {
|
||||
throw new frappe.errors.Conflict(
|
||||
frappe.t`Document ${this.doctype} ${this.name} has been modified after loading`
|
||||
);
|
||||
}
|
||||
|
||||
if (this.submitted && !this.meta.isSubmittable) {
|
||||
throw new frappe.errors.ValidationError(
|
||||
frappe.t`Document type ${this.doctype} is not submittable`
|
||||
);
|
||||
}
|
||||
|
||||
// set submit action flag
|
||||
this.flags = {};
|
||||
if (this.submitted && !currentDoc.submitted) {
|
||||
this.flags.submitAction = true;
|
||||
}
|
||||
|
||||
if (currentDoc.submitted && !this.submitted) {
|
||||
this.flags.revertAction = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async applyFormula(fieldname) {
|
||||
if (!this.meta.hasFormula()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let doc = this;
|
||||
let changed = false;
|
||||
|
||||
// children
|
||||
for (let tablefield of this.meta.getTableFields()) {
|
||||
let formulaFields = frappe
|
||||
.getMeta(tablefield.childtype)
|
||||
.getFormulaFields();
|
||||
if (formulaFields.length) {
|
||||
const value = this[tablefield.fieldname] || [];
|
||||
for (let row of value) {
|
||||
for (let field of formulaFields) {
|
||||
if (shouldApplyFormula(field, row)) {
|
||||
let val = await this.getValueFromFormula(field, row);
|
||||
let previousVal = row[field.fieldname];
|
||||
if (val !== undefined && previousVal !== val) {
|
||||
row[field.fieldname] = val;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parent or child row
|
||||
for (let field of this.meta.getFormulaFields()) {
|
||||
if (shouldApplyFormula(field, doc)) {
|
||||
let previousVal = doc[field.fieldname];
|
||||
let val = await this.getValueFromFormula(field, doc);
|
||||
if (val !== undefined && previousVal !== val) {
|
||||
doc[field.fieldname] = val;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
|
||||
function shouldApplyFormula(field, doc) {
|
||||
if (field.readOnly) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
fieldname &&
|
||||
field.formulaDependsOn &&
|
||||
field.formulaDependsOn.includes(fieldname)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!frappe.isServer || frappe.isElectron) {
|
||||
if (doc[field.fieldname] == null || doc[field.fieldname] == '') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getValueFromFormula(field, doc) {
|
||||
let value;
|
||||
|
||||
if (doc.meta.isChild) {
|
||||
value = await field.formula(doc, doc.parentdoc);
|
||||
} else {
|
||||
value = await field.formula(doc);
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ('Float' === field.fieldtype) {
|
||||
value = this.round(value, field);
|
||||
}
|
||||
|
||||
if (field.fieldtype === 'Table' && Array.isArray(value)) {
|
||||
value = value.map((row) => {
|
||||
let doc = this._initChild(row, field.fieldname);
|
||||
doc.roundFloats();
|
||||
return doc;
|
||||
});
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
roundFloats() {
|
||||
let fields = this.meta
|
||||
.getValidFields()
|
||||
.filter((df) => ['Float', 'Table'].includes(df.fieldtype));
|
||||
|
||||
for (let df of fields) {
|
||||
let value = this[df.fieldname];
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
// child
|
||||
if (Array.isArray(value)) {
|
||||
value.map((row) => row.roundFloats());
|
||||
continue;
|
||||
}
|
||||
// field
|
||||
let roundedValue = this.round(value, df);
|
||||
if (roundedValue && value !== roundedValue) {
|
||||
this[df.fieldname] = roundedValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async commit() {
|
||||
// re-run triggers
|
||||
this.setKeywords();
|
||||
this.setChildIdx();
|
||||
await this.applyFormula();
|
||||
await this.trigger('validate');
|
||||
}
|
||||
|
||||
async insert() {
|
||||
await setName(this);
|
||||
this.setStandardValues();
|
||||
await this.commit();
|
||||
await this.validateInsert();
|
||||
await this.trigger('beforeInsert');
|
||||
|
||||
let oldName = this.name;
|
||||
const data = await frappe.db.insert(this.doctype, this.getValidDict());
|
||||
this.syncValues(data);
|
||||
|
||||
if (oldName !== this.name) {
|
||||
frappe.removeFromCache(this.doctype, oldName);
|
||||
}
|
||||
|
||||
await this.trigger('afterInsert');
|
||||
await this.trigger('afterSave');
|
||||
|
||||
telemetry.log(Verb.Created, this.doctype);
|
||||
return this;
|
||||
}
|
||||
|
||||
async update(...args) {
|
||||
if (args.length) {
|
||||
await this.set(...args);
|
||||
}
|
||||
await this.compareWithCurrentDoc();
|
||||
await this.commit();
|
||||
await this.trigger('beforeUpdate');
|
||||
|
||||
// before submit
|
||||
if (this.flags.submitAction) await this.trigger('beforeSubmit');
|
||||
if (this.flags.revertAction) await this.trigger('beforeRevert');
|
||||
|
||||
// update modifiedBy and modified
|
||||
this.updateModified();
|
||||
|
||||
const data = await frappe.db.update(this.doctype, this.getValidDict());
|
||||
this.syncValues(data);
|
||||
|
||||
await this.trigger('afterUpdate');
|
||||
await this.trigger('afterSave');
|
||||
|
||||
// after submit
|
||||
if (this.flags.submitAction) await this.trigger('afterSubmit');
|
||||
if (this.flags.revertAction) await this.trigger('afterRevert');
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
async insertOrUpdate() {
|
||||
if (this._notInserted) {
|
||||
return await this.insert();
|
||||
} else {
|
||||
return await this.update();
|
||||
}
|
||||
}
|
||||
|
||||
async delete() {
|
||||
await this.trigger('beforeDelete');
|
||||
await frappe.db.delete(this.doctype, this.name);
|
||||
await this.trigger('afterDelete');
|
||||
|
||||
telemetry.log(Verb.Deleted, this.doctype);
|
||||
}
|
||||
|
||||
async submitOrRevert(isSubmit) {
|
||||
const wasSubmitted = this.submitted;
|
||||
this.submitted = isSubmit;
|
||||
try {
|
||||
await this.update();
|
||||
} catch (e) {
|
||||
this.submitted = wasSubmitted;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async submit() {
|
||||
this.cancelled = 0;
|
||||
await this.submitOrRevert(1);
|
||||
}
|
||||
|
||||
async revert() {
|
||||
await this.submitOrRevert(0);
|
||||
}
|
||||
|
||||
async rename(newName) {
|
||||
await this.trigger('beforeRename');
|
||||
await frappe.db.rename(this.doctype, this.name, newName);
|
||||
this.name = newName;
|
||||
await this.trigger('afterRename');
|
||||
}
|
||||
|
||||
// trigger methods on the class if they match
|
||||
// with the trigger name
|
||||
async trigger(event, params) {
|
||||
if (this[event]) {
|
||||
await this[event](params);
|
||||
}
|
||||
await super.trigger(event, params);
|
||||
}
|
||||
|
||||
// helper functions
|
||||
getSum(tablefield, childfield, convertToFloat = true) {
|
||||
const sum = (this[tablefield] || [])
|
||||
.map((d) => {
|
||||
const value = d[childfield] ?? 0;
|
||||
if (!isPesa(value)) {
|
||||
try {
|
||||
return frappe.pesa(value);
|
||||
} catch (err) {
|
||||
err.message += ` value: '${value}' of type: ${typeof value}, fieldname: '${tablefield}', childfield: '${childfield}'`;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
})
|
||||
.reduce((a, b) => a.add(b), frappe.pesa(0));
|
||||
|
||||
if (convertToFloat) {
|
||||
return sum.float;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
getFrom(doctype, name, fieldname) {
|
||||
if (!name) return '';
|
||||
return frappe.db.getCachedValue(doctype, name, fieldname);
|
||||
}
|
||||
|
||||
round(value, df = null) {
|
||||
if (typeof df === 'string') {
|
||||
df = this.meta.getField(df);
|
||||
}
|
||||
const precision =
|
||||
frappe.SystemSettings.internalPrecision ?? DEFAULT_INTERNAL_PRECISION;
|
||||
return frappe.pesa(value).clip(precision).float;
|
||||
}
|
||||
|
||||
isNew() {
|
||||
return this._notInserted;
|
||||
}
|
||||
|
||||
getFieldMetaMap() {
|
||||
return this.meta.fields.reduce((obj, meta) => {
|
||||
obj[meta.fieldname] = meta;
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
|
||||
async duplicate() {
|
||||
const updateMap = {};
|
||||
const fieldValueMap = this.getValidDict();
|
||||
const keys = this.meta.fields.map((f) => f.fieldname);
|
||||
for (const key of keys) {
|
||||
let value = fieldValueMap[key];
|
||||
if (!value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isPesa(value)) {
|
||||
value = value.copy();
|
||||
}
|
||||
|
||||
if (value instanceof Array) {
|
||||
value.forEach((row) => {
|
||||
delete row.name;
|
||||
delete row.parent;
|
||||
});
|
||||
}
|
||||
|
||||
updateMap[key] = value;
|
||||
}
|
||||
|
||||
if (this.numberSeries) {
|
||||
delete updateMap.name;
|
||||
} else {
|
||||
updateMap.name = updateMap.name + ' CPY';
|
||||
}
|
||||
|
||||
const doc = frappe.getEmptyDoc(this.doctype, false);
|
||||
await doc.set(updateMap);
|
||||
await doc.insert();
|
||||
}
|
||||
}
|
||||
|
||||
function getPreDefaultValues(fieldtype) {
|
||||
switch (fieldtype) {
|
||||
case 'Table':
|
||||
return [];
|
||||
case 'Currency':
|
||||
return frappe.pesa(0.0);
|
||||
case 'Int':
|
||||
case 'Float':
|
||||
return 0;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,12 +1,13 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { SchemaMap } from 'schemas/types';
|
||||
import { DatabaseMethod } from 'utils/db/types';
|
||||
import { DatabaseDemuxBase, DatabaseMethod } from 'utils/db/types';
|
||||
import { IPC_ACTIONS } from 'utils/messages';
|
||||
import { DatabaseResponse } from '../../utils/ipc/types';
|
||||
|
||||
export class DatabaseDemux {
|
||||
export class DatabaseDemux extends DatabaseDemuxBase {
|
||||
#isElectron: boolean = false;
|
||||
constructor(isElectron: boolean) {
|
||||
super();
|
||||
this.#isElectron = isElectron;
|
||||
}
|
||||
|
||||
|
@ -1,127 +0,0 @@
|
||||
import config from '@/config';
|
||||
import SQLiteDatabase from 'frappe/backends/sqlite';
|
||||
import fs from 'fs';
|
||||
import regionalModelUpdates from '../models/regionalModelUpdates';
|
||||
import postStart, { setCurrencySymbols } from '../server/postStart';
|
||||
import { DB_CONN_FAILURE } from '../utils/messages';
|
||||
import runMigrate from './migrate';
|
||||
import { getId } from './telemetry/helpers';
|
||||
import telemetry from './telemetry/telemetry';
|
||||
import { callInitializeMoneyMaker, getSavePath } from './utils';
|
||||
|
||||
export async function createNewDatabase() {
|
||||
const { canceled, filePath } = await getSavePath('books', 'db');
|
||||
if (canceled || filePath.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
async function runRegionalModelUpdates() {
|
||||
if (!(await frappe.db.knex.schema.hasTable('SingleValue'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { country, setupComplete } = await frappe.db.get('AccountingSettings');
|
||||
if (!parseInt(setupComplete)) return;
|
||||
await regionalModelUpdates({ country });
|
||||
}
|
||||
|
||||
export async function connectToLocalDatabase(filePath) {
|
||||
if (!filePath) {
|
||||
return { connectionSuccess: false, reason: DB_CONN_FAILURE.INVALID_FILE };
|
||||
}
|
||||
|
||||
frappe.auth.login('Administrator');
|
||||
try {
|
||||
frappe.db = new SQLiteDatabase({
|
||||
dbPath: filePath,
|
||||
});
|
||||
await frappe.db.connect();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { connectionSuccess: false, reason: DB_CONN_FAILURE.CANT_CONNECT };
|
||||
}
|
||||
|
||||
// first init no currency, for migratory needs
|
||||
await callInitializeMoneyMaker();
|
||||
|
||||
try {
|
||||
await runRegionalModelUpdates();
|
||||
} catch (error) {
|
||||
console.error('regional model updates failed', error);
|
||||
}
|
||||
|
||||
try {
|
||||
await runMigrate();
|
||||
await postStart();
|
||||
} catch (error) {
|
||||
if (!error.message.includes('SQLITE_CANTOPEN')) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
return { connectionSuccess: false, reason: DB_CONN_FAILURE.CANT_OPEN };
|
||||
}
|
||||
|
||||
// set file info in config
|
||||
const { companyName } = frappe.AccountingSettings;
|
||||
let files = config.get('files') || [];
|
||||
if (
|
||||
!files.find(
|
||||
(file) => file.filePath === filePath && file.companyName === companyName
|
||||
)
|
||||
) {
|
||||
files = [
|
||||
{
|
||||
companyName,
|
||||
id: getId(),
|
||||
filePath,
|
||||
},
|
||||
...files.filter((file) => file.filePath !== filePath),
|
||||
];
|
||||
config.set('files', files);
|
||||
}
|
||||
|
||||
// set last selected file
|
||||
config.set('lastSelectedFilePath', filePath);
|
||||
|
||||
// second init with currency, normal usage
|
||||
await callInitializeMoneyMaker();
|
||||
await telemetry.start();
|
||||
|
||||
if (frappe.store.isDevelopment) {
|
||||
// @ts-ignore
|
||||
window.telemetry = telemetry;
|
||||
}
|
||||
|
||||
return { connectionSuccess: true, reason: '' };
|
||||
}
|
||||
|
||||
export async function purgeCache(purgeAll = false) {
|
||||
const filterFunction = purgeAll
|
||||
? () => true
|
||||
: (d) => frappe.docs[d][d] instanceof frappe.Meta;
|
||||
|
||||
Object.keys(frappe.docs)
|
||||
.filter(filterFunction)
|
||||
.forEach((d) => {
|
||||
frappe.removeFromCache(d, d);
|
||||
delete frappe[d];
|
||||
});
|
||||
|
||||
if (purgeAll) {
|
||||
delete frappe.db;
|
||||
const models = (await import('../models')).default;
|
||||
await frappe.initializeAndRegister(models, true);
|
||||
}
|
||||
}
|
||||
|
||||
export async function postSetup() {
|
||||
await setCurrencySymbols();
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
import frappe from 'frappe';
|
||||
import runPatches from 'frappe/model/runPatches';
|
||||
import patches from '../patches/patches.json';
|
||||
|
||||
export default async function runMigrate() {
|
||||
const canRunPatches = await getCanRunPatches();
|
||||
if (!canRunPatches) {
|
||||
return await frappe.db.migrate();
|
||||
}
|
||||
|
||||
const patchList = await fetchPatchList();
|
||||
await runPatches(patchList.filter(({ beforeMigrate }) => beforeMigrate));
|
||||
await frappe.db.migrate();
|
||||
await runPatches(patchList.filter(({ beforeMigrate }) => !beforeMigrate));
|
||||
}
|
||||
|
||||
async function fetchPatchList() {
|
||||
return await Promise.all(
|
||||
patches.map(async ({ version, fileName, beforeMigrate }) => {
|
||||
if (typeof beforeMigrate === 'undefined') {
|
||||
beforeMigrate = true;
|
||||
}
|
||||
|
||||
const patchName = `${version}/${fileName}`;
|
||||
// This import is pseudo dynamic
|
||||
// webpack runs static analysis on the static portion of the import
|
||||
// i.e. '../patches/' this may break on windows due to the path
|
||||
// delimiter used.
|
||||
//
|
||||
// Only way to fix this is probably upgrading the build from
|
||||
// webpack to something else.
|
||||
const patchFunction = (await import('../patches/' + patchName)).default;
|
||||
return { patchName, patchFunction, beforeMigrate };
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function getCanRunPatches() {
|
||||
return (
|
||||
(
|
||||
await frappe.db
|
||||
.knex('sqlite_master')
|
||||
.where({ type: 'table', name: 'PatchRun' })
|
||||
.select('name')
|
||||
).length > 0
|
||||
);
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
export type Map = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export enum FieldType {
|
||||
Data = 'Data',
|
||||
Select = 'Select',
|
||||
Link = 'Link',
|
||||
Date = 'Date',
|
||||
Table = 'Table',
|
||||
AutoComplete = 'AutoComplete',
|
||||
Check = 'Check',
|
||||
AttachImage = 'AttachImage',
|
||||
DynamicLink = 'DynamicLink',
|
||||
Int = 'Int',
|
||||
Float = 'Float',
|
||||
Currency = 'Currency',
|
||||
Text = 'Text',
|
||||
Color = 'Color',
|
||||
}
|
||||
|
||||
export interface Field {
|
||||
fieldname: string;
|
||||
fieldtype: FieldType;
|
||||
label: string;
|
||||
childtype?: string;
|
||||
target?: string;
|
||||
default?: unknown;
|
||||
required?: number;
|
||||
readOnly?: number;
|
||||
hidden?: number | Function;
|
||||
options?: string[];
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface Doc {
|
||||
name: string;
|
||||
set: (fieldname: Map | string, value?: unknown) => Promise<void>;
|
||||
insert: () => Promise<void>;
|
||||
submit: () => Promise<void>;
|
||||
}
|
||||
|
||||
export interface Model {
|
||||
label?: string;
|
||||
name: string;
|
||||
doctype?: string;
|
||||
fields: Field[];
|
||||
isSingle?: number; // boolean
|
||||
regional?: number; // boolean
|
||||
augmented?: number; // boolean
|
||||
keywordFields?: string[];
|
||||
quickEditFields?: string[];
|
||||
}
|
12
tests/testFrappe.spec.ts
Normal file
12
tests/testFrappe.spec.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import 'mocha';
|
||||
import { DatabaseManager } from '../backend/database/manager';
|
||||
import { Frappe } from '../frappe';
|
||||
|
||||
describe('Frappe', function () {
|
||||
const frappe = new Frappe(DatabaseManager);
|
||||
|
||||
specify('Init', async function () {
|
||||
await frappe.init();
|
||||
await frappe.db.connectToDatabase(':memory:');
|
||||
});
|
||||
});
|
@ -6,6 +6,8 @@
|
||||
* match on both ends.
|
||||
*/
|
||||
|
||||
import { SchemaMap } from "schemas/types";
|
||||
|
||||
type UnknownMap = Record<string, unknown>;
|
||||
export abstract class DatabaseBase {
|
||||
// Create
|
||||
@ -61,3 +63,21 @@ export interface GetAllOptions {
|
||||
}
|
||||
|
||||
export type QueryFilter = Record<string, string | string[]>;
|
||||
|
||||
|
||||
/**
|
||||
* DatabaseDemuxBase is an abstract class that ensures that the function signatures
|
||||
* match between the DatabaseManager and the DatabaseDemux.
|
||||
*
|
||||
* This allows testing the frontend code while directly plugging in the DatabaseManager
|
||||
* and bypassing all the API and IPC calls.
|
||||
*/
|
||||
export abstract class DatabaseDemuxBase {
|
||||
abstract getSchemaMap(): Promise<SchemaMap> | SchemaMap
|
||||
|
||||
abstract createNewDatabase(dbPath: string, countryCode?: string): Promise<void>
|
||||
|
||||
abstract connectToDatabase(dbPath: string, countryCode?: string): Promise<void>
|
||||
|
||||
abstract call(method: DatabaseMethod, ...args: unknown[]): Promise<unknown>
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user