2
0
mirror of https://github.com/frappe/books.git synced 2024-11-14 09:24:04 +00:00

incr: create typed frappe

This commit is contained in:
18alantom 2022-03-22 14:58:36 +05:30
parent 2afec6a74b
commit 72b6478e29
11 changed files with 449 additions and 30 deletions

View File

@ -1,3 +1,5 @@
import { Frappe } from 'frappe/core/frappe';
interface AuthConfig { interface AuthConfig {
serverURL: string; serverURL: string;
backend: string; backend: string;
@ -9,11 +11,13 @@ interface Session {
token: string; token: string;
} }
export class Auth { export class AuthHandler {
#config: AuthConfig; #config: AuthConfig;
#session: Session; #session: Session;
frappe: Frappe;
constructor() { constructor(frappe: Frappe) {
this.frappe = frappe;
this.#config = { this.#config = {
serverURL: '', serverURL: '',
backend: 'sqlite', backend: 'sqlite',
@ -34,6 +38,7 @@ export class Auth {
return { ...this.#config }; return { ...this.#config };
} }
init() {}
async login(email: string, password: string) { async login(email: string, password: string) {
if (email === 'Administrator') { if (email === 'Administrator') {
this.#session.user = 'Administrator'; this.#session.user = 'Administrator';

20
frappe/core/dbHandler.ts Normal file
View File

@ -0,0 +1,20 @@
import { Frappe } from 'frappe/core/frappe';
type SingleValue = { fieldname: string; parent: string; value: unknown };
export class DbHandler {
frappe: Frappe;
constructor(frappe: Frappe) {
this.frappe = frappe;
}
init() {}
close() {}
exists(doctype: string, name: string): boolean {
return false;
}
getSingleValues(...fieldnames: Omit<SingleValue, 'value'>[]): SingleValue[] {
return [];
}
}

254
frappe/core/docHandler.ts Normal file
View File

@ -0,0 +1,254 @@
import { Field, Model } from '@/types/model';
import Doc from 'frappe/model/document';
import Meta from 'frappe/model/meta';
import { getDuplicates, getRandomString } from 'frappe/utils';
import Observable from 'frappe/utils/observable';
import { Frappe } from './frappe';
type DocMap = Record<string, Doc | undefined>;
type MetaMap = Record<string, Meta | undefined>;
interface DocData {
doctype: string;
name?: string;
[key: string]: unknown;
}
export class DocHandler {
frappe: Frappe;
singles: DocMap = {};
metaCache: MetaMap = {};
docs?: Observable<Doc>;
models: Record<string, Model | undefined> = {};
constructor(frappe: Frappe) {
this.frappe = frappe;
}
init() {
this.models = {};
this.metaCache = {};
this.docs = new Observable();
}
registerModels(models: Record<string, Model>) {
for (const doctype in models) {
const metaDefinition = models[doctype];
if (!metaDefinition.name) {
throw new Error(`Name is mandatory for ${doctype}`);
}
if (metaDefinition.name !== doctype) {
throw new Error(
`Model name mismatch for ${doctype}: ${metaDefinition.name}`
);
}
const fieldnames = (metaDefinition.fields || [])
.map((df) => df.fieldname)
.sort();
const duplicateFieldnames = getDuplicates(fieldnames);
if (duplicateFieldnames.length > 0) {
throw new Error(
`Duplicate fields in ${doctype}: ${duplicateFieldnames.join(', ')}`
);
}
this.models[doctype] = metaDefinition;
}
}
getModels(filterFunction: (name: Model) => boolean) {
const models: Model[] = [];
for (const doctype in this.models) {
models.push(this.models[doctype]!);
}
return filterFunction ? models.filter(filterFunction) : models;
}
/**
* Cache operations
*/
addToCache(doc: Doc) {
if (!this.docs) return;
// add to `docs` cache
const name = doc.name as string | undefined;
const doctype = doc.doctype as string | undefined;
if (!doctype || !name) {
return;
}
if (!this.docs[doctype]) {
this.docs[doctype] = {};
}
(this.docs[doctype] as DocMap)[name] = doc;
// singles available as first level objects too
if (doctype === doc.name) {
this.singles[name] = doc;
}
// propogate change to `docs`
doc.on('change', (params: unknown) => {
this.docs!.trigger('change', params);
});
}
removeFromCache(doctype: string, name: string) {
const docMap = this.docs?.[doctype] as DocMap | undefined;
const doc = docMap?.[name];
if (doc) {
delete docMap[name];
} else {
console.warn(`Document ${doctype} ${name} does not exist`);
}
}
getDocFromCache(doctype: string, name: string): Doc | undefined {
const doc = (this.docs?.[doctype] as DocMap)?.[name];
return doc;
}
isDirty(doctype: string, name: string) {
const doc = (this.docs?.[doctype] as DocMap)?.[name];
if (doc === undefined) {
return false;
}
return !!doc._dirty;
}
/**
* Meta Operations
*/
getMeta(doctype: string): Meta {
const meta = this.metaCache[doctype];
if (meta) {
return meta;
}
const model = this.models?.[doctype];
if (!model) {
throw new Error(`${doctype} is not a registered doctype`);
}
this.metaCache[doctype] = new this.frappe.Meta!(model);
return this.metaCache[doctype]!;
}
createMeta(fields: Field[]) {
return new this.frappe.Meta!({ isCustom: 1, fields });
}
/**
* Doc Operations
*/
async getDoc(
doctype: string,
name: string,
options = { skipDocumentCache: false }
) {
let doc = null;
if (!options?.skipDocumentCache) {
doc = this.getDocFromCache(doctype, name);
}
if (doc) {
return doc;
}
const DocClass = this.getDocumentClass(doctype);
doc = new DocClass({
doctype: doctype,
name: name,
});
await doc.load();
this.addToCache(doc);
return doc;
}
getDocumentClass(doctype: string): typeof Doc {
const meta = this.getMeta(doctype);
let documentClass = this.frappe.Document!;
if (meta && meta.documentClass) {
documentClass = meta.documentClass as typeof Doc;
}
return documentClass;
}
async getSingle(doctype: string) {
return await this.getDoc(doctype, doctype);
}
async getDuplicate(doc: Doc) {
const doctype = doc.doctype as string;
const newDoc = await this.getEmptyDoc(doctype);
const meta = this.getMeta(doctype);
const fields = meta.getValidFields() as Field[];
for (const field of fields) {
if (['name', 'submitted'].includes(field.fieldname)) {
continue;
}
newDoc[field.fieldname] = doc[field.fieldname];
if (field.fieldtype === 'Table') {
const value = (doc[field.fieldname] as DocData[]) || [];
newDoc[field.fieldname] = value.map((d) => {
const childData = Object.assign({}, d);
childData.name = '';
return childData;
});
}
}
return newDoc;
}
getEmptyDoc(doctype: string, cacheDoc: boolean = true): Doc {
const doc = this.getNewDoc({ doctype });
doc._notInserted = true;
doc.name = getRandomString();
if (cacheDoc) {
this.addToCache(doc);
}
return doc;
}
getNewDoc(data: DocData): Doc {
const DocClass = this.getDocumentClass(data.doctype);
const doc = new DocClass(data);
doc.setDefaults();
return doc;
}
async syncDoc(data: DocData) {
let doc;
const { doctype, name } = data;
if (!doctype || !name) {
return;
}
const docExists = await this.frappe.db.exists(doctype, name);
if (docExists) {
doc = await this.getDoc(doctype, name);
Object.assign(doc, data);
await doc.update();
} else {
doc = this.getNewDoc(data);
await doc.insert();
}
}
}

144
frappe/core/frappe.ts Normal file
View File

@ -0,0 +1,144 @@
import { ErrorLog } from '@/errorHandling';
import { Model } from '@/types/model';
import Doc from 'frappe/model/document';
import Meta from 'frappe/model/meta';
import { getMoneyMaker, MoneyMaker } from 'pesa';
import { markRaw } from 'vue';
import {
DEFAULT_DISPLAY_PRECISION,
DEFAULT_INTERNAL_PRECISION,
} from '../utils/consts';
import * as errors from '../utils/errors';
import { format } from '../utils/format';
import { t, T } from '../utils/translation';
import { AuthHandler } from './authHandler';
import { DbHandler } from './dbHandler';
import { DocHandler } from './docHandler';
export class Frappe {
t = t;
T = T;
format = format;
errors = errors;
isElectron = false;
isServer = false;
pesa: MoneyMaker;
auth: AuthHandler;
doc: DocHandler;
db: DbHandler;
Meta?: typeof Meta;
Document?: typeof Doc;
_initialized: boolean = false;
errorLog?: ErrorLog[];
methods?: Record<string, Function>;
temp?: Record<string, unknown>;
constructor() {
this.auth = new AuthHandler(this);
this.doc = new DocHandler(this);
this.db = new DbHandler(this);
this.pesa = getMoneyMaker({
currency: 'XXX',
precision: DEFAULT_INTERNAL_PRECISION,
display: DEFAULT_DISPLAY_PRECISION,
wrapper: markRaw,
});
}
get initialized() {
return this._initialized;
}
get docs() {
return this.doc.docs;
}
get models() {
return this.doc.models;
}
async initializeAndRegister(customModels = {}, force = false) {
this.init(force);
this.Meta = (await import('frappe/model/meta')).default;
this.Document = (await import('frappe/model/document')).default;
const coreModels = await import('frappe/models');
this.doc.registerModels(coreModels.default as Record<string, Model>);
this.doc.registerModels(customModels);
}
init(force: boolean) {
if (this._initialized && !force) return;
this.methods = {};
this.errorLog = [];
// temp params while calling routes
this.temp = {};
this.doc.init();
this.auth.init();
this.db.init();
this._initialized = true;
}
async initializeMoneyMaker(currency: string = 'XXX') {
// to be called after db initialization
const values =
(await this.db?.getSingleValues(
{
fieldname: 'internalPrecision',
parent: 'SystemSettings',
},
{
fieldname: 'displayPrecision',
parent: 'SystemSettings',
}
)) ?? [];
const acc = values.reduce((acc, sv) => {
acc[sv.fieldname] = sv.value as string | number | undefined;
return acc;
}, {} as Record<string, string | number | undefined>);
let precision: string | number =
acc.internalPrecision ?? DEFAULT_INTERNAL_PRECISION;
let display: string | number =
acc.displayPrecision ?? DEFAULT_DISPLAY_PRECISION;
if (typeof precision === 'string') {
precision = parseInt(precision);
}
if (typeof display === 'string') {
display = parseInt(display);
}
this.pesa = getMoneyMaker({
currency,
precision,
display,
wrapper: markRaw,
});
}
close() {
this.db.close();
this.auth.logout();
}
store = {
isDevelopment: false,
appVersion: '',
};
}
export { T, t };
export default new Frappe();

View File

@ -1,6 +1,6 @@
import { getMoneyMaker } from 'pesa'; import { getMoneyMaker } from 'pesa';
import { markRaw } from 'vue'; import { markRaw } from 'vue';
import { Auth } from './core/auth'; import { AuthHandler } from './core/authHandler';
import { asyncHandler, getDuplicates, getRandomString } from './utils'; import { asyncHandler, getDuplicates, getRandomString } from './utils';
import { import {
DEFAULT_DISPLAY_PRECISION, DEFAULT_DISPLAY_PRECISION,
@ -11,7 +11,7 @@ import { format } from './utils/format';
import Observable from './utils/observable'; import Observable from './utils/observable';
import { t, T } from './utils/translation'; import { t, T } from './utils/translation';
class Frappe { export class Frappe {
t = t; t = t;
T = T; T = T;
format = format; format = format;
@ -21,7 +21,7 @@ class Frappe {
isServer = false; isServer = false;
constructor() { constructor() {
this.auth = new Auth(); this.auth = new AuthHandler();
} }
async initializeAndRegister(customModels = {}, force = false) { async initializeAndRegister(customModels = {}, force = false) {

View File

@ -3,7 +3,7 @@ enum EventType {
OnceListeners = '_onceListeners', OnceListeners = '_onceListeners',
} }
export default class Observable { export default class Observable<T> {
[key: string]: unknown; [key: string]: unknown;
_isHot: Map<string, boolean>; _isHot: Map<string, boolean>;
_eventQueue: Map<string, unknown[]>; _eventQueue: Map<string, unknown[]>;

View File

@ -27,7 +27,7 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"luxon": "^2.0.2", "luxon": "^2.0.2",
"node-fetch": "2", "node-fetch": "2",
"pesa": "^1.1.3", "pesa": "^1.1.11",
"sqlite3": "npm:@vscode/sqlite3@^5.0.7", "sqlite3": "npm:@vscode/sqlite3@^5.0.7",
"vue": "^3.2.30", "vue": "^3.2.30",
"vue-router": "^4.0.12" "vue-router": "^4.0.12"

View File

@ -1,18 +1,18 @@
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import frappe, { t } from 'frappe'; import frappe, { t } from 'frappe';
import Document from 'frappe/model/document';
import { import {
DuplicateEntryError, DuplicateEntryError,
LinkValidationError, LinkValidationError,
MandatoryError, MandatoryError,
ValidationError, ValidationError,
} from 'frappe/utils/errors'; } from 'frappe/utils/errors';
import Document from 'frappe/model/document';
import config, { ConfigKeys, TelemetrySetting } from './config'; import config, { ConfigKeys, TelemetrySetting } from './config';
import { IPC_ACTIONS, IPC_MESSAGES } from './messages'; import { IPC_ACTIONS, IPC_MESSAGES } from './messages';
import telemetry from './telemetry/telemetry'; import telemetry from './telemetry/telemetry';
import { showMessageDialog, showToast } from './utils'; import { showMessageDialog, showToast } from './utils';
interface ErrorLog { export interface ErrorLog {
name: string; name: string;
message: string; message: string;
stack?: string; stack?: string;

View File

@ -1,20 +1,3 @@
import FormControl from '@/components/Controls/FormControl';
import LanguageSelector from '@/components/Controls/LanguageSelector.vue';
import Popover from '@/components/Popover';
import TwoColumnForm from '@/components/TwoColumnForm';
import config from '@/config';
import { connectToLocalDatabase, purgeCache } from '@/initialization';
import { IPC_MESSAGES } from '@/messages';
import { setLanguageMap, showMessageDialog } from '@/utils';
import { ipcRenderer } from 'electron';
import frappe from 'frappe';
import fs from 'fs';
import path from 'path';
import {
getErrorMessage, handleErrorWithDialog, showErrorDialog
} from '../../errorHandling';
import setupCompany from './setupCompany';
import Slide from './Slide.vue';
<template> <template>
<div> <div>
<Slide <Slide

View File

@ -30,6 +30,7 @@ export interface Field {
readOnly?: number; readOnly?: number;
hidden?: number | Function; hidden?: number | Function;
options?: string[]; options?: string[];
description?: string;
} }
export interface Doc { export interface Doc {
@ -38,3 +39,15 @@ export interface Doc {
insert: () => Promise<void>; insert: () => Promise<void>;
submit: () => 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[];
}

View File

@ -8573,10 +8573,10 @@ performance-now@^2.1.0:
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
pesa@^1.1.3: pesa@^1.1.11:
version "1.1.6" version "1.1.11"
resolved "https://registry.yarnpkg.com/pesa/-/pesa-1.1.6.tgz#a123dc7a31c977bbfb6dbbee14a9913c8385cd0f" resolved "https://registry.yarnpkg.com/pesa/-/pesa-1.1.11.tgz#85829e5aa11ea3e44d7c9ea4b500e4d6def6dc5f"
integrity sha512-EDs4Tj8QX7hZITkKhfHeVT/9oQRqRPyF3pVet1FqGX94/zkcqreBYb94ojUVrzZmir26+IHaKyCpTB6eqC04uw== integrity sha512-eyl0lpdUIV0dNXVeTMnhBJj6u9GRIYwP+vFdUN+767Fv3PNQHPHAkCQJqDseGfEF75lhe23ZnfbA/uMidlq5/Q==
pg-connection-string@2.5.0: pg-connection-string@2.5.0:
version "2.5.0" version "2.5.0"