/
base_plugin.ts
138 lines (113 loc) · 4.05 KB
/
base_plugin.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
/*
* Copyright © 2019 Lisk Foundation
*
* See the LICENSE file at the top-level directory of this distribution
* for licensing information.
*
* Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation,
* no part of this software, including this file, may be copied, modified,
* propagated, or distributed except according to the terms contained in the
* LICENSE file.
*
* Removal or modification of this copyright notice is prohibited.
*/
import { join } from 'path';
import { validator } from '@liskhq/lisk-validator';
import { APIClient, createIPCClient } from '@liskhq/lisk-api-client';
import { objects } from '@liskhq/lisk-utils';
import { Logger } from '../logger';
import { systemDirs } from '../system_dirs';
import { ApplicationConfigForPlugin, PluginConfig, SchemaWithDefault } from '../types';
import { ImplementationMissingError } from '../errors';
import { BasePluginEndpoint } from './base_plugin_endpoint';
export interface PluginInitContext<T = Record<string, unknown>> {
logger: Logger;
config: PluginConfig & T;
appConfig: ApplicationConfigForPlugin;
}
export const getPluginExportPath = (pluginInstance: BasePlugin): string | undefined => {
let plugin: Record<string, unknown> | undefined;
if (!pluginInstance.nodeModulePath) {
return undefined;
}
try {
// Check if plugin nodeModulePath is an npm package
// eslint-disable-next-line global-require, import/no-dynamic-require, @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-assignment
plugin = require(pluginInstance.nodeModulePath);
} catch (error) {
/* Plugin nodeModulePath is not an npm package */
}
if (!plugin?.[pluginInstance.constructor.name]) {
return undefined;
}
const Klass = plugin[pluginInstance.constructor.name];
if (typeof Klass !== 'function' || Klass.name !== pluginInstance.constructor.name) {
return undefined;
}
if (!(pluginInstance instanceof Klass)) {
return undefined;
}
return pluginInstance.nodeModulePath;
};
export const validatePluginSpec = (pluginInstance: BasePlugin): void => {
if (!pluginInstance.name) {
throw new ImplementationMissingError('Plugin "name" is required.');
}
if (!pluginInstance.load) {
throw new ImplementationMissingError('Plugin "load" interface is required.');
}
if (!pluginInstance.unload) {
throw new ImplementationMissingError('Plugin "unload" interface is required.');
}
if (pluginInstance.configSchema) {
validator.validateSchema(pluginInstance.configSchema);
}
};
export abstract class BasePlugin<T = Record<string, unknown>> {
public readonly configSchema?: SchemaWithDefault;
public endpoint?: BasePluginEndpoint;
protected logger!: Logger;
private _apiClient!: APIClient;
private _config!: T;
private _appConfig!: ApplicationConfigForPlugin;
public get name(): string {
const name = this.constructor.name.replace('Plugin', '');
return name.charAt(0).toLowerCase() + name.substr(1);
}
public get config(): T {
return this._config;
}
public get appConfig(): ApplicationConfigForPlugin {
return this._appConfig;
}
public get dataPath(): string {
const dirs = systemDirs(this.appConfig.system.dataPath);
return join(dirs.plugins, this.name, 'data');
}
public get events(): string[] {
return [];
}
public get apiClient(): APIClient {
if (!this._apiClient) {
throw new Error('RPC with IPC protocol must be enabled to use MethodClient.');
}
return this._apiClient;
}
public async init(context: PluginInitContext): Promise<void> {
this.logger = context.logger;
if (this.configSchema) {
this._config = objects.mergeDeep({}, this.configSchema.default ?? {}, context.config) as T;
validator.validate(this.configSchema, this.config as Record<string, unknown>);
} else {
this._config = {} as T;
}
this._appConfig = context.appConfig;
if (this._appConfig.rpc.modes.includes('ipc')) {
const dirs = systemDirs(this.appConfig.system.dataPath);
this._apiClient = await createIPCClient(dirs.dataPath);
}
}
public abstract get nodeModulePath(): string;
public abstract load(): Promise<void>;
public abstract unload(): Promise<void>;
}