2
0
mirror of https://github.com/frappe/books.git synced 2024-12-22 10:58:59 +00:00

incr: add registration of models onload

- refactor a few imports
- move model file into plugin
This commit is contained in:
18alantom 2023-07-24 14:53:57 +05:30
parent 6da030de90
commit 5489cd979f
21 changed files with 209 additions and 96 deletions

View File

@ -8,6 +8,7 @@ import { getSchemas } from 'schemas/index';
import { SchemaStub } from 'schemas/types';
import { PluginInfo } from 'utils/types';
import type DatabaseCore from './core';
import { PluginConfig } from './types';
export async function executeFirstMigration(
db: DatabaseCore,
@ -36,9 +37,14 @@ export async function getIsFirstRun(knex: Knex): Promise<boolean> {
}
export async function getPluginInfoList(knex: Knex): Promise<PluginInfo[]> {
const plugins = (await knex('Plugin').select(['info'])) as {
info: string;
}[];
let plugins: { info: string }[];
try {
plugins = (await knex('Plugin').select(['info'])) as {
info: string;
}[];
} catch {
return [];
}
return plugins.map(({ info }) => JSON.parse(info) as PluginInfo);
}
@ -77,7 +83,12 @@ export async function unzipPluginsIfDoesNotExist(
function deletePluginFolder(info: PluginInfo) {
const pluginsRootPath = getAppPath('plugins');
const folderNamePrefix = getPluginFolderNameFromInfo(info, true) + '-';
for (const folderName of fs.readdirSync(pluginsRootPath)) {
let folderNames: string[] = [];
try {
folderNames = fs.readdirSync(pluginsRootPath);
} catch {}
for (const folderName of folderNames) {
if (!folderName.startsWith(folderNamePrefix)) {
continue;
}
@ -86,44 +97,47 @@ function deletePluginFolder(info: PluginInfo) {
}
}
export async function getRawPluginSchemaList(
infoList: PluginInfo[]
): Promise<SchemaStub[]> {
const pluginsRoot = getAppPath('plugins');
const schemaStubs: SchemaStub[][] = [];
const folderSet = new Set(
infoList.map((info) => getPluginFolderNameFromInfo(info))
);
export async function getPluginConfig(info: PluginInfo): Promise<PluginConfig> {
const folderName = getPluginFolderNameFromInfo(info);
const pluginRoot = path.join(getAppPath('plugins'), folderName);
if (!fs.existsSync(pluginsRoot)) {
return [];
const config: PluginConfig = {
info,
schemas: [],
paths: {
folderName,
root: pluginRoot,
},
};
const schemasPath = path.resolve(pluginRoot, 'schemas.js');
if (fs.existsSync(schemasPath)) {
config.paths.schemas = schemasPath;
config.schemas = await importSchemas(schemasPath);
}
for (const pluginFolderName of fs.readdirSync(pluginsRoot)) {
if (!folderSet.has(pluginFolderName)) {
continue;
}
const modelsPath = path.resolve(pluginRoot, 'models.js');
if (fs.existsSync(schemasPath)) {
config.paths.models = modelsPath;
}
const pluginPath = path.join(pluginsRoot, pluginFolderName);
const schemasJs = path.resolve(path.join(pluginPath, 'schemas.js'));
if (!fs.existsSync(schemasJs)) {
continue;
}
return config;
}
async function importSchemas(schemasPath: string): Promise<SchemaStub[]> {
try {
const {
default: { default: schemas },
} = (await import(schemasJs)) as {
default: { default: exportedSchemas },
} = (await import(schemasPath)) as {
default: { default: unknown };
};
if (!isSchemaStubList(schemas)) {
continue;
if (isSchemaStubList(exportedSchemas)) {
return exportedSchemas;
}
} catch {}
schemaStubs.push(schemas);
}
return schemaStubs.flat();
return [];
}
function isSchemaStubList(schemas: unknown): schemas is SchemaStub[] {

View File

@ -13,15 +13,16 @@ import { BespokeQueries } from './bespoke';
import DatabaseCore from './core';
import {
executeFirstMigration,
getPluginConfig,
getPluginInfoList,
getRawPluginSchemaList,
unzipPluginsIfDoesNotExist,
} from './helpers';
import { runPatches } from './runPatch';
import { BespokeFunction, Patch } from './types';
import { BespokeFunction, Patch, PluginConfig } from './types';
export class DatabaseManager extends DatabaseDemuxBase {
db?: DatabaseCore;
plugins: PluginConfig[] = [];
rawPluginSchemaList?: SchemaStub[];
get #isInitialized(): boolean {
@ -54,10 +55,7 @@ export class DatabaseManager extends DatabaseDemuxBase {
}
await executeFirstMigration(this.db, countryCode);
const infoList = await getPluginInfoList(this.db.knex);
await unzipPluginsIfDoesNotExist(this.db.knex, infoList);
this.rawPluginSchemaList = await getRawPluginSchemaList(infoList);
await this.initializePlugins();
const schemaMap = getSchemas(countryCode, this.rawPluginSchemaList);
this.db.setSchemaMap(schemaMap);
@ -65,6 +63,23 @@ export class DatabaseManager extends DatabaseDemuxBase {
return countryCode;
}
async initializePlugins() {
if (!this.db?.knex) {
return;
}
const infoList = await getPluginInfoList(this.db.knex);
await unzipPluginsIfDoesNotExist(this.db.knex, infoList);
this.plugins = [];
for (const info of infoList) {
const config = await getPluginConfig(info);
this.plugins.push(config);
}
this.rawPluginSchemaList = this.plugins.map((p) => p.schemas).flat();
}
async #executeMigration() {
const version = await this.#getAppVersion();
const patches = await this.#getPatchesToExecute(version);

View File

@ -1,5 +1,6 @@
import { Field, RawValue } from '../../schemas/types';
import DatabaseCore from './core';
import { PluginInfo } from 'utils/types';
import { Field, RawValue, SchemaStub } from '../../schemas/types';
import type DatabaseCore from './core';
import { DatabaseManager } from './manager';
export interface GetQueryBuilderOptions {
@ -80,3 +81,14 @@ export type SingleValue<T> = {
parent: string;
value: T;
}[];
export type PluginConfig = {
info: PluginInfo;
schemas: SchemaStub[];
paths: {
root: string;
schemas?: string;
models?: string;
folderName: string;
};
};

View File

@ -90,8 +90,7 @@ async function buildPlugin(pluginName) {
sourcemap: 'inline',
sourcesContent: false,
bundle: true,
platform: 'node',
target: 'node16',
...getSpecificConfig(key),
outfile: path.join(pluginBuildPath, `${key}.js`),
external: ['knex', 'electron', 'better-sqlite3', 'electron-store'],
plugins: [excludeVendorFromSourceMap],
@ -108,6 +107,14 @@ async function buildPlugin(pluginName) {
await createPluginPackage(pluginName, pluginBuildPath);
}
function getSpecificConfig(key) {
if (key === 'schemas') {
return { platform: 'node', target: 'node16', format: 'cjs' };
}
return { platform: 'browser', target: 'chrome100', format: 'esm' };
}
async function createPluginPackage(pluginName, pluginBuildPath) {
const zip = new AdmZip();
for (const file of fs.readdirSync(pluginBuildPath)) {

View File

@ -22,7 +22,7 @@ export class DocHandler {
}
init() {
this.models = {};
this.models = { ...coreModels };
this.singles = {};
this.docs = new Observable();
this.observer = new Observable();
@ -32,17 +32,18 @@ export class DocHandler {
this.init();
}
registerModels(models: ModelMap, regionalModels: ModelMap = {}) {
for (const schemaName in this.fyo.db.schemaMap) {
if (coreModels[schemaName] !== undefined) {
this.models[schemaName] = coreModels[schemaName];
} else if (regionalModels[schemaName] !== undefined) {
this.models[schemaName] = regionalModels[schemaName];
} else if (models[schemaName] !== undefined) {
this.models[schemaName] = models[schemaName];
} else {
this.models[schemaName] = Doc;
registerModels(models: ModelMap) {
for (const schemaName in models) {
const Model = models[schemaName];
if (
!this.fyo.db.schemaMap[schemaName] ||
!!coreModels[schemaName] ||
!Model
) {
continue;
}
this.models[schemaName] = Model;
}
}
@ -98,7 +99,7 @@ export class DocHandler {
throw new NotFoundError(`Schema not found for ${schemaName}`);
}
const doc = new Model!(schema, data, this.fyo, isRawValueMap);
const doc = new (Model ?? Doc)(schema, data, this.fyo, isRawValueMap);
doc.name ??= this.getTemporaryName(schema);
if (cacheDoc) {
this.#addToCache(doc);

View File

@ -26,6 +26,8 @@ export class Fyo {
t = t;
T = T;
Doc = Doc;
errors = errors;
isElectron: boolean;
@ -112,7 +114,8 @@ export class Fyo {
await this.#initializeModules();
await this.#initializeMoneyMaker();
this.doc.registerModels(models, regionalModels);
this.doc.registerModels(models);
this.doc.registerModels(regionalModels);
await this.doc.getDoc('SystemSettings');
this._initialized = true;
}

View File

@ -1,5 +1,5 @@
import { Fyo } from 'fyo';
import NumberSeries from 'fyo/models/NumberSeries';
import { NumberSeries } from 'fyo/models/NumberSeries';
import { DEFAULT_SERIES_START } from 'fyo/utils/consts';
import { BaseError } from 'fyo/utils/errors';
import { getRandomString } from 'utils';

View File

@ -1,6 +1,6 @@
import { Fyo } from 'fyo';
import { DocValue, DocValueMap } from 'fyo/core/types';
import type SystemSettings from 'fyo/models/SystemSettings';
import type { SystemSettings } from 'fyo/models/SystemSettings';
import { FieldType, Schema, SelectOption } from 'schemas/types';
import { QueryFilter } from 'utils/db/types';
import { RouteLocationRaw, Router } from 'vue-router';

View File

@ -8,7 +8,7 @@ function getPaddedName(prefix: string, next: number, padZeros: number): string {
return prefix + next.toString().padStart(padZeros ?? 4, '0');
}
export default class NumberSeries extends Doc {
export class NumberSeries extends Doc {
validations: ValidationMap = {
name: (value) => {
if (typeof value !== 'string') {

21
fyo/models/Plugin.ts Normal file
View File

@ -0,0 +1,21 @@
import { Doc } from 'fyo/model/doc';
import { ListViewSettings } from 'fyo/model/types';
export class Plugin extends Doc {
name?: string;
version?: string;
info?: string;
/*
override get canDelete(): boolean {
return false;
}
*/
static getListViewSettings(): ListViewSettings {
return {
formRoute: (name) => `/plugin/${name}`,
columns: ['name'],
};
}
}

View File

@ -6,7 +6,7 @@ import { t } from 'fyo/utils/translation';
import { SelectOption } from 'schemas/types';
import { getCountryInfo } from 'utils/misc';
export default class SystemSettings extends Doc {
export class SystemSettings extends Doc {
dateFormat?: string;
locale?: string;
displayPrecision?: number;

View File

@ -1,8 +1,10 @@
import { ModelMap } from 'fyo/model/types';
import NumberSeries from './NumberSeries';
import SystemSettings from './SystemSettings';
import { NumberSeries } from './NumberSeries';
import { SystemSettings } from './SystemSettings';
import { Plugin } from './Plugin';
export const coreModels = {
Plugin,
NumberSeries,
SystemSettings,
} as ModelMap;

View File

@ -59,6 +59,12 @@ const ipc = {
)) as { name: string; info: string; data: string };
},
async getPluginModules() {
return (await ipcRenderer.invoke(
IPC_ACTIONS.GET_PLUGIN_MODULES
)) as string[];
},
async getSaveFilePath(options: SaveDialogOptions) {
return (await ipcRenderer.invoke(
IPC_ACTIONS.GET_SAVE_FILEPATH,

View File

@ -33,6 +33,7 @@ import {
unzipFile,
} from './helpers';
import { saveHtmlAsPdf } from './saveHtmlAsPdf';
import manager from '../backend/database/manager';
export default function registerIpcMainActionListeners(main: Main) {
ipcMain.handle(IPC_ACTIONS.CHECK_DB_ACCESS, async (_, filePath: string) => {
@ -268,4 +269,14 @@ export default function registerIpcMainActionListeners(main: Main) {
data: fs.readFileSync(filePath).toString('base64'),
};
});
ipcMain.handle(IPC_ACTIONS.GET_PLUGIN_MODULES, () => {
if (main.isTest) {
return [];
}
return manager.plugins
.map(({ paths }) => paths.models)
.filter(Boolean) as string[];
});
}

View File

@ -1,21 +0,0 @@
import { Doc } from 'fyo/model/doc';
import { ListViewSettings } from 'fyo/model/types';
export class Plugin extends Doc {
name?: string;
version?: string;
info?: string;
/*
override get canDelete(): boolean {
return false;
}
*/
static getListViewSettings(): ListViewSettings {
return {
formRoute: (name) => `/plugin/${name}`,
columns: ['name'],
};
}
}

View File

@ -33,7 +33,6 @@ import { ShipmentItem } from './inventory/ShipmentItem';
import { StockLedgerEntry } from './inventory/StockLedgerEntry';
import { StockMovement } from './inventory/StockMovement';
import { StockMovementItem } from './inventory/StockMovementItem';
import { Plugin } from './baseModels/Plugin';
export const models = {
Account,
@ -47,7 +46,6 @@ export const models = {
JournalEntryAccount,
Misc,
Party,
Plugin,
Payment,
PaymentFor,
PrintSettings,

View File

@ -1,10 +0,0 @@
import { Doc } from 'fyo/model/doc';
export class PluginTemplate extends Doc {
name?: string;
value?: string;
get isPluginTemplate() {
return true;
}
}

View File

@ -1,3 +1,19 @@
import { PluginTemplate } from './PluginTemplate';
import type { Fyo } from 'fyo';
export default { PluginTemplate };
export default function initialize(fyo: Fyo) {
const models = getModels(fyo);
fyo.doc.registerModels(models);
}
function getModels(fyo: Fyo) {
class PluginTemplate extends fyo.Doc {
name?: string;
value?: string;
get isPluginTemplate() {
return true;
}
}
return { PluginTemplate };
}

View File

@ -16,11 +16,11 @@
</div>
</template>
<script lang="ts">
import { Plugin } from 'models/baseModels/Plugin';
import { ModelNameEnum } from 'models/types';
import PageHeader from 'src/components/PageHeader.vue';
import Button from 'src/components/Button.vue';
import { defineComponent } from 'vue';
import { Plugin } from 'fyo/models/Plugin';
export default defineComponent({
components: { PageHeader, Button },

View File

@ -33,6 +33,7 @@ export async function initializeInstance(
await setInstanceId(fyo);
await setOpenCount(fyo);
await setCurrencySymbols(fyo);
await loadPlugins(fyo);
}
async function closeDbIfConnected(fyo: Fyo) {
@ -185,3 +186,39 @@ function getOpenCountFromFiles(fyo: Fyo) {
return null;
}
async function loadPlugins(fyo: Fyo) {
/**
* Property `ipc` is set by the preload script. This
* doesn't run when Frappe Books is not running in electron.
*/
if (!fyo.isElectron || !ipc?.getPluginModules) {
return;
}
const srcs = await ipc.getPluginModules();
for (const src of srcs) {
const module = document.createElement('script');
module.setAttribute('type', 'module');
module.setAttribute('src', src);
const loadPromise = new Promise<void>((resolve) => {
module.onload = async () => {
const { default: initialize } = (await import(
src /* @vite-ignore */
)) as {
default: (fyo: Fyo) => Promise<void>;
};
if (typeof initialize !== 'function') {
resolve();
}
await initialize(fyo);
resolve();
};
});
document.body.appendChild(module);
await loadPromise;
}
}

View File

@ -27,6 +27,7 @@ export enum IPC_ACTIONS {
DELETE_FILE = 'delete-file',
GET_DB_DEFAULT_PATH = 'get-db-default-path',
GET_PLUGIN_DATA = 'get-plugin-data',
GET_PLUGIN_MODULES = 'get-plugin-modules',
// Database messages
DB_CREATE = 'db-create',
DB_CONNECT = 'db-connect',