2
0
mirror of https://github.com/frappe/books.git synced 2024-11-09 15:20:56 +00:00

incr: database demux base

- remove a few redundant files
This commit is contained in:
18alantom 2022-04-07 19:08:17 +05:30
parent 0efd2b4c33
commit a66e2bcc45
11 changed files with 69 additions and 1001 deletions

View File

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

View File

@ -2,18 +2,29 @@ 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;
this.#demux = new DatabaseDemux(frappe.isElectron);
if (Demux !== undefined) {
this.#demux = new Demux(frappe.isElectron);
} else {
this.#demux = new DatabaseDemux(frappe.isElectron);
}
}
async createNewDatabase(dbPath: string, countryCode?: string) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View 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:');
});
});

View File

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