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:
parent
6da030de90
commit
5489cd979f
@ -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[] {
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
@ -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)) {
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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
21
fyo/models/Plugin.ts
Normal 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'],
|
||||
};
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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[];
|
||||
});
|
||||
}
|
||||
|
@ -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'],
|
||||
};
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -1,10 +0,0 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
|
||||
export class PluginTemplate extends Doc {
|
||||
name?: string;
|
||||
value?: string;
|
||||
|
||||
get isPluginTemplate() {
|
||||
return true;
|
||||
}
|
||||
}
|
@ -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 };
|
||||
}
|
||||
|
@ -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 },
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user