diff --git a/backend/database/manager.ts b/backend/database/manager.ts index 33d7e434..53d52e32 100644 --- a/backend/database/manager.ts +++ b/backend/database/manager.ts @@ -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 { diff --git a/frappe/core/dbHandler.ts b/frappe/core/dbHandler.ts index 3e78b453..70c13d4f 100644 --- a/frappe/core/dbHandler.ts +++ b/frappe/core/dbHandler.ts @@ -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 = {}; - 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) { diff --git a/frappe/core/types.ts b/frappe/core/types.ts index 532c7f50..4a8afa49 100644 --- a/frappe/core/types.ts +++ b/frappe/core/types.ts @@ -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; @@ -11,3 +12,10 @@ export type SingleValue = { 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 \ No newline at end of file diff --git a/frappe/index.ts b/frappe/index.ts index 19d40d4b..249116d8 100644 --- a/frappe/index.ts +++ b/frappe/index.ts @@ -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; temp?: Record; - 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 = {}; diff --git a/frappe/model/document.js b/frappe/model/document.js deleted file mode 100644 index fc7303c6..00000000 --- a/frappe/model/document.js +++ /dev/null @@ -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; - } -} diff --git a/src/demux/db.ts b/src/demux/db.ts index 1fce6cf3..41abe1b8 100644 --- a/src/demux/db.ts +++ b/src/demux/db.ts @@ -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; } diff --git a/src/initialization.js b/src/initialization.js deleted file mode 100644 index 13d77cb1..00000000 --- a/src/initialization.js +++ /dev/null @@ -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(); -} diff --git a/src/migrate.js b/src/migrate.js deleted file mode 100644 index ab745cdb..00000000 --- a/src/migrate.js +++ /dev/null @@ -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 - ); -} diff --git a/src/types/model.ts b/src/types/model.ts deleted file mode 100644 index 4a50a29a..00000000 --- a/src/types/model.ts +++ /dev/null @@ -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; - insert: () => Promise; - submit: () => Promise; -} - -export interface Model { - label?: string; - name: string; - doctype?: string; - fields: Field[]; - isSingle?: number; // boolean - regional?: number; // boolean - augmented?: number; // boolean - keywordFields?: string[]; - quickEditFields?: string[]; -} \ No newline at end of file diff --git a/tests/testFrappe.spec.ts b/tests/testFrappe.spec.ts new file mode 100644 index 00000000..9f5d8ebe --- /dev/null +++ b/tests/testFrappe.spec.ts @@ -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:'); + }); +}); diff --git a/utils/db/types.ts b/utils/db/types.ts index f25170bb..56403d34 100644 --- a/utils/db/types.ts +++ b/utils/db/types.ts @@ -6,6 +6,8 @@ * match on both ends. */ +import { SchemaMap } from "schemas/types"; + type UnknownMap = Record; export abstract class DatabaseBase { // Create @@ -61,3 +63,21 @@ export interface GetAllOptions { } export type QueryFilter = Record; + + +/** + * 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 + + abstract createNewDatabase(dbPath: string, countryCode?: string): Promise + + abstract connectToDatabase(dbPath: string, countryCode?: string): Promise + + abstract call(method: DatabaseMethod, ...args: unknown[]): Promise +}