mirror of
https://github.com/unraid/api.git
synced 2026-01-10 10:40:04 -06:00
chore: lint
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import stw from 'spread-the-word';
|
||||
import { log, discoveryLogger } from '../log';
|
||||
import { discoveryLogger as log } from '../log';
|
||||
|
||||
/**
|
||||
* Listen to devices on the local network via mDNS.
|
||||
@@ -17,13 +17,13 @@ export const listen = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
discoveryLogger.info(`Found a new local server [${ipAddress}], visit your my servers dashboard to claim.`);
|
||||
log.info(`Found a new local server [${ipAddress}], visit your my servers dashboard to claim.`);
|
||||
}
|
||||
}
|
||||
// Console.log(`${service.name} is up! (from ${referrer.address}`);
|
||||
})
|
||||
.on('down', (remoteService, _res, referrer) => {
|
||||
discoveryLogger.debug(`${remoteService.name} is down! (from ${referrer.address})`);
|
||||
log.debug(`${remoteService.name} is down! (from ${referrer.address})`);
|
||||
});
|
||||
|
||||
await stw.listen();
|
||||
|
||||
@@ -11,6 +11,6 @@ import { AppError } from './app-error';
|
||||
export class FieldMissingError extends AppError {
|
||||
constructor(private readonly field: string) {
|
||||
// Overriding both message and status code.
|
||||
super('Field missing: ' + field, 400);
|
||||
super(`Field missing: ${field}`, 400);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,11 @@ import { sharesState, slotsState } from '../states';
|
||||
import { ensurePermission } from '../utils';
|
||||
|
||||
export const addShare = async (context: CoreContext): Promise<CoreResult> => {
|
||||
const { user, data = {} } = context;
|
||||
const { user, data } = context;
|
||||
|
||||
if (!data?.name) {
|
||||
throw new AppError('No name provided');
|
||||
}
|
||||
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
@@ -18,7 +22,7 @@ export const addShare = async (context: CoreContext): Promise<CoreResult> => {
|
||||
possession: 'any'
|
||||
});
|
||||
|
||||
const { name } = data;
|
||||
const name: string = data.name;
|
||||
const userShares = sharesState.find().map(({ name }) => name);
|
||||
const diskShares = slotsState.find({ exportable: 'yes' }).filter(({ name }) => name.startsWith('disk')).map(({ name }) => name);
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export const getArray = (context: CoreContext): CoreResult => {
|
||||
|
||||
// Array state
|
||||
const arrayState = varState?.data?.mdState.toLowerCase();
|
||||
const state = arrayState.startsWith('error') ? arrayState.split(':')[1] : arrayState;
|
||||
const state: string = arrayState.startsWith('error') ? arrayState.split(':')[1] : arrayState;
|
||||
|
||||
// All known disks
|
||||
const allDisks = slotsState.find().filter(disk => disk.device);
|
||||
|
||||
@@ -8,8 +8,7 @@ import { CoreResult, CoreContext } from '../types';
|
||||
import { paths } from '../paths';
|
||||
import { FileMissingError } from '../errors';
|
||||
import { ensurePermission } from '../utils';
|
||||
|
||||
const Table = require('cli-table');
|
||||
import Table from 'cli-table';
|
||||
|
||||
/**
|
||||
* Get parity history.
|
||||
|
||||
@@ -86,13 +86,13 @@ const systemPciDevices = async (): Promise<PciDevice[]> => {
|
||||
* - Add whether kernel-bound driver exists
|
||||
* - Cleanup device vendor/product names
|
||||
*/
|
||||
const processedDevices = await filterDevices(filteredDevices).then(devices => {
|
||||
return devices
|
||||
const processedDevices = await filterDevices(filteredDevices).then(async devices => {
|
||||
return Promise.all(devices
|
||||
// @ts-expect-error
|
||||
.map(addDeviceClass)
|
||||
.map(device => {
|
||||
.map(async device => {
|
||||
// Attempt to get the current kernel-bound driver for this pci device
|
||||
isSymlink(`${basePath}${device.id}/driver`).then(symlink => {
|
||||
await isSymlink(`${basePath}${device.id}/driver`).then(symlink => {
|
||||
if (symlink) {
|
||||
// $strLink = @readlink('/sys/bus/pci/devices/0000:'.$arrMatch['id']. '/driver');
|
||||
// if (!empty($strLink)) {
|
||||
@@ -106,7 +106,7 @@ const systemPciDevices = async (): Promise<PciDevice[]> => {
|
||||
device.productname = sanitizeProduct(device.productname);
|
||||
|
||||
return device;
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
return processedDevices;
|
||||
@@ -144,6 +144,7 @@ const getSystemUSBDevices = async (): Promise<any[]> => {
|
||||
// Get a list of all usb hubs so we can filter the allowed/disallowed
|
||||
const usbHubs = await execa('cat /sys/bus/usb/drivers/hub/*/modalias', { shell: true }).then(({ stdout }) => {
|
||||
return stdout.split('\n').map(line => {
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-regexp-exec
|
||||
const [, id] = line.match(/usb:v(\w{9})/)!;
|
||||
return id.replace('p', ':');
|
||||
});
|
||||
@@ -175,6 +176,7 @@ const getSystemUSBDevices = async (): Promise<any[]> => {
|
||||
|
||||
// Parse the line
|
||||
const [, _] = line.split(/[ \t]{2,}/).filter(Boolean);
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-regexp-exec
|
||||
const match = _.match(/^(\S+)\s(.*)/)?.slice(1);
|
||||
|
||||
// If there's no match return nothing
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface Options extends NotifierOptions {}
|
||||
export class HttpNotifier extends Notifier {
|
||||
readonly $http = fetch;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
||||
constructor(options: Options) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ export class MqttNotifier extends Notifier {
|
||||
super(options);
|
||||
|
||||
if (MqttNotifier.instance) {
|
||||
// eslint-disable-next-line no-constructor-return
|
||||
return MqttNotifier.instance;
|
||||
}
|
||||
|
||||
|
||||
@@ -152,6 +152,7 @@ export class PluginManager {
|
||||
}
|
||||
|
||||
// Get the plugin's package.json
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const pluginPackage: PackageJson = require(pluginPackagePath);
|
||||
if (!pluginPackage.main) {
|
||||
throw new AppError(`Plugin "${pluginName}" is missing it’s "main" field in the "package.json".`);
|
||||
|
||||
@@ -10,7 +10,9 @@ import { parseConfig } from '../utils/misc';
|
||||
import { ArrayState } from './state';
|
||||
import { LooseObject } from '../types';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface Device {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface DeviceIni {}
|
||||
|
||||
const parse = (state: DeviceIni[]): Device[] => Object.values(state);
|
||||
@@ -19,14 +21,15 @@ const parse = (state: DeviceIni[]): Device[] => Object.values(state);
|
||||
* Devices
|
||||
*/
|
||||
class Devices extends ArrayState {
|
||||
public channel = 'devices';
|
||||
private static instance: Devices;
|
||||
public channel = 'devices';
|
||||
_data: any;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
if (Devices.instance) {
|
||||
// eslint-disable-next-line no-constructor-return
|
||||
return Devices.instance;
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ class Network extends ArrayState {
|
||||
super();
|
||||
|
||||
if (Network.instance) {
|
||||
// eslint-disable-next-line no-constructor-return
|
||||
return Network.instance;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,13 +27,14 @@ const parse = (state: SecIni[]) => {
|
||||
};
|
||||
|
||||
class NfsSec extends ArrayState {
|
||||
public channel = 'nsf-sec';
|
||||
private static instance: NfsSec;
|
||||
public channel = 'nsf-sec';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
if (NfsSec.instance) {
|
||||
// eslint-disable-next-line no-constructor-return
|
||||
return NfsSec.instance;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,13 +41,14 @@ const parse = (state: SharesIni[]): Share[] => {
|
||||
};
|
||||
|
||||
class Shares extends ArrayState {
|
||||
public channel = 'shares';
|
||||
private static instance: Shares;
|
||||
public channel = 'shares';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
if (Shares.instance) {
|
||||
// eslint-disable-next-line no-constructor-return
|
||||
return Shares.instance;
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ const parse = (state: SlotIni[]) => {
|
||||
fsSize: toNumber(slot.fsSize),
|
||||
fsFree: toNumber(slot.fsFree),
|
||||
exportable: slot.exportable === 'e',
|
||||
fsColor: slot.fsColor && slot.fsColor.replace('-', '_')
|
||||
fsColor: slot.fsColor.replace('-', '_')
|
||||
};
|
||||
|
||||
return result;
|
||||
@@ -77,6 +77,7 @@ class Slots extends ArrayState {
|
||||
super();
|
||||
|
||||
if (Slots.instance) {
|
||||
// eslint-disable-next-line no-constructor-return
|
||||
return Slots.instance;
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ class SmbSec extends ArrayState {
|
||||
super();
|
||||
|
||||
if (SmbSec.instance) {
|
||||
// eslint-disable-next-line no-constructor-return
|
||||
return SmbSec.instance;
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ class Users extends ArrayState {
|
||||
super();
|
||||
|
||||
if (Users.instance) {
|
||||
// eslint-disable-next-line no-constructor-return
|
||||
return Users.instance;
|
||||
}
|
||||
|
||||
|
||||
@@ -284,7 +284,7 @@ class VarState extends State {
|
||||
super();
|
||||
|
||||
if (VarState.instance) {
|
||||
// eslint-ignore-next-line no-constructor-return
|
||||
// eslint-disable-next-line no-constructor-return
|
||||
return VarState.instance;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* Network interface
|
||||
* @interface NetworkInterface
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface NetworkInterface {
|
||||
// DHCP_KEEPRESOLV="no"
|
||||
// DNS_SERVER1="1.1.1.1"
|
||||
|
||||
@@ -25,4 +25,4 @@ const client = new Docker({
|
||||
/**
|
||||
* Docker client
|
||||
*/
|
||||
export const docker = (pify(client));
|
||||
export const docker = pify(client) as unknown as Promisify<Docker>;
|
||||
|
||||
@@ -13,7 +13,7 @@ import { AppError } from '../../errors';
|
||||
const data = {};
|
||||
|
||||
const getSubEndpoint = () => {
|
||||
const httpPort = states.varState.data?.port;
|
||||
const httpPort: string = states.varState.data?.port;
|
||||
return `http://localhost:${httpPort}/sub`;
|
||||
};
|
||||
|
||||
@@ -50,62 +50,64 @@ const endpointToStateMapping = {
|
||||
};
|
||||
|
||||
const subscribe = async (endpoint: string) => {
|
||||
await sleep(1000).then(async () => {
|
||||
debugTimer(`subscribe(${endpoint})`);
|
||||
const response = await fetch(`${getSubEndpoint()}/${endpoint}`).catch(async () => {
|
||||
// If we throw then let's check if nchan is down
|
||||
// or if it's an actual error
|
||||
const isUp = await isNchanUp();
|
||||
// Wait 1s before subscribing
|
||||
await sleep(1000);
|
||||
|
||||
if (isUp) {
|
||||
throw new AppError(`Cannot connect to nchan at ${getSubEndpoint()}/${endpoint}`);
|
||||
}
|
||||
debugTimer(`subscribe(${endpoint})`);
|
||||
const response = await fetch(`${getSubEndpoint()}/${endpoint}`).catch(async () => {
|
||||
// If we throw then let's check if nchan is down
|
||||
// or if it's an actual error
|
||||
const isUp = await isNchanUp();
|
||||
|
||||
throw new AppError('Cannot connect to nchan');
|
||||
});
|
||||
|
||||
if (response.status === 502) {
|
||||
// Status 502 is a connection timeout error,
|
||||
// may happen when the connection was pending for too long,
|
||||
// and the remote server or a proxy closed it
|
||||
// let's reconnect
|
||||
await subscribe(endpoint);
|
||||
} else if (response.status === 200) {
|
||||
// Get and show the message
|
||||
const message = await response.text();
|
||||
|
||||
// Create endpoint field on data
|
||||
if (!data[endpoint]) {
|
||||
const fileName = endpoint + '.js';
|
||||
data[endpoint] = {
|
||||
handlerPath: path.resolve(__dirname, '../../states', fileName)
|
||||
};
|
||||
}
|
||||
|
||||
// Only re-run parser if the message changed
|
||||
if (data[endpoint].message !== message) {
|
||||
data[endpoint].updated = new Date();
|
||||
data[endpoint].message = message;
|
||||
|
||||
try {
|
||||
const state = parseConfig({
|
||||
file: message,
|
||||
type: 'ini'
|
||||
});
|
||||
|
||||
// Update state
|
||||
endpointToStateMapping[endpoint].parse(state);
|
||||
} catch { }
|
||||
}
|
||||
|
||||
debugTimer(`subscribe(${endpoint})`);
|
||||
} else {
|
||||
// An error - let's show it
|
||||
coreLogger.error(JSON.stringify(response));
|
||||
if (isUp) {
|
||||
throw new AppError(`Cannot connect to nchan at ${getSubEndpoint()}/${endpoint}`);
|
||||
}
|
||||
}).then(() => {
|
||||
subscribe(endpoint);
|
||||
|
||||
throw new AppError('Cannot connect to nchan');
|
||||
});
|
||||
|
||||
if (response.status === 502) {
|
||||
// Status 502 is a connection timeout error,
|
||||
// may happen when the connection was pending for too long,
|
||||
// and the remote server or a proxy closed it
|
||||
// let's reconnect
|
||||
await subscribe(endpoint);
|
||||
} else if (response.status === 200) {
|
||||
// Get and show the message
|
||||
const message = await response.text();
|
||||
|
||||
// Create endpoint field on data
|
||||
if (!data[endpoint]) {
|
||||
const fileName = endpoint + '.js';
|
||||
data[endpoint] = {
|
||||
handlerPath: path.resolve(__dirname, '../../states', fileName)
|
||||
};
|
||||
}
|
||||
|
||||
// Only re-run parser if the message changed
|
||||
if (data[endpoint].message !== message) {
|
||||
data[endpoint].updated = new Date();
|
||||
data[endpoint].message = message;
|
||||
|
||||
try {
|
||||
const state = parseConfig({
|
||||
file: message,
|
||||
type: 'ini'
|
||||
});
|
||||
|
||||
// Update state
|
||||
endpointToStateMapping[endpoint].parse(state);
|
||||
} catch { }
|
||||
}
|
||||
|
||||
debugTimer(`subscribe(${endpoint})`);
|
||||
} else {
|
||||
// An error - let's show it
|
||||
coreLogger.error(JSON.stringify(response));
|
||||
}
|
||||
|
||||
// Re-subscribe
|
||||
await subscribe(endpoint);
|
||||
};
|
||||
|
||||
export const subscribeToNchanEndpoint = async (endpoint: string) => {
|
||||
@@ -113,5 +115,6 @@ export const subscribeToNchanEndpoint = async (endpoint: string) => {
|
||||
throw new AppError(`Invalid nchan endpoint "${endpoint}".`);
|
||||
}
|
||||
|
||||
subscribe(endpoint);
|
||||
// Subscribe
|
||||
await subscribe(endpoint);
|
||||
};
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
* @param ms How many milliseconds to sleep for.
|
||||
*/
|
||||
export const atomicSleep = async (ms: number): Promise<any> => {
|
||||
return new Promise(resolve => {
|
||||
// eslint-disable-next-line no-undef
|
||||
return new Promise<void>(resolve => {
|
||||
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
||||
resolve();
|
||||
});
|
||||
|
||||
@@ -60,6 +60,7 @@ const fixObjectArrays = (object: Record<string, any>) => {
|
||||
|
||||
// An object without any array items
|
||||
const filteredObject = filterObject(object, (key, value) => {
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-regexp-exec
|
||||
const [_, name, index] = [...((key).match(/(.*):(\d+$)/) ?? [])];
|
||||
if (!name || !index) {
|
||||
return true;
|
||||
|
||||
@@ -7,4 +7,10 @@
|
||||
* Sleep for a certain amount of milliseconds.
|
||||
* @param ms How many milliseconds to sleep for.
|
||||
*/
|
||||
export const sleep = async (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
export const sleep = async (ms: number) => {
|
||||
return new Promise<void>(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, ms);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ export const validateApiKey = async (apiKey: string) => {
|
||||
const body = new FormData();
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
body.append(key, `${value}`);
|
||||
body.append(key, String(value));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ const encodeParameters = (parameters: LooseObject) => {
|
||||
|
||||
interface Options {
|
||||
/** File path */
|
||||
file;
|
||||
file: string;
|
||||
/** HTTP Method GET/POST */
|
||||
method?: string;
|
||||
/** Request query */
|
||||
|
||||
@@ -23,7 +23,7 @@ type Overload = {
|
||||
/**
|
||||
* Get all share types.
|
||||
*/
|
||||
export const getShares: Overload = (type?: any, filter?: any) => {
|
||||
export const getShares: Overload = (type?: string, filter?: Filter) => {
|
||||
const types = {
|
||||
user: (name?: string) => processShare('user', sharesState.findOne(name ? { name } : {})),
|
||||
users: () => sharesState.find().map(share => processShare('user', share)),
|
||||
@@ -34,7 +34,7 @@ export const getShares: Overload = (type?: any, filter?: any) => {
|
||||
// Return a type of share
|
||||
if (type) {
|
||||
if (!Object.keys(types).includes(type)) {
|
||||
throw new AppError(`Unknown type "${type}", valid types are ${Object.keys(types)}.`);
|
||||
throw new AppError(`Unknown type "${type}", valid types are ${Object.keys(types).join(', ')}.`);
|
||||
}
|
||||
|
||||
return types[type](filter?.name);
|
||||
|
||||
@@ -66,6 +66,7 @@ class Upcast {
|
||||
function: (value: unknown) => () => value,
|
||||
null: () => null,
|
||||
number: (value: unknown) => Number(value),
|
||||
// eslint-disable-next-line no-new-object
|
||||
object: (value: unknown) => new Object(value),
|
||||
string: (value: unknown) => String(value),
|
||||
undefined: () => undefined
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CoreContext } from '../../../types';
|
||||
import { FieldMissingError } from '../../../errors';
|
||||
|
||||
export const ensureData = (context: CoreContext, field) => {
|
||||
export const ensureData = (context: CoreContext, field: string) => {
|
||||
const hasData = context.data && Object.keys(context.data).includes(field);
|
||||
|
||||
if (!hasData) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CoreContext } from '../../../types';
|
||||
import { FieldMissingError } from '../../../errors';
|
||||
|
||||
export const ensureParameter = (context: CoreContext, field: any) => {
|
||||
export const ensureParameter = (context: CoreContext, field: string) => {
|
||||
const hasParameter = context.params && Object.keys(context.params).includes(field);
|
||||
|
||||
if (!hasParameter) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CoreContext } from '../../../types';
|
||||
import { FieldMissingError } from '../../../errors';
|
||||
|
||||
export const ensureQuery = (context: CoreContext, field: any) => {
|
||||
export const ensureQuery = (context: CoreContext, field: string) => {
|
||||
const hasQuery = context.query && Object.keys(context.query).includes(field);
|
||||
|
||||
if (!hasQuery) {
|
||||
|
||||
@@ -24,7 +24,7 @@ export const parseDomain = async (type: DomainLookupType, id: string): Promise<D
|
||||
};
|
||||
|
||||
if (!type || !Object.keys(types).includes(type)) {
|
||||
throw new Error(`Type must be one of [${Object.keys(types)}], ${type} given.`);
|
||||
throw new Error(`Type must be one of [${Object.keys(types).join(', ')}], ${type} given.`);
|
||||
}
|
||||
|
||||
const client = await getHypervisor();
|
||||
|
||||
@@ -3,13 +3,13 @@ import path from 'path';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import { coreLogger } from '../log';
|
||||
|
||||
const writeFile = (filePath: string, fileContents: string | Buffer) => {
|
||||
const writeFile = async (filePath: string, fileContents: string | Buffer) => {
|
||||
coreLogger.debug(`Writing ${prettyBytes(fileContents.length)} to ${filePath}`);
|
||||
fs.promises.writeFile(filePath, fileContents);
|
||||
await fs.promises.writeFile(filePath, fileContents);
|
||||
};
|
||||
|
||||
export const writeToBoot = (filePath: string, fileContents: string | Buffer) => {
|
||||
export const writeToBoot = async (filePath: string, fileContents: string | Buffer) => {
|
||||
const basePath = '/boot/config/plugins/dynamix/';
|
||||
const resolvedPath = path.resolve(basePath, filePath);
|
||||
writeFile(resolvedPath, fileContents);
|
||||
await writeFile(resolvedPath, fileContents);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import get from 'lodash.get';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import * as core from '../core';
|
||||
// eslint-disable-next-line @typescript-eslint/no-duplicate-imports
|
||||
import { bus, apiManager, graphqlLogger, config, pluginManager, modules, coreLogger } from '../core';
|
||||
import { AppError, FatalAppError, PluginError } from '../core/errors';
|
||||
import { usersState } from '../core/states';
|
||||
@@ -163,10 +164,19 @@ const getPluginModule = (pluginName: string, pluginModuleName: string) => {
|
||||
*/
|
||||
class FuncDirective extends SchemaDirectiveVisitor {
|
||||
visitFieldDefinition(field: Record<string, any>) {
|
||||
// @ts-expect-error
|
||||
const { args } = this;
|
||||
field.resolve = async function (_source, directiveArgs: Record<string, any>, { user }, info: Record<string, any>) {
|
||||
const { module: moduleName, result: resultType } = args;
|
||||
field.resolve = async function (_source, directiveArgs: {
|
||||
[key: string]: string | any;
|
||||
plugin: string;
|
||||
module: string;
|
||||
result: string;
|
||||
input: Record<string, any>;
|
||||
query: Record<string, any>;
|
||||
}, { user }, info: Record<string, any>) {
|
||||
const { module: moduleName, result: resultType } = args as {
|
||||
module: string;
|
||||
result: string;
|
||||
};
|
||||
const { plugin: pluginName, module: pluginModuleName, result: pluginType, input, ...params } = directiveArgs;
|
||||
const operationType = info.operation.operation;
|
||||
const query = {
|
||||
@@ -192,6 +202,7 @@ class FuncDirective extends SchemaDirectiveVisitor {
|
||||
if (pluginName) {
|
||||
// @ts-expect-error
|
||||
const { filePath } = getPluginModule(pluginName, pluginModuleName);
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const pluginModule = require(filePath);
|
||||
// The file will either use a default export or a named one
|
||||
// If it's named it should be the same as a module name
|
||||
@@ -203,7 +214,7 @@ class FuncDirective extends SchemaDirectiveVisitor {
|
||||
if (isNodeError(error, AppError)) {
|
||||
// Rethrow clean error message about module being missing
|
||||
if (error.code === 'MODULE_NOT_FOUND') {
|
||||
throw new AppError(`Cannot find ${pluginName ? 'Plugin: "' + pluginName + '" ' : ''}Module: "${pluginName ? pluginModuleName : moduleName}"`);
|
||||
throw new AppError(`Cannot find ${pluginName ? `Plugin: "${pluginName}" ` : ''}Module: "${pluginName ? pluginModuleName : moduleName}"`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,7 +327,7 @@ bus.on('varstate', async data => {
|
||||
const user = usersState.findOne({ name: 'root' });
|
||||
|
||||
if (user) {
|
||||
publish('info', 'UPDATED', {
|
||||
await publish('info', 'UPDATED', {
|
||||
os: {
|
||||
hostname
|
||||
}
|
||||
@@ -338,7 +349,7 @@ dee.on('*', async (data: { Type: string }) => {
|
||||
const { json } = await modules.getAppCount({
|
||||
user
|
||||
});
|
||||
publish('info', 'UPDATED', {
|
||||
await publish('info', 'UPDATED', {
|
||||
apps: json
|
||||
});
|
||||
}
|
||||
@@ -391,7 +402,7 @@ export const graphql = {
|
||||
resolve({
|
||||
user,
|
||||
websocketId
|
||||
}); return;
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
reject(error);
|
||||
}
|
||||
@@ -416,7 +427,12 @@ export const graphql = {
|
||||
return;
|
||||
}
|
||||
|
||||
const { user, websocketId } = context;
|
||||
const { user, websocketId } = context as {
|
||||
user: {
|
||||
name: string;
|
||||
};
|
||||
websocketId: string;
|
||||
};
|
||||
graphqlLogger.debug(`<ws> ${user.name}[${websocketId}] disconnected.`);
|
||||
|
||||
// Update ws connection count and other needed values
|
||||
|
||||
@@ -64,7 +64,10 @@ export const Subscription = {
|
||||
...createSubscription('vms/domains')
|
||||
},
|
||||
pluginModule: {
|
||||
subscribe: async (_: unknown, directiveArgs, context: Context) => {
|
||||
subscribe: async (_: unknown, directiveArgs: {
|
||||
plugin: string;
|
||||
module: string;
|
||||
}, context: Context) => {
|
||||
const { plugin: pluginName, module: pluginModuleName } = directiveArgs;
|
||||
const channel = `${pluginName}/${pluginModuleName}`;
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export const createSubscription = (channel: string, resource?: string) => ({
|
||||
|
||||
// Check the user has permissison to subscribe to this endpoint
|
||||
ensurePermission(context.user, {
|
||||
resource: resource || channel,
|
||||
resource: resource ?? channel,
|
||||
action: 'read',
|
||||
possession: 'any'
|
||||
});
|
||||
@@ -83,8 +83,9 @@ export const getServers = async (): Promise<Server[]> => {
|
||||
const guid = varState?.data?.regGuid;
|
||||
const name = varState?.data?.name;
|
||||
const wanip = null;
|
||||
const lanip = networkState.data[0].ipaddr[0];
|
||||
const localurl = `http://${lanip}:${varState?.data?.port}`;
|
||||
const lanip: string = networkState.data[0].ipaddr[0];
|
||||
const port: string = varState?.data?.port;
|
||||
const localurl = `http://${lanip}:${port}`;
|
||||
const remoteurl = null;
|
||||
|
||||
return [{
|
||||
|
||||
@@ -75,21 +75,24 @@ export class CustomSocket {
|
||||
}
|
||||
|
||||
public onConnect() {
|
||||
const logger = this.logger;
|
||||
const connection = this.connection;
|
||||
const apiKey = this.apiKey;
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const customSocket = this;
|
||||
return async function (this: WebSocketWithHeartBeat) {
|
||||
try {
|
||||
const apiKey = customSocket.apiKey;
|
||||
if (!apiKey || (typeof apiKey === 'string' && apiKey.length === 0)) {
|
||||
throw new AppError('Missing key', 4422);
|
||||
}
|
||||
|
||||
customSocket.logger.debug('Connected via %s.', customSocket.connection?.url);
|
||||
logger.debug('Connected via %s.', connection?.url);
|
||||
|
||||
// Reset connection attempts
|
||||
customSocket.connectionAttempts = 0;
|
||||
} catch (error: unknown) {
|
||||
if (isNodeError(error, AppError)) {
|
||||
this.close(Number(error.code?.length === 4 ? error.code : `4${error.code}`), JSON.stringify({
|
||||
this.close(Number(error.code?.length === 4 ? error.code : `4${error.code ?? 500}`), JSON.stringify({
|
||||
message: error.message ?? 'Internal Server Error'
|
||||
}));
|
||||
}
|
||||
@@ -97,24 +100,86 @@ export class CustomSocket {
|
||||
};
|
||||
}
|
||||
|
||||
public onMessage() {
|
||||
const logger = this.logger;
|
||||
return async function (message: string, ...args: any[]) {
|
||||
logger.silly('message="%s" args="%s"', message, ...args);
|
||||
};
|
||||
}
|
||||
|
||||
public async connect(retryAttempt = 0) {
|
||||
const lock = await this.getLock();
|
||||
try {
|
||||
// Set retry attempt count
|
||||
await this.setRetryAttempt(retryAttempt);
|
||||
|
||||
// Get the current apiKey
|
||||
this.apiKey = await this.getApiKey();
|
||||
|
||||
// Check the connection is allowed
|
||||
await this.isConnectionAllowed();
|
||||
|
||||
// Cleanup old connections
|
||||
await this.cleanup();
|
||||
|
||||
// Connect to endpoint
|
||||
await this._connect();
|
||||
|
||||
// Log we connected
|
||||
this.logger.debug('Connected to %s', this.uri);
|
||||
} catch (error: unknown) {
|
||||
if (isNodeError(error, AppError)) {
|
||||
this.logger.error('Failed connecting reason=%s', error.message);
|
||||
}
|
||||
} finally {
|
||||
lock.release();
|
||||
}
|
||||
}
|
||||
|
||||
public async disconnect() {
|
||||
const lock = await this.getLock();
|
||||
try {
|
||||
if (this.connection && (this.connection.readyState !== this.connection.CLOSED)) {
|
||||
// 4200 === ok
|
||||
this.connection.close(4200);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (isNodeError(error, AppError)) {
|
||||
this.logger.error('Failed disconnecting reason=%s', error.message);
|
||||
}
|
||||
} finally {
|
||||
lock.release();
|
||||
}
|
||||
}
|
||||
|
||||
public async reconnect() {
|
||||
await this.disconnect();
|
||||
await sleep(1000);
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
protected onDisconnect() {
|
||||
const logger = this.logger;
|
||||
const connect = this.connect.bind(this);
|
||||
const connectionAttempts = this.connectionAttempts;
|
||||
const uri = this.uri;
|
||||
const responses = {
|
||||
// OK
|
||||
4200: async () => {
|
||||
// This is usually because the API key is updated
|
||||
// Let's reset the reconnect count so we reconnect instantly
|
||||
customSocket.connectionAttempts = 0;
|
||||
this.connectionAttempts = 0;
|
||||
},
|
||||
// Unauthorized - Invalid/missing API key.
|
||||
4401: async () => {
|
||||
customSocket.logger.debug('Invalid API key, waiting for new key...');
|
||||
this.logger.debug('Invalid API key, waiting for new key...');
|
||||
},
|
||||
// Rate limited
|
||||
4429: async message => {
|
||||
try {
|
||||
let interval: NodeJS.Timeout | undefined;
|
||||
const retryAfter = parseInt(message['Retry-After'], 10) || 30;
|
||||
customSocket.logger.debug('Rate limited, retrying after %ss', retryAfter);
|
||||
this.logger.debug('Rate limited, retrying after %ss', retryAfter);
|
||||
|
||||
// Less than 30s
|
||||
if (retryAfter <= 30) {
|
||||
@@ -123,7 +188,7 @@ export class CustomSocket {
|
||||
// Print retry once per second
|
||||
interval = setInterval(() => {
|
||||
seconds--;
|
||||
customSocket.logger.debug('Retrying connection in %ss', seconds);
|
||||
this.logger.debug('Retrying connection in %ss', seconds);
|
||||
}, ONE_SECOND);
|
||||
}
|
||||
|
||||
@@ -143,11 +208,10 @@ export class CustomSocket {
|
||||
await sleep(ONE_SECOND * 5);
|
||||
}
|
||||
};
|
||||
const customSocket = this;
|
||||
return async function (this: WebSocketWithHeartBeat, code: number, _message: string) {
|
||||
try {
|
||||
const message: { message?: string } = _message.trim() === '' ? { message: '' } : JSON.parse(_message);
|
||||
customSocket.logger.debug('Connection closed with code=%s reason="%s"', code, code === 1006 ? 'Terminated' : message.message);
|
||||
logger.debug('Connection closed with code=%s reason="%s"', code, code === 1006 ? 'Terminated' : message.message);
|
||||
|
||||
// Stop ws heartbeat
|
||||
if (this.heartbeat) {
|
||||
@@ -163,31 +227,24 @@ export class CustomSocket {
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (isNodeError(error, AppError)) {
|
||||
customSocket.logger.debug('Connection closed with code=%s reason="%s"', code, error.message);
|
||||
logger.debug('Connection closed with code=%s reason="%s"', code, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Wait a few seconds
|
||||
await sleep(backoff(customSocket.connectionAttempts, ONE_MINUTE, 5));
|
||||
await sleep(backoff(connectionAttempts, ONE_MINUTE, 5));
|
||||
|
||||
// Reconnect
|
||||
await customSocket.connect(customSocket.connectionAttempts + 1);
|
||||
await connect(connectionAttempts + 1);
|
||||
} catch (error: unknown) {
|
||||
if (isNodeError(error, AppError)) {
|
||||
customSocket.logger.debug('Failed reconnecting to %s reason="%s"', customSocket.uri, error.message);
|
||||
logger.debug('Failed reconnecting to %s reason="%s"', uri, error.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public onMessage() {
|
||||
const customSocket = this;
|
||||
return async function (message: string, ...args: any[]) {
|
||||
customSocket.logger.silly('message="%s" args="%s"', message, ...args);
|
||||
};
|
||||
}
|
||||
|
||||
protected async cleanup() {
|
||||
// Kill existing socket connection
|
||||
if (this.connection) {
|
||||
@@ -263,61 +320,5 @@ export class CustomSocket {
|
||||
this.connection.on('close', this.onDisconnect());
|
||||
this.connection.on('open', this.onConnect());
|
||||
this.connection.on('message', this.onMessage());
|
||||
// This.connection.on('ping', console.log);
|
||||
// this.connection.on('error', console.log);
|
||||
// this.connection.on('close', console.log);
|
||||
// this.connection.on('open', console.log);
|
||||
// this.connection.on('message', console.log);
|
||||
}
|
||||
|
||||
public async connect(retryAttempt = 0) {
|
||||
const lock = await this.getLock();
|
||||
try {
|
||||
// Set retry attempt count
|
||||
await this.setRetryAttempt(retryAttempt);
|
||||
|
||||
// Get the current apiKey
|
||||
this.apiKey = await this.getApiKey();
|
||||
|
||||
// Check the connection is allowed
|
||||
await this.isConnectionAllowed();
|
||||
|
||||
// Cleanup old connections
|
||||
await this.cleanup();
|
||||
|
||||
// Connect to endpoint
|
||||
await this._connect();
|
||||
|
||||
// Log we connected
|
||||
this.logger.debug('Connected to %s', this.uri);
|
||||
} catch (error: unknown) {
|
||||
if (isNodeError(error, AppError)) {
|
||||
this.logger.error('Failed connecting reason=%s', error.message);
|
||||
}
|
||||
} finally {
|
||||
lock.release();
|
||||
}
|
||||
}
|
||||
|
||||
public async disconnect() {
|
||||
const lock = await this.getLock();
|
||||
try {
|
||||
if (this.connection && (this.connection.readyState !== this.connection.CLOSED)) {
|
||||
// 4200 === ok
|
||||
this.connection.close(4200);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (isNodeError(error, AppError)) {
|
||||
this.logger.error('Failed disconnecting reason=%s', error.message);
|
||||
}
|
||||
} finally {
|
||||
lock.release();
|
||||
}
|
||||
}
|
||||
|
||||
public async reconnect() {
|
||||
await this.disconnect();
|
||||
await sleep(1000);
|
||||
await this.connect();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export class InternalGraphql extends CustomSocket {
|
||||
await sleep(1000);
|
||||
|
||||
// Re-connect to internal graphql server
|
||||
connect();
|
||||
await connect();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export const subscribeToServers = async (apiKey: string) => {
|
||||
|
||||
// Subscribe
|
||||
const subscription = query.subscribe({
|
||||
next: ({ data, errors }) => {
|
||||
next: async ({ data, errors }) => {
|
||||
log.silly('Got data back with %s errors', errors?.length ?? 0);
|
||||
log.silly('Got data %s', data);
|
||||
log.silly('Got errors %s', errors);
|
||||
@@ -54,7 +54,7 @@ export const subscribeToServers = async (apiKey: string) => {
|
||||
});
|
||||
|
||||
// Update subscribers
|
||||
pubsub.publish('servers', {
|
||||
await pubsub.publish('servers', {
|
||||
servers: data.servers
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CoreContext, CoreResult } from './core/types';
|
||||
import type { CoreContext, CoreResult, Result } from './core/types';
|
||||
import { pubsub, coreLogger } from './core';
|
||||
import { debugTimer, isNodeError } from './core/utils';
|
||||
import { AppError } from './core/errors';
|
||||
@@ -27,7 +27,7 @@ export const publish = async (channel: string, mutation: string, node?: Record<s
|
||||
|
||||
interface RunOptions {
|
||||
node?: Record<string, unknown>;
|
||||
moduleToRun?: (context: CoreContext) => Promise<CoreResult>;
|
||||
moduleToRun?: (context: CoreContext) => Promise<CoreResult | Result> | CoreResult | Result;
|
||||
context?: any;
|
||||
}
|
||||
|
||||
|
||||
3225
package-lock.json
generated
3225
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -12,9 +12,9 @@
|
||||
"copy-schemas": "cpx app/**/*.graphql dist/types",
|
||||
"clean": "modclean --no-progress --run --path .",
|
||||
"commit": "npx git-cz",
|
||||
"lint": "xo --verbose",
|
||||
"lint:quiet": "xo --quiet",
|
||||
"lint:fix": "xo --fix",
|
||||
"lint": "eslint app/**/*.ts",
|
||||
"lint:quiet": "eslint --quiet",
|
||||
"lint:fix": "eslint --fix",
|
||||
"test": "npx nyc@latest ava",
|
||||
"cover": "npm run cover:unit && npm run cover:integration && npm run cover:report",
|
||||
"cover:unit": "nyc --silent npm run test:unit",
|
||||
@@ -124,7 +124,10 @@
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^11.0.0",
|
||||
"@commitlint/config-conventional": "^11.0.0",
|
||||
"@types/cli-table": "^0.3.0",
|
||||
"@types/dockerode": "^3.2.2",
|
||||
"@types/lodash.get": "^4.4.6",
|
||||
"@types/pify": "^5.0.0",
|
||||
"@types/semver-regex": "^3.1.0",
|
||||
"@types/stoppable": "^1.1.0",
|
||||
"@types/supertest": "^2.0.10",
|
||||
@@ -135,6 +138,7 @@
|
||||
"cpx": "1.5.0",
|
||||
"cz-conventional-changelog": "3.3.0",
|
||||
"eslint": "^7.18.0",
|
||||
"esprint": "^2.0.0",
|
||||
"husky": "4.3.8",
|
||||
"modclean": "^3.0.0-beta.1",
|
||||
"node-env-run": "^4.0.2",
|
||||
@@ -143,9 +147,9 @@
|
||||
"standard-version": "^9.1.0",
|
||||
"supertest": "^6.0.1",
|
||||
"ts-node": "9.1.1",
|
||||
"tsup": "^3.12.0",
|
||||
"typescript": "4.1.3",
|
||||
"typescript-coverage-report": "^0.4.0",
|
||||
"xo": "0.37.1"
|
||||
"typescript-coverage-report": "^0.4.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"libvirt": "^1.2.1"
|
||||
@@ -160,11 +164,6 @@
|
||||
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
|
||||
}
|
||||
},
|
||||
"xo": {
|
||||
"rules": {
|
||||
"max-params": 0
|
||||
}
|
||||
},
|
||||
"bundledDependencies": [
|
||||
"@apollo/client",
|
||||
"@graphql-tools/load-files",
|
||||
|
||||
@@ -68,6 +68,6 @@
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
|
||||
/* Advanced Options */
|
||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
|
||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user