@alius/rpc
Version:
JSON-RPC 2.0 implementation configured with OpenRPC
1,494 lines (1,418 loc) • 46.1 kB
JavaScript
"use strict";
import Ajv from "ajv";
import addFormats from "ajv-formats";
import RefParser from "@apidevtools/json-schema-ref-parser";
import { Exception } from "@alius/exception";
import { ObjectUtils } from "@alius/utils";
import openrpcSchema from "./openrpc.schema.json" assert { type: "json" };
import jsonrpcRequestSchema from "./jsonrpc.request.schema.json" assert { type: "json" };
import emptyConfig from "./empty.openrpc.json" assert { type: "json" };
import rpcDiscoverDef from "./rpc.discover.json" assert { type: "json" };
/**
* JSON-RPC 2.0 provider.
* OpenRPC is used to provide API configuration.
*/
class RPC {
/** @type {Function} */
#initFunction;
/** @type {Ajv} */
#ajv;
/** @type {OpenRPC} */
#config;
/** @type {Object<string, Function>} */
#handlers;
/** @type {RPCOptionsType} */
#options;
/** @type {JSONRPCRequest} */
#json;
/**
* Creates new RPC provider instance with specified config and handlers.
* <b>{@link RPC#init await init()} has to be executed to finish asynchronous initalization
* of this object.</b>
* <p>Every method specified in config must have corresponding handler.</p>
*
* @param {OpenRPC} [config="./empty.openrpc.json"] - API config
* @param {Object<string, Function>} [handlers={}] - handlers for methods
* @param {RPCOptionsType} [options={}] - additional options
*/
constructor(config, handlers = {}, options = {}) {
this.#ajv = new Ajv({
strict: false,
allowUnionTypes: true,
});
addFormats(this.#ajv);
this.#ajv.addSchema(jsonrpcRequestSchema);
this.#ajv.addSchema(openrpcSchema);
if (!config) {
config = emptyConfig;
handlers = {};
}
if (!handlers) {
handlers = {};
}
this.#json = null;
// asynchronous part of initialization will be executed in init()
this.#initFunction = async () => {
await this.configure(config, handlers, options);
};
}
/**
* true when object initialization is complete.
*
* @type {boolean}
* @readonly
*/
get initialized() {
return !this.#initFunction;
}
/**
* JSON validator.
*
* @type {Ajv}
* @readonly
*/
get ajv() {
return this.#ajv;
}
/**
* JSON request object being processed.
* <strong>Accessing this property must be done in current task of JS event loop.</strong>
*
* @type {JSONRPCRequest}
* @readonly
*/
get json() {
return this.#json;
}
/**
* Initialized asynchronous part of this object.
*/
async init() {
if (!this.initialized) {
await this.#initFunction();
this.#initFunction = null;
}
return this;
}
/**
* Configures this RPC provider with new configuration.
*
* @param {OpenRPC} config - API config
* @param {Object<string, Function>} [handlers={}] - handlers for methods
* @param {RPCOptionsType} [options={}] - additional options
*/
async configure(config, handlers = {}, options = {}) {
RPC.assert(config && typeof config === "object", "config must be object");
// methods array is provided before dereferencing
if (Array.isArray(config.methods)) {
// Configuration for 'rpc.discover' method has result schema same as whole configuration.
// Trying to validate it results in circular schema.
// Test here to avoid validation.
RPC.assert(
!config.methods.find(
(method) =>
// @ts-ignore
method && method.name === RPC.RPC_DISCOVER
),
`${RPC.RPC_DISCOVER} method is reserved for service discovery`
);
}
// deep-clone config to avoid changes from outside
config = ObjectUtils.deepClone(config);
// @ts-ignore
config = await RefParser.dereference(config);
RPC.assert(
this.ajv.validate(openrpcSchema.$id, config),
"config must be valid OpenRPC object"
);
RPC.assert(
// @ts-ignore
!config.methods.find((method) => method.name === RPC.RPC_DISCOVER),
`${RPC.RPC_DISCOVER} method is reserved for service discovery`
);
if (!handlers) {
handlers = {};
}
RPC.assert(typeof handlers === "object", "handlers must be object");
// deep-clone handlers to avoid changes from outside
handlers = ObjectUtils.deepClone(handlers);
for (const method of config.methods) {
RPC.assert(
// @ts-ignore
typeof handlers[method.name] === "function",
// @ts-ignore
`handler for method ${method.name} must be function`
);
}
RPC.assert(
options && typeof options === "object",
"options must be object"
);
// deep-clone options to avoid changes from outside
options = ObjectUtils.deepClone(options);
if (typeof options.preProcessors !== "undefined") {
this.#assertPreProcessors(options.preProcessors);
} else {
options.preProcessors = [];
}
if (typeof options.postProcessors !== "undefined") {
this.#assertPostProcessors(options.postProcessors);
} else {
options.postProcessors = [];
}
// Save configuration copy
this.#config = config;
this.#handlers = handlers;
this.#options = options;
this.#buildValidators();
}
// adds validators for all methods' parameters and results
#buildValidators() {
// remove any schemas left from previous configuration
this.ajv.removeSchema(new RegExp("^param/"));
this.ajv.removeSchema(new RegExp("^result/"));
// adding schemas for methods' parameters and results
for (const method of this.#config.methods) {
// @ts-ignore
for (const param of method.params) {
// @ts-ignore
this.ajv.addSchema(param.schema, `param/${method.name}/${param.name}`);
}
// @ts-ignore
this.ajv.addSchema(method.result.schema, `result/${method.name}`);
}
}
/**
* Handler for 'rpc.discover' method.
*
* @returns {OpenRPC} current API configuration
*/
serviceDiscover() {
RPC.assert(this.initialized, `${this.constructor.name} is not initialized`);
return ObjectUtils.deepClone(this.#config);
}
/**
* Adds new method to API.
* If method with such name already exists - replaces it with provided config and handler.
*
* @param {OpenRPCMethod} config - method configuration
* @param {function} handler - method handler
*/
async putMethod(config, handler) {
RPC.assert(this.initialized, `${this.constructor.name} is not initialized`);
RPC.assert(config && typeof config === "object", "config must be object");
// method name is checked before dereferencing
if (config.name) {
// Configuration for 'rpc.discover' method has result schema same as whole configuration.
// Trying to validate it results in circular schema.
// Test here to avoid validation
RPC.assert(
config.name !== RPC.RPC_DISCOVER,
`method '${RPC.RPC_DISCOVER}' can not be replaced`
);
}
config = ObjectUtils.deepClone(config);
// @ts-ignore
config = await RefParser.dereference(config);
RPC.assert(
this.ajv.validate(
`${openrpcSchema.$id}#/definitions/methodObject`,
config
),
"config must be valid OpenRPC methodObject"
);
RPC.assert(typeof handler === "function", "handler must be function");
RPC.assert(
config.name !== RPC.RPC_DISCOVER,
`method '${RPC.RPC_DISCOVER}' can not be replaced`
);
this.removeMethod(config.name);
this.#config.methods.push(config);
this.#handlers[config.name] = handler;
this.#buildValidators();
}
/**
* Removes named method from API.
* If there is no method with provided name - silently returns.
*
* @param {string} name - method name to remove
*/
removeMethod(name) {
RPC.assert(this.initialized, `${this.constructor.name} is not initialized`);
RPC.assert(
name && typeof name === "string",
"name must be non-empty string"
);
RPC.assert(
name !== RPC.RPC_DISCOVER,
`method '${RPC.RPC_DISCOVER}' can not be removed`
);
const index = this.#config.methods.findIndex(
// @ts-ignore
(method) => method.name === name
);
if (index >= 0) {
this.#config.methods.splice(index, 1);
delete this.#handlers[name];
this.#buildValidators();
}
}
/**
* Returns method config.
* Returns <code>undefined</code> if method with such name is not found.
*
* @param {string} name - method name
* @returns {OpenRPCMethod|undefined} method config
*/
getMethodConfig(name) {
RPC.assert(this.initialized, `${this.constructor.name} is not initialized`);
// @ts-ignore
return this.#config.methods.find((method) => method.name === name);
}
/**
* Executes single JSON-RPC request or batch of requests.
* Execution order:<ul>
* <li>parses request if string passed</li>
* <li>if single request is passed - executes it with {@link RPC#executeSingle executeSingle} and returns response</li>
* <li>if empty batch is passed - returns invalid request error (-32600)</li>
* <li>executes all requests with {@link RPC#executeSingle executeSingle()} and collects responses</li>
* <li>if there are no responses (batch contains notifications only) - returns</li>
* <li>returns responses</li>
* </ul>
*
* @param {(Array<JSONRPCRequest>|JSONRPCRequest|string)} json - JSON-RPC request as object or encoded in JSON string
* @param {object} [options] - processing options
* @param {Array<PreProcessorFunction>} [options.preProcessors] - list of additional pre-processor functions
* will be executed before configured pre-processors
* @param {boolean} [options.skipPreProcessors=false] - skips configured pre-processors if true
* @param {Array<PostProcessorFunction>} [options.postProcessors] - list of additional post-processor functions
* will be executed after configured post-processors
* @param {boolean} [options.skipPostProcessors=false] - skips configured post-processors if true
* @returns {Promise<JSONRPCResult|JSONRPCError|Array<JSONRPCResult|JSONRPCError>|undefined>} response object or undefined (for notifications)
*/
async execute(json, options = {}) {
RPC.assert(this.initialized, `${this.constructor.name} is not initialized`);
// if string is passed - parse JSON
if (typeof json === "string") {
try {
json = JSON.parse(json);
} catch (err) {
// Failure to parse JSON always results in parse error response
const e = RPC.ERROR_32700;
e[RPC.ERROR_DATA] = Exception.errSerializer(err);
return RPC.errorResponse(e, null);
}
}
if (!Array.isArray(json)) {
return this.executeSingle(json, options);
}
if (json.length <= 0) {
// Empty batch results in invalid request error (-32600)
return RPC.errorResponse(RPC.ERROR_32600, null);
}
const requests = [];
/** @type {Array<JSONRPCResult|JSONRPCError>} */
const responses = [];
for (const request of json) {
requests.push(
(async () => {
const response = await this.executeSingle(request, options);
if (typeof response !== "undefined") {
responses.push(response);
}
})()
);
}
await Promise.all(requests);
if (responses.length > 0) {
return responses;
}
return;
}
/**
* Executes single JSON-RPC request.
* Execution order:<ul>
* <li>parses request if string passed</li>
* <li>validates request</li>
* <li>tests if API has handler for requested method</li>
* <li>executes additional pre-processors</li>
* <li>executes configured pre-processors</li>
* <li>matches params by name if passed as object - excesive parameters are discarded</li>
* <li>validates params - excesive parameters are discarded</li>
* <li>tests if values are provided for all params marked required</li>
* <li>executes handler with provided parameters</li>
* <li>validates response (on validation failure - changes response to error (-32030)
* with original response saved in 'data' property)</li>
* <li>executes configured post-processors</li>
* <li>executes additional post-processors</li>
* <li>on successful execution<ul>
* <li>returns if request was notification</li>
* <li>tries to stringify result to JSON to make sure that valid JSON can be produced
* (on failure to stringify - returns stringify error (-32000) with stringify error
* saved in 'data' property)</li>
* <li>returns result response</li>
* </ul></li>
* <li>on execution failure<ul>
* <li>if error code id thrown - tries to find error object among defined errors by code;
* returns error object if found</li>
* <li>if error object thrown - tries to find error object among defined errors by err.code;
* if found - replaces 'message' and 'data' default property values with provided (if provided)
* and returns resulting error object</li>
* <li>if predefined error can not be found - returns method execution error (-32020) object
* with original error serialized in 'data' property.</li>
* </ul></li>
* </ul>
* <p>If parameter <code>json</code> value is object (not string) - it can be modified
* by pre/post-processors. General rule - do not reuse request objects.
* If you decide to reuse request object - take necessary precautions to prevent unpredictable results.</p>
*
* @param {(JSONRPCRequest|string)} json - JSON-RPC request as object or encoded in JSON string
* @param {object} [options] - processing options
* @param {Array<PreProcessorFunction>} [options.preProcessors] - list of additional pre-processor functions
* will be executed before configured pre-processors
* @param {boolean} [options.skipPreProcessors=false] - skips configured pre-processors if true
* @param {Array<PostProcessorFunction>} [options.postProcessors] - list of additional post-processor functions
* will be executed after configured post-processors
* @param {boolean} [options.skipPostProcessors=false] - skips configured post-processors if true
* @returns {Promise<JSONRPCResult|JSONRPCError|undefined>} response object or undefined (for notifications)
*/
async executeSingle(json, options = {}) {
RPC.assert(this.initialized, `${this.constructor.name} is not initialized`);
RPC.assert(
options && typeof options === "object",
"options must be object"
);
if (typeof options.preProcessors !== "undefined") {
this.#assertPreProcessors(options.preProcessors);
} else {
options.preProcessors = [];
}
if (typeof options.postProcessors !== "undefined") {
this.#assertPostProcessors(options.postProcessors);
} else {
options.postProcessors = [];
}
// if string is passed - parse JSON
if (typeof json === "string") {
try {
json = JSON.parse(json);
} catch (err) {
// failure to parse JSON always results in parse error response
const e = RPC.ERROR_32700;
e[RPC.ERROR_DATA] = Exception.errSerializer(err);
return RPC.errorResponse(e, null);
}
}
if (!this.ajv.validate(jsonrpcRequestSchema.$id, json)) {
// json parameter (after parsing) is not valid request object
// always results in invalid request response
return RPC.errorResponse(RPC.ERROR_32600, null);
}
const id = json[RPC.ID];
const methodName = json[RPC.METHOD];
// special handler for 'rpc.discover' method
if (RPC.RPC_DISCOVER === methodName) {
if (typeof id !== "undefined") {
// @ts-ignore
return {
[RPC.JSONRPC]: RPC.VERSION,
[RPC.ID]: id,
[RPC.RESULT]: this.serviceDiscover(),
};
}
return;
}
const method = this.#config.methods.find(
// @ts-ignore
(method) => method.name === methodName
);
const handler = this.#handlers[methodName];
if (!method || !handler) {
return RPC.errorResponse(RPC.ERROR_32601, id);
}
// execute explicitly passed pre-processors
for (const preProcessor of options.preProcessors) {
try {
// @ts-ignore
this.#json = json;
await preProcessor(json, this);
} catch (err) {
return RPC.errorResponse(
// @ts-ignore
RPC.findError(err, RPC.ERROR_32010, method.errors),
id
);
}
}
if (!options.skipPreProcessors) {
// execute preconfigured pre-processors
for (const preProcessor of this.#options.preProcessors) {
try {
// @ts-ignore
this.#json = json;
await preProcessor(json, this);
} catch (err) {
return RPC.errorResponse(
// @ts-ignore
RPC.findError(err, RPC.ERROR_32010, method.errors),
id
);
}
}
}
let params = json[RPC.PARAMS];
if (
params &&
// @ts-ignore
method.paramStructure === "by-name" &&
!(params instanceof Object)
) {
const e = RPC.ERROR_32602;
e[RPC.ERROR_DATA] =
"Parameters must be object (specified parameter structure \"by-name\")";
return RPC.errorResponse(e, id);
}
if (
params &&
// @ts-ignore
method.paramStructure === "by-position" &&
!Array.isArray(params)
) {
const e = RPC.ERROR_32602;
e[RPC.ERROR_DATA] =
"Parameters must be array (specified parameter structure \"by-position\")";
return RPC.errorResponse(e, id);
}
// convert object parameters-by-name to array prameters-by-index
if (params && !Array.isArray(params)) {
const paramsByPosition = [];
// @ts-ignore
for (const param of method.params) {
if (param.required && params[param.name] === undefined) {
const e = RPC.ERROR_32602;
e[
RPC.ERROR_DATA
] = `Value for parameter '${param.name}' is not specified`;
return RPC.errorResponse(e, id);
}
paramsByPosition.push(params[param.name]);
}
params = paramsByPosition;
}
// if parameters are not passed - use empty array
if (!Array.isArray(params)) {
params = [];
}
// testing passed paremeter types
let i = 0;
// @ts-ignore
while (i < method.params.length && i < params.length) {
// @ts-ignore
const param = method.params[i];
if (param.required || typeof params[i] !== "undefined") {
if (
!this.ajv.validate(`param/${methodName}/${param.name}`, params[i])
) {
const e = RPC.ERROR_32602;
e[RPC.ERROR_DATA] = {
message: `Parameter at index ${i} ('${param.name}') has invalid value`,
errors: this.ajv.errors,
};
return RPC.errorResponse(e, id);
}
}
i++;
}
// remove excess paramters
if (i < params.length) {
params.splice(i);
}
// test if there are required parameters with no passed values
// and add undefined values for missing non-required parameters
// @ts-ignore
while (i < method.params.length) {
// @ts-ignore
const param = method.params[i];
if (param.required) {
const e = RPC.ERROR_32602;
e[
RPC.ERROR_DATA
] = `Value for parameter at index ${i} ('${param.name}') is not specified`;
return RPC.errorResponse(e, id);
} else {
params.push(undefined);
}
i++;
}
// at this point params variable contains values for all parameters
// and we can execute handler
try {
// @ts-ignore
this.#json = json;
let result = await handler(...params);
/** @type {JSONRPCResult|JSONRPCError} */
let resp;
// Validate result
// Skip validation for notifications
if (typeof id !== "undefined") {
// For validation try to convert result to JSON and back.
// This makes sure that all objects with toJSON() method
// are correctly converted before validation process.
let resultForValidation = result;
try {
resultForValidation = JSON.parse(JSON.stringify(result));
} catch (err) {
// If result can not be converted to JSON - validate original object
// Maybe there are post processors which will fix stringify issue later
// Anyway testing for JSON stringify will be done before returning response
resultForValidation = result;
}
if (this.ajv.validate(`result/${methodName}`, resultForValidation)) {
// @ts-ignore
resp = {
[RPC.JSONRPC]: RPC.VERSION,
[RPC.ID]: id,
[RPC.RESULT]: result,
};
} else {
// result is not valid
const e = RPC.ERROR_32030;
e[RPC.ERROR_DATA] = {
result,
errors: this.ajv.errors,
};
return RPC.errorResponse(e, id);
}
}
if (!options.skipPostProcessors) {
// execute preconfigured post-processors
for (const postProcessor of this.#options.postProcessors) {
try {
// @ts-ignore
this.#json = json;
await postProcessor(resp, json, this);
} catch (err) {
return RPC.errorResponse(
// @ts-ignore
RPC.findError(err, RPC.ERROR_32050, method.errors),
id
);
}
}
}
// execute explicitly passed post-processors
for (const postProcessor of options.postProcessors) {
try {
// @ts-ignore
this.#json = json;
await postProcessor(resp, json, this);
} catch (err) {
return RPC.errorResponse(
// @ts-ignore
RPC.findError(err, RPC.ERROR_32050, method.errors),
id
);
}
}
if (typeof id !== "undefined") {
// Testing if response can be stringified
try {
JSON.stringify(resp);
return resp;
} catch (err) {
// failure to stringify result to JSON
const e = RPC.ERROR_32000;
e[RPC.ERROR_DATA] = Exception.errSerializer(err);
return RPC.errorResponse(e, id);
}
}
} catch (err) {
return RPC.errorResponse(
// @ts-ignore
RPC.findError(err, RPC.ERROR_32020, method.errors),
id
);
}
}
#assertPreProcessors(preProcessors) {
RPC.assert(Array.isArray(preProcessors), "preProcessors must be array");
for (const pp of preProcessors) {
RPC.assert(
typeof pp === "function",
"all provided pre-processors must be function"
);
}
}
#assertPostProcessors(postProcessors) {
RPC.assert(Array.isArray(postProcessors), "postProcessors must be array");
for (const pp of postProcessors) {
RPC.assert(
typeof pp === "function",
"all provided post-processors must be function"
);
}
}
/**
* Creates JSON-RPC request object.
*
* @param {(null|string|number)} id - request id
* @param {string} method - method name to execute
* @param {(Object<string, *>|Array<*>)} [params] - list of parameter values to pass to method
* @returns {JSONRPCRequest} created JSON-RPC request object
*/
static request(id, method, params) {
RPC.assert(
id === null || typeof id === "string" || typeof id === "number",
"id must be either null or string or number"
);
RPC.assert(typeof method === "string", "method must be string");
const req = {
[RPC.JSONRPC]: RPC.VERSION,
[RPC.ID]: id,
[RPC.METHOD]: method,
};
if (typeof params !== "undefined") {
RPC.assert(
params && (Array.isArray(params) || typeof params === "object"),
"params must be either array or object"
);
// @ts-ignore
req[RPC.PARAMS] = params;
}
// @ts-ignore
return req;
}
/**
* Creates JSON-RPC notification object.
*
* @param {string} method - method name to execute
* @param {(Object<string, *>|Array<*>)} [params] - list of parameter values to pass to method
* @returns {JSONRPCRequest} created JSON-RPC notification object
*/
static notification(method, params) {
RPC.assert(typeof method === "string", "method must be string");
const req = {
[RPC.JSONRPC]: RPC.VERSION,
[RPC.METHOD]: method,
};
if (typeof params !== "undefined") {
RPC.assert(
params && (Array.isArray(params) || typeof params === "object"),
"params must be either array or object"
);
// @ts-ignore
req[RPC.PARAMS] = params;
}
// @ts-ignore
return req;
}
/**
* Creates JSON-RPC error response object.
* Passing <code>id</code> value <code>undefined</code> results in returning <code>undefined</code> value.
* According to specification if <code>id</code> is undefined - server MUST NOT respond.
*
* @param {JSONRPCErrorObject} error - execution error to report
* @param {(null|string|number)} [id] - response id
* @returns {(JSONRPCError|undefined)} JSON-RPC error response object or
* undefined if id parameter value is undefined
*/
static errorResponse(error, id) {
if (typeof id === "undefined") {
return;
}
RPC.assert(
id === null || typeof id === "string" || typeof id === "number",
"id must be either null or string or number"
);
RPC.assert(error && typeof error === "object", "error must be object");
RPC.assert(typeof error.code === "number", "error.code must be number");
RPC.assert(
typeof error.message === "string",
"error.message must be string"
);
// @ts-ignore
return {
[RPC.JSONRPC]: RPC.VERSION,
[RPC.ID]: id,
[RPC.ERROR]: error,
};
}
/**
* Throws TypeError if provided test is false.
*
* @param {boolean} test - test value
* @param {string} [message] - error message
* @throws {TypeError} when test is false
*/
static assert(test, message) {
if (!test) {
if (typeof message === "string") {
throw new TypeError(message);
} else {
throw new TypeError();
}
}
}
/**
* JSON-RPC version.
*
* @type {string}
* @readonly
*/
static get VERSION() {
return "2.0";
}
/**
* Member name for specifying JSON-RPC version.
* Used in request and response objects.
*
* @type {string}
* @readonly
*/
static get JSONRPC() {
return "jsonrpc";
}
/**
* Member name for specifying identifier established by client.
* Used in request and response objects.
*
* @type {string}
* @readonly
*/
static get ID() {
return "id";
}
/**
* Member name for specifying method name to be invoked.
* Used in request objects.
*
* @type {string}
* @readonly
*/
static get METHOD() {
return "method";
}
/**
* Member name for specifying parameter values to be used during the invocation of the method.
* Used in request objects.
*
* @type {string}
* @readonly
*/
static get PARAMS() {
return "params";
}
/**
* Member name for specifying result of the successful method invocation.
* Used in response objects.
*
* @type {string}
* @readonly
*/
static get RESULT() {
return "result";
}
/**
* Member name for specifying error of the method invocation.
* Used in response objects.
*
* @type {string}
* @readonly
*/
static get ERROR() {
return "error";
}
/**
* Member name for specifying error code in error object.
* Used in response error objects.
*
* @type {string}
* @readonly
*/
static get ERROR_CODE() {
return "code";
}
/**
* Member name for specifying error message in error object.
* Used in response error objects.
*
* @type {string}
* @readonly
*/
static get ERROR_MESSAGE() {
return "message";
}
/**
* Member name for specifying error additional data in error object.
* Used in response error objects.
*
* @type {string}
* @readonly
*/
static get ERROR_DATA() {
return "data";
}
/**
* Finds corresponding JSONRPCErrorObject among implementation defined and
* among additional errors (if provided).
* If 'err' is number - tests err against JSONRPCErrorObject.code.
* If 'err' is object - tests err.code against JSONRPCErrorObject.code.
* If error is found and 'err' is object - resulting error object is updated
* with 'err.message' and 'err.data' values (if exists).
* If error is not found and default is provided - returns default error
* with 'data' property containing serialized original error.
* If error is not found and there is no default provided - returns undefined.
*
* @param {number|object|Error} err - error to look for
* @param {JSONRPCErrorObject|OpenRPCError} [defaultError] - error response to return if not found
* @param {Array<JSONRPCErrorObject|OpenRPCError>} [errors] - additional list of errors to look
* @returns {JSONRPCErrorObject|undefined} error response object or undefined if not found
*/
static findError(err, defaultError, errors) {
let foundError;
if (typeof err === "number") {
if (Array.isArray(errors)) {
// first look in provided errors list
foundError = errors.find((e) => {
if (e && typeof e === "object") {
if (e[RPC.ERROR_CODE] === err) {
if (typeof e[RPC.ERROR_MESSAGE] === "string") {
return true;
}
}
}
return false;
});
}
// look in implementation errors list if not found
if (!foundError) {
foundError = RPC.IMPL_ERRORS.find((e) => e[RPC.ERROR_CODE] === err);
}
}
if (!foundError && err && typeof err === "object") {
if (Array.isArray(errors)) {
// first look in provided errors list
foundError = errors.find((e) => {
if (e && typeof e === "object") {
if (e[RPC.ERROR_CODE] === err[RPC.ERROR_CODE]) {
if (typeof e[RPC.ERROR_MESSAGE] === "string") {
return true;
}
}
}
return false;
});
}
// look in implementation errors list if not found
if (!foundError) {
foundError = RPC.IMPL_ERRORS.find(
(e) => e[RPC.ERROR_CODE] === err[RPC.ERROR_CODE]
);
}
// Update 'message' and 'data' properties
if (foundError) {
foundError = ObjectUtils.deepClone(foundError);
if (
err[RPC.ERROR_MESSAGE] &&
typeof err[RPC.ERROR_MESSAGE] === "string"
) {
foundError[RPC.ERROR_MESSAGE] = err[RPC.ERROR_MESSAGE];
}
if (typeof err[RPC.ERROR_DATA] !== "undefined") {
foundError[RPC.ERROR_DATA] = err[RPC.ERROR_DATA];
}
}
}
if (
!foundError &&
defaultError &&
typeof defaultError === "object" &&
typeof defaultError[RPC.ERROR_CODE] === "number" &&
typeof defaultError[RPC.ERROR_MESSAGE] === "string"
) {
foundError = ObjectUtils.deepClone(defaultError);
foundError[RPC.ERROR_DATA] = Exception.errSerializer(err);
}
return foundError;
}
/**
* JSON stringify error.
* Reported when response can not be stringified to JSON
* (usualy due circular references).
* Non-standard.
*
* @type {JSONRPCErrorObject}
* @readonly
*/
static get ERROR_32000() {
// @ts-ignore
return {
[RPC.ERROR_CODE]: -32000,
[RPC.ERROR_MESSAGE]: "Stringify error",
};
}
/**
* Pre-processor execution error.
* Reported when pre-processor throws unknown error.
* Non-standard.
*
* @type {JSONRPCErrorObject}
* @readonly
*/
static get ERROR_32010() {
// @ts-ignore
return {
[RPC.ERROR_CODE]: -32010,
[RPC.ERROR_MESSAGE]: "Pre-processor execution error",
};
}
/**
* Method execution error.
* Reported when method execution throws error.
* Non-standard.
*
* @type {JSONRPCErrorObject}
* @readonly
*/
static get ERROR_32020() {
// @ts-ignore
return {
[RPC.ERROR_CODE]: -32020,
[RPC.ERROR_MESSAGE]: "Method execution error",
};
}
/**
* Invalid result error.
* Reported when method produces invalid result.
* Non-standard.
*
* @type {JSONRPCErrorObject}
* @readonly
*/
static get ERROR_32030() {
// @ts-ignore
return {
[RPC.ERROR_CODE]: -32030,
[RPC.ERROR_MESSAGE]: "Invalid result",
};
}
/**
* General security error.
* Non-standard.
*
* @type {JSONRPCErrorObject}
* @readonly
*/
static get ERROR_32040() {
// @ts-ignore
return {
[RPC.ERROR_CODE]: -32040,
[RPC.ERROR_MESSAGE]: "Security error",
};
}
/**
* Credentials expired error.
* Non-standard.
*
* @type {JSONRPCErrorObject}
* @readonly
*/
static get ERROR_32041() {
// @ts-ignore
return {
[RPC.ERROR_CODE]: -32041,
[RPC.ERROR_MESSAGE]: "Credentials expired",
};
}
/**
* Post-processor execution error.
* Reported when post-processor throws unknown error.
* Non-standard.
*
* @type {JSONRPCErrorObject}
* @readonly
*/
static get ERROR_32050() {
// @ts-ignore
return {
[RPC.ERROR_CODE]: -32050,
[RPC.ERROR_MESSAGE]: "Post-processor execution error",
};
}
/**
* Invalid request error.
*
* @type {JSONRPCErrorObject}
* @readonly
*/
static get ERROR_32600() {
// @ts-ignore
return {
[RPC.ERROR_CODE]: -32600,
[RPC.ERROR_MESSAGE]: "Invalid Request",
};
}
/**
* Method not found error.
*
* @type {JSONRPCErrorObject}
* @readonly
*/
static get ERROR_32601() {
// @ts-ignore
return {
[RPC.ERROR_CODE]: -32601,
[RPC.ERROR_MESSAGE]: "Method not found",
};
}
/**
* Invalid params error.
*
* @type {JSONRPCErrorObject}
* @readonly
*/
static get ERROR_32602() {
// @ts-ignore
return {
[RPC.ERROR_CODE]: -32602,
[RPC.ERROR_MESSAGE]: "Invalid params",
};
}
/**
* Internal error.
*
* @type {JSONRPCErrorObject}
* @readonly
*/
static get ERROR_32603() {
// @ts-ignore
return {
[RPC.ERROR_CODE]: -32603,
[RPC.ERROR_MESSAGE]: "Internal error",
};
}
/**
* JSON parse error.
*
* @type {JSONRPCErrorObject}
* @readonly
*/
static get ERROR_32700() {
// @ts-ignore
return {
[RPC.ERROR_CODE]: -32700,
[RPC.ERROR_MESSAGE]: "Parse error",
};
}
/**
* List of error objects defined in specs.
*
* @type {Array<JSONRPCErrorObject>}
* @readonly
*/
static get SPEC_ERRORS() {
return [
RPC.ERROR_32600,
RPC.ERROR_32601,
RPC.ERROR_32602,
RPC.ERROR_32603,
RPC.ERROR_32700,
];
}
/**
* List of all error objects defined in this implementation
* (including spec errors).
*
* @type {Array<JSONRPCErrorObject>}
* @readonly
*/
static get IMPL_ERRORS() {
return [
RPC.ERROR_32000,
RPC.ERROR_32010,
RPC.ERROR_32020,
RPC.ERROR_32030,
RPC.ERROR_32040,
RPC.ERROR_32041,
RPC.ERROR_32050,
...RPC.SPEC_ERRORS,
];
}
/**
* Service discovery method name as specified in OpenRPC.
*
* @type {string}
* @readonly
*/
static get RPC_DISCOVER() {
return rpcDiscoverDef.name;
}
/**
* Service discovery method schema as specified in OpenRPC.
*
* @type {OpenRPC}
* @readonly
*/
static get RPC_DISCOVER_SCHEMA() {
return ObjectUtils.deepClone(rpcDiscoverDef);
}
}
export { RPC };
/**
* @typedef RPCOptionsType
* @type {object}
* @property {Array<PreProcessorFunction>} [preProcessors] - list of functions executed before handler execution
* @property {Array<PostProcessorFunction>} [postProcessors] - list of functions executed before handler execution
*/
/**
* Pre-processor funtion definition.
*
* @typedef PreProcessorFunction
* @type {function}
* @param {JSONRPCRequest} request
* @param {RPC} rpc - reference to RPC handler
* @throws {number|object|Error} stops request processing and
* returns error response.
*/
/**
* Post-processor funtion definition.
*
* @typedef PostProcessorFunction
* @type {function}
* @param {JSONRPCRequest} request
* @param {JSONRPCResult} result
* @param {RPC} rpc - reference to RPC handler
* @throws {number|object|Error} returns specified error response instead.
*/
/**
* JSON-RPC request object structure.
*
* @typedef JSONRPCRequest
* @type {object}
* @property {"2.0"} jsonrpc - JSON-RPC version always exact string "2.0"
* @property {null|string|number} [id] - request id
* @property {string} method - method to execute
* @property {(Object<string, *>|Array)} [params] - parameters for method
*/
/**
* JSON-RPC error object structure.
*
* @typedef JSONRPCErrorObject
* @type {object}
* @property {number} code - error code
* @property {string} message - error message
* @property {*} [data] - additional error data
*/
/**
* JSON-RPC error response object structure.
*
* @typedef JSONRPCError
* @type {object}
* @property {"2.0"} jsonrpc - JSON-RPC version always exact string "2.0"
* @property {null|string|number} id - request id
* @property {JSONRPCErrorObject} error - error object
*/
/**
* JSON-RPC result object response structure.
*
* @typedef JSONRPCResult
* @type {object}
* @property {"2.0"} jsonrpc - JSON-RPC version always exact string "2.0"
* @property {null|string|number} id - request id
* @property {*} result - result object
*/
/**
* OpenRPC object structure.
* This object MAY be extended with Specification Extensions.
*
* @typedef OpenRPC
* @type {object}
* @property {string} openrpc - OpenRPC semantic version number
* @property {OpenRPCInfo} info - API metadata
* @property {Array<OpenRPCServer>} [servers] - array of object providing information on connectivity to target server
* @property {Array<(OpenRPCMethod|OpenRPCReference)>} methods - list of methods available in API
* @property {Array<OpenRPCComponents>} [components] - holds various schemas for specification
* @property {OpenRPCExternalDoc} [externalDocs] - additional external documentation
*/
/**
* OpenRPC Info structure.
* This object MAY be extended with Specification Extensions.
*
* @typedef OpenRPCInfo
* @type {object}
* @property {string} title - application title
* @property {string} [description] - verbose application description
* @property {string} [termsOfService] - URL to terms of service for API
* @property {OpenRPCContact} [contact] - contact information for exposed API
* @property {OpenRPCLicense} [license] - license information for exposed API
* @property {string} version - API version
*/
/**
* OpenRPC Contact structure.
* This object MAY be extended with Specification Extensions.
*
* @typedef OpenRPCContact
* @type {object}
* @property {string} [name] - name of contact person/organization
* @property {string} [url] - URL to contact information
* @property {string} [email] - email of contact person/organization
*/
/**
* OpenRPC License structure.
* This object MAY be extended with Specification Extensions.
*
* @typedef OpenRPCLicense
* @type {object}
* @property {string} name - license name used for API
* @property {string} [url] - URL to license
*/
/**
* OpenRPC Server structure.
* This object MAY be extended with Specification Extensions.
*
* @typedef OpenRPCServer
* @type {object}
* @property {string} name - cannonical name for the server
* @property {string} url - URL to the target host
* @property {string} [summary] - short summary for what server is
* @property {string} [description] - description of host designated by url
* @property {Object<string, OpenRPCServerVariable>} [variables] - map of variables
*/
/**
* OpenRPC Server variable structure.
* This object MAY be extended with Specification Extensions.
*
* @typedef OpenRPCServerVariable
* @type {object}
* @property {Array<string>} [enum] - enumeration of string values to be used if the substitution options are from a limited set
* @property {string} default - default value to use for substitution, which SHALL be sent if an alternate value is not supplied
* @property {string} [description] - Description of server variable
*/
/**
* OpenRPC Method structure.
* This object MAY be extended with Specification Extensions.
*
* @typedef OpenRPCMethod
* @type {object}
* @property {string} name - method name
* @property {Array<OpenRPCTag|OpenRPCReference>} [tags] - list of tags
* @property {string} [summary] - short summary for what method does
* @property {string} [description] - verbose explanation of the method behavior
* @property {OpenRPCExternalDoc} [externalDocs] - additional external documentation
* @property {Array<(OpenRPCContentDescriptor|OpenRPCReference)>} params - list of parameters for method
* @property {(OpenRPCContentDescriptor|OpenRPCReference)} result - result returned by method
* @property {boolean} [deprecated] - declares this method deprecated
* @property {Array<OpenRPCServer>} [servers] - alternative servers to service this method
* @property {Array<(OpenRPCError|OpenRPCReference)>} [errors] - list of custom application defined errors
* @property {Array<(OpenRPCLink|OpenRPCReference)>} [links] - list of possible links from this method call
* @property {("by-name"|"by-position"|"either")} [paramStructure] - expected format of paramters. Defaults to "by-position".
* @property {Array<OpenRPCExamplePairing>} [examples] - list of examples
*/
/**
* OpenRPC Content Descriptor structure.
* This object MAY be extended with Specification Extensions.
*
* @typedef OpenRPCContentDescriptor
* @type {object}
* @property {string} name - name of content being described
* @property {string} [summary] - short summary of content being described
* @property {string} [description] - verbose explanation of content descriptor behavior
* @property {boolean} [required] - determines if content is required field. Defaults to false
* @property {object} schema - schema that describes content
* @property {boolean} [deprecated] - specifies that content is deprecated. Defaults to false
*/
/**
* OpenRPC Example Pairing structure.
* This object MAY be extended with Specification Extensions.
*
* @typedef OpenRPCExamplePairing
* @type {object}
* @property {string} [name] - name for example pairing
* @property {string} [summary] - short description for example pairing
* @property {string} [description] - verbose explanation of example pairing
* @property {Array<(OpenRPCExample|OpenRPCReference)>} [params] - example parameters
* @property {(OpenRPCExample|OpenRPCReference)} [result] - example result
*/
/**
* OpenRPC Example structure.
* This object MAY be extended with Specification Extensions.
*
* @typedef OpenRPCExample
* @type {object}
* @property {string} [name] - canonical example name
* @property {string} [summary] - short description for example
* @property {string} [description] - verbose explanation of example
* @property {*} [value] - embeded literal example
* @property {string} [externalValue] - URL to literal example which can not easily be included in JSON
*/
/**
* OpenRPC Link structure.
* This object MAY be extended with Specification Extensions.
*
* @typedef OpenRPCLink
* @type {object}
* @property {string} name - canonical link name
* @property {string} [summary] - short description for link
* @property {string} [description] - full description for link
* @property {string} [method] - name of existing, resolvable OpenRPC method
* @property {Object<string, *>} [params] - params to pass to method
* @property {OpenRPCServer} [server] - server to be used by target method
*/
/**
* OpenRPC Error structure.
*
* @typedef OpenRPCError
* @type {object}
* @property {number} code - error code
* @property {string} message - short error description
* @property {*} [data] - additional information about error
*/
/**
* OpenRPC Components structure.
* This object MAY be extended with Specification Extensions.
*
* @typedef OpenRPCComponents
* @type {object}
* @property {Object<string, OpenRPCContentDescriptor>} [contentDescriptors] - reusable content descriptors
* @property {Object<string, object>} [schemas] - reusable schemas
* @property {Object<string, OpenRPCExample>} [examples] - reusable examples
* @property {Object<string, OpenRPCLink>} [links] - reusable links
* @property {Object<string, OpenRPCError>} [errors] - reusable errors
* @property {Object<string, OpenRPCExamplePairing>} [examplePairingObjects] - reusable example pairings
* @property {Object<string, OpenRPCTag>} [tags] - reusable tags
*/
/**
* OpenRPC Tag structure.
* This object MAY be extended with Specification Extensions.
*
* @typedef OpenRPCTag
* @type {object}
* @property {string} name - tag name
* @property {string} [summary] - short summary for tag
* @property {string} [description] - verbose tag explanation
* @property {OpenRPCExternalDoc} [externalDocs] - additional external documentation
*/
/**
* OpenRPC External Document structure.
* This object MAY be extended with Specification Extensions.
*
* @typedef OpenRPCExternalDoc
* @type {object}
* @property {string} [description] - document description
* @property {string} url - target document url
*/
/**
* OpenRPC Reference structure.
*
* @typedef OpenRPCReference
* @type {object}
* @property {string} $ref - reference string
*/
/**
* OpenRPC Specification Extensions structure.
* The extensions properties are implemented as patterned fields
* that are always prefixed by "x-".
*
* @typedef OpenRPCSpecificationExtensions
* @type {object}
*/