refactor: allow service-specific API errors to be registered

This commit is contained in:
KernelDeimos 2024-12-10 15:23:54 -05:00
parent 8557c54527
commit ff62d9009e
4 changed files with 112 additions and 49 deletions

View File

@ -324,36 +324,6 @@ module.exports = class APIError {
message: () => 'Invalid token.',
},
// drivers
'interface_not_found': {
status: 404,
message: ({ interface_name }) => `Interface not found: ${quot(interface_name)}`,
},
'no_implementation_available': {
status: 502,
message: ({
iface,
interface_name,
driver
}) => `No implementation available for ` +
(iface ?? interface_name) ? 'interface' : 'driver' +
' ' + quot(iface ?? interface_name ?? driver) + '.',
},
'method_not_found': {
status: 404,
message: ({ interface_name, method_name }) => `Method not found: ${quot(method_name)} on interface ${quot(interface_name)}`,
},
'missing_required_argument': {
status: 400,
message: ({ interface_name, method_name, arg_name }) =>
`Missing required argument ${quot(arg_name)} for method ${quot(method_name)} on interface ${quot(interface_name)}`,
},
'argument_consolidation_failed': {
status: 400,
message: ({ interface_name, method_name, arg_name, message }) =>
`Failed to parse or process argument ${quot(arg_name)} for method ${quot(method_name)} on interface ${quot(interface_name)}: ${message}`,
},
// SLA
'rate_limit_exceeded': {
status: 429,

View File

@ -0,0 +1,54 @@
const APIError = require("../../api/APIError");
const BaseService = require("../../services/BaseService");
/**
* @typedef {Object} ErrorSpec
* @property {string} code - The error code
* @property {string} status - HTTP status code
* @property {function} message - A function that generates an error message
*/
/**
* The APIErrorService class provides a mechanism for registering and managing
* error codes and messages which may be sent to clients.
*
* This allows for a single source-of-truth for error codes and messages that
* are used by multiple services.
*/
class APIErrorService extends BaseService {
_construct () {
this.codes = {
...this.constructor.codes,
};
}
// Hardcoded error codes from before this service was created
static codes = APIError.codes;
/**
* Registers API error codes.
*
* @param {Object.<string, ErrorSpec>} codes - A map of error codes to error specifications
*/
register (codes) {
for ( const code in codes ) {
this.codes[code] = codes[code];
}
}
create (code, fields) {
const error_spec = this.codes[code];
if ( ! error_spec ) {
return new APIError(500, 'Missing error message.', null, {
code,
});
}
return new APIError(error_spec.status, error_spec.message, null, {
...fields,
code,
});
}
}
module.exports = APIErrorService;

View File

@ -19,6 +19,9 @@ class WebModule extends AdvancedBase {
const WebServerService = require("./WebServerService");
services.registerService('web-server', WebServerService);
const APIErrorService = require("./APIErrorService");
services.registerService('api-error', APIErrorService);
}
}

View File

@ -26,6 +26,8 @@ const { PermissionUtil } = require("../auth/PermissionService");
const { Invoker } = require("../../../../putility/src/libs/invoker");
const { get_user } = require("../../helpers");
const strutil = require('@heyputer/putility').libs.string;
/**
* DriverService provides the functionality of Puter drivers.
* This class is responsible for managing and interacting with Puter drivers.
@ -50,8 +52,52 @@ class DriverService extends BaseService {
this.interface_to_test_service = {};
this.service_aliases = {};
}
/**
* This method is responsible for calling a driver's method with provided arguments.
* It checks for permissions, selects the best option, and applies rate and monthly usage limits before invoking the driver.
*
* @param {Object} o - An object containing driver, interface, method, and arguments.
* @returns {Promise<{success: boolean, service: DriverService.Driver, result: any, metadata: any}>}
*/
_init () {
const svc_registry = this.services.get('registry');
svc_registry.register_collection('');
const { quot } = strutil;
const svc_apiError = this.services.get('api-error');
svc_apiError.register({
'missing_required_argument': {
status: 400,
message: ({ interface_name, method_name, arg_name }) =>
`Missing required argument ${quot(arg_name)} for method ${quot(method_name)} on interface ${quot(interface_name)}`,
},
'argument_consolidation_failed': {
status: 400,
message: ({ interface_name, method_name, arg_name, message }) =>
`Failed to parse or process argument ${quot(arg_name)} for method ${quot(method_name)} on interface ${quot(interface_name)}: ${message}`,
},
'interface_not_found': {
status: 404,
message: ({ interface_name }) => `Interface not found: ${quot(interface_name)}`,
},
'method_not_found': {
status: 404,
message: ({ interface_name, method_name }) => `Method not found: ${quot(method_name)} on interface ${quot(interface_name)}`,
},
'no_implementation_available': {
status: 502,
message: ({
iface,
interface_name,
driver
}) => `No implementation available for ` +
(iface ?? interface_name) ? 'interface' : 'driver' +
' ' + quot(iface ?? interface_name ?? driver) + '.',
},
});
}
/**
* This method is responsible for registering collections in the service registry.
* It registers 'interfaces', 'drivers', and 'types' collections.
@ -91,19 +137,6 @@ class DriverService extends BaseService {
{ col_drivers });
}
/**
* This method is responsible for calling a driver's method with provided arguments.
* It checks for permissions, selects the best option, and applies rate and monthly usage limits before invoking the driver.
*
* @param {Object} o - An object containing driver, interface, method, and arguments.
* @returns {Promise<{success: boolean, service: DriverService.Driver, result: any, metadata: any}>}
*/
_init () {
const svc_registry = this.services.get('registry');
svc_registry.register_collection('');
}
register_driver (interface_name, implementation) {
this.interface_to_implementation[interface_name] = implementation;
}
@ -235,7 +268,8 @@ class DriverService extends BaseService {
})();
if ( ! driver_service_exists ) {
throw APIError.create('no_implementation_available', null, { iface })
const svc_apiError = this.services.get('api-error');
throw svc_apiError.create('no_implementation_available', { iface });
}
const service = this.services.get(driver);
@ -530,17 +564,19 @@ class DriverService extends BaseService {
const svc_registry = this.services.get('registry');
const c_interfaces = svc_registry.get('interfaces');
const c_types = svc_registry.get('types');
const svc_apiError = this.services.get('api-error');
// Note: 'interface' is a strict mode reserved word.
const interface_ = c_interfaces.get(interface_name);
if ( ! interface_ ) {
throw APIError.create('interface_not_found', null, { interface_name });
throw svc_apiError.create('interface_not_found', { interface_name });
}
const processed_args = {};
const method = interface_.methods[method_name];
if ( ! method ) {
throw APIError.create('method_not_found', null, { interface_name, method_name });
throw svc_apiError.create('method_not_found', { interface_name, method_name });
}
for ( const [arg_name, arg_descriptor] of Object.entries(method.parameters) ) {
@ -551,7 +587,7 @@ class DriverService extends BaseService {
// There's a particular way I want to do this that involves
// a trait for extensible behaviour.
if ( arg_value === undefined && arg_descriptor.required ) {
throw APIError.create('missing_required_argument', null, {
throw svc_apiError.create('missing_required_argument', {
interface_name,
method_name,
arg_name,
@ -564,7 +600,7 @@ class DriverService extends BaseService {
processed_args[arg_name] = await arg_behaviour.consolidate(
ctx, arg_value, { arg_descriptor, arg_name });
} catch ( e ) {
throw APIError.create('argument_consolidation_failed', null, {
throw svc_apiError.create('argument_consolidation_failed', {
interface_name,
method_name,
arg_name,