Add error_context function and calls in lib code

master
garrettmills 4 years ago
parent 83c50a63c5
commit f277780d4a
No known key found for this signature in database
GPG Key ID: 6ACD58D6ADACFC6E

@ -4,6 +4,7 @@
const Unit = require('../Unit')
const UnitStaticDependencyError = require('../errors/UnitStaticDependencyError')
const error_context = require('../errors/error_context.fn')
/**
* Analyzes various aspects of the application and its dependencies.
@ -31,10 +32,13 @@ class Analyzer {
const available_services = ['app']
for ( const UnitClass of Object.values(this.units) ) {
for ( const service of UnitClass.services ) {
if ( !available_services.includes(service) ) {
if (!available_services.includes(service)) {
const error = new UnitStaticDependencyError()
error.unit(UnitClass.name).required(service)
throw error
throw error_context(error, {
UnitClass,
dependent_service: service,
})
}
}
@ -52,24 +56,31 @@ class Analyzer {
// Get a list of services that the service depends on
const service_dependencies = service.constructor.services
// Check those dependencies
const missing_deps = []
for ( const dep_name of service_dependencies ) {
const dep_class = this.app.di().get(dep_name)
if (!dep_class) {
missing_deps.push(dep_name)
continue
}
// If the dep is a unit, make sure it's in the RUNNING state
if ( dep_class instanceof Unit ) {
if ( dep_class.status() !== Unit.STATUS_RUNNING ) {
try {
// Check those dependencies
const missing_deps = []
for (const dep_name of service_dependencies) {
const dep_class = this.app.di().get(dep_name)
if (!dep_class) {
missing_deps.push(dep_name)
continue
}
// If the dep is a unit, make sure it's in the RUNNING state
if (dep_class instanceof Unit) {
if (dep_class.status() !== Unit.STATUS_RUNNING) {
missing_deps.push(dep_name)
}
}
}
}
return missing_deps
return missing_deps
} catch (e) {
throw error_context(e, {
service,
service_dependencies,
})
}
}
}

@ -11,6 +11,7 @@ const SoftError = require('../errors/SoftError')
const FatalError = require('../errors/FatalError')
const UnitRuntimeDependencyError = require('../errors/UnitRuntimeDependencyError')
const RunLevelErrorHandler = require('../errors/RunLevelErrorHandler')
const error_context = require('../errors/error_context.fn')
/**
* The Flitter application.
@ -156,7 +157,9 @@ class FlitterApp extends Service {
unit.status(Unit.STATUS_ERROR)
} else {
// otherwise, bail!
throw e
throw error_context(e, {
unit_name,
})
}
}
}
@ -200,7 +203,9 @@ class FlitterApp extends Service {
} catch (e) {
this.output.error(`Error encountered while starting ${unit.name()}!`)
unit.status(Unit.STATUS_ERROR)
throw e
throw error_context(e, {
unit_service_name: unit.name(),
})
}
}
@ -221,7 +226,9 @@ class FlitterApp extends Service {
} catch (e) {
this.output.error(`Error encountered while stopping ${unit.name()}.`)
unit.status(Unit.STATUS_ERROR)
throw e
throw error_context(e, {
unit_service_name: unit.name(),
})
}
}

@ -3,6 +3,7 @@
*/
const Unit = require('../Unit')
const error_context = require('../errors/error_context.fn')
/**
* Unit service providing access to all canonical accessors by name.
@ -39,16 +40,22 @@ class CanonicalAccessUnit extends Unit {
* @returns {*} - the corresponding resource
*/
get(resource) {
const parts = resource.split('::')
let descending_item = this.resources
for ( let i in parts ) {
if ( typeof descending_item === 'function' ) {
descending_item = descending_item(parts[i])
} else {
descending_item = descending_item[parts[i]]
try {
const parts = resource.split('::')
let descending_item = this.resources
for (let i in parts) {
if (typeof descending_item === 'function') {
descending_item = descending_item(parts[i])
} else {
descending_item = descending_item[parts[i]]
}
}
return descending_item
} catch (e) {
throw error_context(e, {
resource_name: resource,
})
}
return descending_item
}
/**

@ -5,21 +5,28 @@
const path = require('path')
const rra = require('recursive-readdir-async')
const Unit = require('../Unit')
const error_context = require('../errors/error_context.fn')
const priv = {
// Formats a file path to canonical name.
format_file_name(name) {
const base_dir = process.platform === 'win32' ? this.directory.replace(/\\/g, '/') : this.directory
name = name.replace(base_dir, '')
try {
const base_dir = process.platform === 'win32' ? this.directory.replace(/\\/g, '/') : this.directory
name = name.replace(base_dir, '')
if ( this.suffix ) {
const suffix_regexp = new RegExp(`${this.suffix}`, 'g')
name = name.replace(suffix_regexp, '')
}
if (this.suffix) {
const suffix_regexp = new RegExp(`${this.suffix}`, 'g')
name = name.replace(suffix_regexp, '')
}
name = name.replace(/\//g, ':').substring(1)
name = name.replace(/\//g, ':').substring(1)
return name
return name
} catch (e) {
throw error_context(e, {
formatting_file_name: name,
})
}
}
}
@ -87,17 +94,24 @@ class CanonicalUnit extends Unit {
async go(app) {
const files = await rra.list(this.directory)
for ( let key in files ) {
if ( !files.hasOwnProperty(key) ) continue
if ( files[key].fullname && files[key].fullname.endsWith(this.suffix) ) {
const name = priv.format_file_name.bind(this)(files[key].fullname)
const instance = require(files[key].fullname)
if ( app.di().__is_injectable(instance) ) {
app.di().inject(instance)
try {
if (!files.hasOwnProperty(key)) continue
if (files[key].fullname && files[key].fullname.endsWith(this.suffix)) {
const name = priv.format_file_name.bind(this)(files[key].fullname)
const instance = require(files[key].fullname)
if (app.di().__is_injectable(instance)) {
app.di().inject(instance)
}
this.canonical_items[name] = await this.init_canonical_file({
app, name, instance
})
}
this.canonical_items[name] = await this.init_canonical_file({
app, name, instance
} catch (e) {
throw error_context(e, {
file_name: files?.[key]?.fullname,
canonical_item: this.canonical_item,
})
}
}
@ -133,13 +147,19 @@ class CanonicalUnit extends Unit {
* @returns {object}
*/
get(name) {
const name_parts = name.split('.')
let descending_value = this.canonical_items
name_parts.forEach(part => {
descending_value = descending_value[part]
})
return descending_value
try {
const name_parts = name.split('.')
let descending_value = this.canonical_items
name_parts.forEach(part => {
descending_value = descending_value[part]
})
return descending_value
} catch (e) {
return error_context(e, {
resource_name: name,
})
}
}
}

@ -4,6 +4,7 @@
const CanonicalUnit = require('../canon/CanonicalUnit')
const path = require('path')
const error_context = require('../errors/error_context.fn')
// Private helper functions
// Some of these are duplicated in UtilityUnit,
@ -28,14 +29,20 @@ const priv = {
* @return {boolean|null|*|number|undefined}
*/
infer(val){
if ( !val ) return null
else if ( val.toLowerCase() === 'true' ) return true
else if ( val.toLowerCase() === 'false' ) return false
else if ( !isNaN(val) ) return +val
else if ( this.is_json(val) ) return JSON.parse(val)
else if ( val.toLowerCase() === 'null' ) return null
else if ( val.toLowerCase() === 'undefined' ) return undefined
else return val
try {
if (!val) return null
else if (val.toLowerCase() === 'true') return true
else if (val.toLowerCase() === 'false') return false
else if (!isNaN(val)) return +val
else if (this.is_json(val)) return JSON.parse(val)
else if (val.toLowerCase() === 'null') return null
else if (val.toLowerCase() === 'undefined') return undefined
else return val
} catch (e) {
throw error_context(e, {
inference_value: val,
})
}
},
/**

@ -5,6 +5,7 @@
const CanonicalUnit = require('../canon/CanonicalUnit')
const Controller = require('./Controller')
const SoftError = require('../errors/SoftError')
const error_context = require('../errors/error_context.fn')
/**
* Unit to load and manage controller class definitions.
@ -61,22 +62,30 @@ class ControllerUnit extends CanonicalUnit {
* @returns {module:libflitter/controller/Controller~Controller|function}
*/
get(name) {
const name_parts = name.split('.')
const controller_instance = this.canonical_items[name_parts[0]]
try {
const name_parts = name.split('.')
const controller_instance = this.canonical_items[name_parts[0]]
if ( name_parts.length > 1 ) {
const method_instance = controller_instance[name_parts[1]].bind(controller_instance)
if (name_parts.length > 1) {
const method_instance = controller_instance[name_parts[1]].bind(controller_instance)
if ( name_parts > 2 ) {
let descending_value = method_instance
name_parts.slice(2).forEach(part => {
descending_value = descending_value[part]
})
if (name_parts > 2) {
let descending_value = method_instance
name_parts.slice(2).forEach(part => {
descending_value = descending_value[part]
})
return descending_value
return descending_value
} else {
return method_instance
}
} else {
return method_instance
return controller_instance
}
} catch (e) {
throw error_context(e, {
resource_name: name,
})
}
}

@ -7,6 +7,7 @@ const Model = require('flitter-orm/src/model/Model')
const Setting = require('./model/Setting.model')
const ScaffoldService = require('flitter-orm/src/services/Connection')
const SoftError = require('../errors/SoftError')
const error_context = require('../errors/error_context.fn')
/**
* Unit to load and manage database models.
@ -90,15 +91,21 @@ class DatabaseModelsUnit extends CanonicalUnit {
*/
external_model(unit, name, instance) {
name = `${unit.name()}::${name}`
this.output.info(`Registering external model: ${name}`)
if ( instance.prototype instanceof Model ) {
instance.collection = name.replace(/:/g, '_')
this.canonical_items[name] = this.app.di().inject(instance)
} else {
this.output.warn(`Invalid or malformed model ${name}. Models should extend from flitter-orm/src/model/Model.`)
}
try {
this.output.info(`Registering external model: ${name}`)
if (instance.prototype instanceof Model) {
instance.collection = name.replace(/:/g, '_')
this.canonical_items[name] = this.app.di().inject(instance)
} else {
this.output.warn(`Invalid or malformed model ${name}. Models should extend from flitter-orm/src/model/Model.`)
}
return name
return name
} catch (e) {
throw error_context(e, {
external_model_name: name,
})
}
}
/**

@ -0,0 +1,54 @@
/**
* Adds additional contextual variables to an error and
* modifies its stacktrace and toString output to include
* that context.
*
* @param {Error} in_error
* @param {object} context
* @returns {Error}
*/
const error_context = (in_error, context = {}) => {
// If the incoming error already has context, merge the two
if ( typeof in_error.context === 'object' ) {
in_error.context = {...in_error.context, ...context}
} else { // Otherwise, assign the context
in_error.context = context
}
// Preserve the original stacktrace
if ( !in_error.original_stack ) {
in_error.original_stack = in_error.stack
}
in_error.toString = () => {
// Get the JS stacktrace
let stack = in_error.original_stack
// Build the context display
const context_lines = []
if ( typeof in_error.context === 'object' ) {
for ( const name in in_error.context ) {
const val = in_error.context[name]
try {
const string = JSON.stringify(val)
context_lines.push(` - ${name}: ${string}`)
} catch (e) {
if ( typeof val.toString === 'function' ) {
context_lines.push(` - ${name}: ${val}`)
}
}
}
}
if ( context_lines.length > 0 ) {
stack += `\n\nWith the following context:\n${context_lines.join('\n')}\n`
}
return stack
}
in_error.stack = in_error.toString()
return in_error
}
module.exports = exports = error_context

@ -63,6 +63,8 @@ const libflitter = {
ViewEngineUnit: require('./views/ViewEngineUnit'),
Unit: require('./Unit'),
error_context: require('./errors/error_context.fn')
}
module.exports = exports = libflitter

@ -6,6 +6,7 @@ const CanonicalUnit = require('../canon/CanonicalUnit')
const Middleware = require('./Middleware')
const SoftError = require('../errors/SoftError')
const path = require('path')
const error_context = require('../errors/error_context.fn')
/**
* Unit to load and manage middleware class definitions.
@ -102,13 +103,19 @@ class MiddlewareUnit extends CanonicalUnit {
* @returns {function} - the Express middleware
*/
get(name, args) {
if ( !this.canonical_items[name] ) {
throw (new SoftError('Attempted to access middleware that does not exist: '+name)).unit(this.constructor.name)
}
try {
if (!this.canonical_items[name]) {
throw (new SoftError('Attempted to access middleware that does not exist: ' + name)).unit(this.constructor.name)
}
return (req, res, next, merge_args) => {
let pass_args = typeof args === 'undefined' ? (merge_args ? merge_args : {}) : args
return this.canonical_items[name].test(req, res, next, pass_args)
return (req, res, next, merge_args) => {
let pass_args = typeof args === 'undefined' ? (merge_args ? merge_args : {}) : args
return this.canonical_items[name].test(req, res, next, pass_args)
}
} catch(e) {
throw error_context(e, {
resource_name: name,
})
}
}

@ -4,6 +4,7 @@
const { Injectable } = require('flitter-di')
const statuses = require('http-status')
const error_context = require('../errors/error_context.fn')
/**
* System Middleware is an abstract Flitter construct. System middleware is applied to
@ -79,14 +80,22 @@ class ResponseSystemMiddleware extends Injectable {
const status_code = this._status ? this._status : 200
const message = this._message ? this._message : statuses[status_code]
this.old_status(status_code)
this.output.info(`API Response (HTTP ${status_code}) - ${this.request.ip}${this.request.xhr ? ' (XHR)' : ''} - ${this.request.method} ${this.request.path}`)
return this.response.send({
status: status_code,
message,
data,
})
try {
this.old_status(status_code)
this.output.info(`API Response (HTTP ${status_code}) - ${this.request.ip}${this.request.xhr ? ' (XHR)' : ''} - ${this.request.method} ${this.request.path}`)
return this.response.send({
status: status_code,
message,
data,
})
} catch (e) {
throw error_context(e, {
status_code,
message,
data,
})
}
}
/**
@ -133,19 +142,27 @@ class ResponseSystemMiddleware extends Injectable {
* @returns {Promise<*>} - returns the output of the wrapped function
*/
async page(view_name, args = {}, resource_list = {}){
// Automatically include some useful data.
const _app = {
name: this.configs.get('app.name'),
url: this.configs.get('app.url'),
try {
// Automatically include some useful data.
const _app = {
name: this.configs.get('app.name'),
url: this.configs.get('app.url'),
}
// Get the user, if they exist:
if (this.request.session.auth && this.request.session.auth.user) {
_app.user = this.request.session.auth.user
}
this.output.info(`Page Render Response (HTTP ${this._status ? this._status : 200}) - ${this.request.ip}${this.request.xhr ? ' (XHR)' : ''} - ${this.request.method} ${this.request.path}`)
return await this.views.view(this.response, view_name, {...{_app}, ...args}, resource_list)
} catch (e) {
throw error_context(e, {
view_name,
view_args: args,
view_resource_list: resource_list,
})
}
// Get the user, if they exist:
if ( this.request.session.auth && this.request.session.auth.user ){
_app.user = this.request.session.auth.user
}
this.output.info(`Page Render Response (HTTP ${this._status ? this._status : 200}) - ${this.request.ip}${this.request.xhr ? ' (XHR)' : ''} - ${this.request.method} ${this.request.path}`)
return await this.views.view(this.response, view_name, {...{_app}, ...args}, resource_list)
}
}

@ -8,6 +8,7 @@ const express = require('express')
const StopError = require('../errors/StopError')
const FatalError = require('../errors/FatalError')
const SoftError = require('../errors/SoftError')
const error_context = require('../errors/error_context.fn')
/**
* Unit to load, parse, and manage router definitions.
@ -70,20 +71,27 @@ class RoutingUnit extends CanonicalUnit {
let mw_val = instance.middleware[i]
let mw_args = {}
if ( Array.isArray(mw_val) && mw_val.length > 0 && typeof mw_val[0] === 'string' ) {
if ( mw_val.length > 1 ) {
mw_args = mw_val[1]
}
try {
if (Array.isArray(mw_val) && mw_val.length > 0 && typeof mw_val[0] === 'string') {
if (mw_val.length > 1) {
mw_args = mw_val[1]
}
mw_val = mw_val[0]
}
mw_val = mw_val[0]
}
if ( typeof mw_val === 'string' ) {
const mw = this.canon.get(`middleware::${mw_val}`)
router.use(this.system_middleware(app, mw, mw_args))
} else {
this.output.error(`Specifying middlewares as functions is no longer supported. Prefer specifying the canonical name of the middleware. (Router: ${name})`)
throw (new SoftError('Invalid route handler. Please specify the name of the canonical resource to inject in the route.')).unit(this.constructor.name)
if (typeof mw_val === 'string') {
const mw = this.canon.get(`middleware::${mw_val}`)
router.use(this.system_middleware(app, mw, mw_args))
} else {
this.output.error(`Specifying middlewares as functions is no longer supported. Prefer specifying the canonical name of the middleware. (Router: ${name})`)
throw (new SoftError('Invalid route handler. Please specify the name of the canonical resource to inject in the route.')).unit(this.constructor.name)
}
} catch (e) {
throw error_context(e, {
middleware_name: mw_val,
middleware_args: mw_args,
})
}
}
}
@ -95,30 +103,37 @@ class RoutingUnit extends CanonicalUnit {
valid_route_types.forEach(type => {
if ( type in instance ) {
for ( let path in instance[type] ) {
let handlers = instance[type][path]
if ( !Array.isArray(handlers) ) handlers = [handlers]
const express_handlers = []
handlers.forEach(h => {
let h_args = undefined
if ( Array.isArray(h) && h.length > 0 && typeof h[0] === 'string' ) {
if ( h.length > 1 ) {
h_args = h[1]
try {
let handlers = instance[type][path]
if (!Array.isArray(handlers)) handlers = [handlers]
const express_handlers = []
handlers.forEach(h => {
let h_args = undefined
if (Array.isArray(h) && h.length > 0 && typeof h[0] === 'string') {
if (h.length > 1) {
h_args = h[1]
}
h = h[0]
}
h = h[0]
}
if ( typeof h === 'string' ) {
const handler = this.canon.get(h)
express_handlers.push(this.system_middleware(app, handler, h_args))
} else {
this.output.error(`Specifying route handlers as functions is no longer supported. Prefer specifying the canonical name of the handlers. (Router: ${name}, Method: ${type}, Route: ${path})`)
throw (new SoftError('Invalid route handler. Please specify the name of the canonical resource to inject in the route.')).unit(this.constructor.name)
}
})
router[type](path.startsWith('/') ? path : `/${path}`, express_handlers)
if (typeof h === 'string') {
const handler = this.canon.get(h)
express_handlers.push(this.system_middleware(app, handler, h_args))
} else {
this.output.error(`Specifying route handlers as functions is no longer supported. Prefer specifying the canonical name of the handlers. (Router: ${name}, Method: ${type}, Route: ${path})`)
throw (new SoftError('Invalid route handler. Please specify the name of the canonical resource to inject in the route.')).unit(this.constructor.name)
}
})
router[type](path.startsWith('/') ? path : `/${path}`, express_handlers)
} catch (e) {
throw error_context(e, {
route_verb_type: type,
route_path: path,
})
}
}
}
})
@ -174,6 +189,14 @@ class RoutingUnit extends CanonicalUnit {
await app.app_error(e)
}
e = error_context(e, {
request_path: req.path,
request_query: req.query,
request_body: req.body,
request_params: req.params,
request_method: req.method,
})
this.output.error(`Error encountered while handling request (${req.method} ${req.path}): ${e.message}`)
return next(e)
}

@ -3,6 +3,7 @@
*/
const CanonicalUnit = require('../canon/CanonicalUnit')
const error_context = require('../errors/error_context.fn')
/**
* Unit to load and manage externally-defined services.
@ -47,8 +48,14 @@ class ServicesUnit extends CanonicalUnit {
* @returns {Promise<*>}
*/
async init_canonical_file({app, name, instance}) {
await this.app.di().container.register_service(name, instance)
return instance
try {
await this.app.di().container.register_service(name, instance)
return instance
} catch (e) {
throw error_context(e, {
file_name: name,
})
}
}
/**

@ -6,6 +6,7 @@ const Unit = require('../Unit')
const path = require('path')
const Output = require('./services/Output.service')
const error_context = require('../errors/error_context.fn')
/**
* The utility unit contains various utility functions and tools that
@ -163,19 +164,25 @@ class UtilityUnit extends Unit {
* @returns {Object}
*/
deep_copy(item){
if ( Array.isArray(item) ){
const ret = []
item.forEach(e => {
ret.push(this.deep_copy(e))
})
return ret
} else if ( typeof item === 'object' ){
const ret = {}
Object.keys(item).forEach(key => {
ret[key] = this.deep_copy(item[key])
try {
if (Array.isArray(item)) {
const ret = []
item.forEach(e => {
ret.push(this.deep_copy(e))
})
return ret
} else if (typeof item === 'object') {
const ret = {}
Object.keys(item).forEach(key => {
ret[key] = this.deep_copy(item[key])
})
return ret
} else return item
} catch (e) {
throw error_context(e, {
deep_copy_item: item,
})
return ret
} else return item
}
}
/* Useful functions */
@ -188,14 +195,21 @@ class UtilityUnit extends Unit {
* @returns {*}
*/
deep_merge(obj1, obj2) {
for ( let key in obj2 ){
if ( !Object.keys(obj1).includes(key) ) obj1[key] = obj2[key]
else if ( typeof obj1[key] !== 'object' ) obj1[key] = obj2[key]
else if ( typeof obj2[key] !== 'object' ) obj1[key] = obj2[key]
else obj1[key] = this.deep_merge(obj1[key], obj2[key])
}
try {
for (let key in obj2) {
if (!Object.keys(obj1).includes(key)) obj1[key] = obj2[key]
else if (typeof obj1[key] !== 'object') obj1[key] = obj2[key]
else if (typeof obj2[key] !== 'object') obj1[key] = obj2[key]
else obj1[key] = this.deep_merge(obj1[key], obj2[key])
}
return obj1
return obj1
} catch (e) {
throw error_context(e, {
deep_merge_obj1: obj1,
deep_merge_obj2: obj2,
})
}
}
/**
@ -216,14 +230,20 @@ class UtilityUnit extends Unit {
* @return {boolean|null|*|number|undefined}
*/
infer(val){
if ( !val ) return null
else if ( val.toLowerCase() === 'true' ) return true
else if ( val.toLowerCase() === 'false' ) return false
else if ( !isNaN(val) ) return +val
else if ( this.is_json(val) ) return JSON.parse(val)
else if ( val.toLowerCase() === 'null' ) return null
else if ( val.toLowerCase() === 'undefined' ) return undefined
else return val
try {
if (!val) return null
else if (val.toLowerCase() === 'true') return true
else if (val.toLowerCase() === 'false') return false
else if (!isNaN(val)) return +val
else if (this.is_json(val)) return JSON.parse(val)
else if (val.toLowerCase() === 'null') return null
else if (val.toLowerCase() === 'undefined') return undefined
else return val
} catch (e) {
throw error_context(e, {
infer_value: val,
})
}
}
/**

@ -4,6 +4,7 @@
const path = require('path')
const Unit = require('../Unit')
const error_context = require('../errors/error_context.fn')
/**
* The view engine unit is responsible for registering the view engine
@ -78,19 +79,25 @@ class ViewEngineUnit extends Unit {
let resources = {}
for ( let find_key in resource_list ){
const find = resource_list[find_key]
let returns;
if ( find.find === 'one' ){
returns = await this.models.get(find.model).findOne(find.criteria)
}
else {
returns = await this.models.get(find.model).find(find.criteria)
try {
const find = resource_list[find_key]
let returns;
if (find.find === 'one') {
returns = await this.models.get(find.model).findOne(find.criteria)
} else {
returns = await this.models.get(find.model).find(find.criteria)
}
if (find.modifier) returns = await find.modifier(returns)
resources[find.name] = returns
} catch (e) {
throw error_context(e, {
view_name,
resource_find_key: find_key,
})
}
if ( find.modifier ) returns = await find.modifier(returns)
resources[find.name] = returns
}
args = {...args, ...resources}

Loading…
Cancel
Save