@samanbayaka/core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +812 -0
- package/commit-hash.mjs +1 -0
- package/helper/mol-built-in/AjvValidator.mjs +90 -0
- package/helper/mol-built-in/CustomLogger.mjs +83 -0
- package/helper/mol-built-in/HybridCacher.mjs +155 -0
- package/helper/utility/access-token-validator.mjs +249 -0
- package/helper/utility/aux-broker-params-validator.mjs +126 -0
- package/helper/utility/check-syntax.mjs +91 -0
- package/helper/utility/config-handler.mjs +161 -0
- package/helper/utility/error-handler.mjs +456 -0
- package/helper/utility/file-handler.mjs +121 -0
- package/helper/utility/global-configs-validator.mjs +47 -0
- package/helper/utility/openapi-to-mol-params.mjs +84 -0
- package/helper/utility/sign-jwt.mjs +35 -0
- package/helper/utility/telemetry.mjs +129 -0
- package/index.mjs +427 -0
- package/package.json +78 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import * as fs from "fs"
|
|
2
|
+
import * as path from "path"
|
|
3
|
+
import * as acorn from "acorn"
|
|
4
|
+
|
|
5
|
+
import { ABSOLUTE_PATH } from '#hUti/file-handler.mjs'
|
|
6
|
+
|
|
7
|
+
const extensions = new Set([".js", ".mjs"])
|
|
8
|
+
|
|
9
|
+
const getFiles = async(dir) => {
|
|
10
|
+
const entries = await fs.promises.readdir(dir, {
|
|
11
|
+
withFileTypes: true
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const files = []
|
|
15
|
+
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
if (entry.name === "node_modules") {
|
|
18
|
+
continue
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const fullPath = path.join(dir, entry.name)
|
|
22
|
+
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
files.push(...await getFiles(fullPath))
|
|
25
|
+
} else if (extensions.has(path.extname(entry.name))) {
|
|
26
|
+
files.push(fullPath)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return files
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const checkSyntax = async(file) => {
|
|
34
|
+
const source = await fs.promises.readFile(file, "utf8")
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
acorn.parse(source, {
|
|
38
|
+
ecmaVersion: "latest",
|
|
39
|
+
sourceType: "module",
|
|
40
|
+
locations: true
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
return null
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return {
|
|
46
|
+
file,
|
|
47
|
+
message: err.message,
|
|
48
|
+
line: err.loc?.line,
|
|
49
|
+
column: err.loc?.column
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const checkProjectSyntax = async(projectDir = ABSOLUTE_PATH) => {
|
|
55
|
+
const files = await getFiles(path.resolve(projectDir))
|
|
56
|
+
const errors = []
|
|
57
|
+
|
|
58
|
+
for (const file of files) {
|
|
59
|
+
const error = await checkSyntax(file)
|
|
60
|
+
|
|
61
|
+
if (error) {
|
|
62
|
+
errors.push(error)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
filesChecked: files.length,
|
|
68
|
+
errors
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// // Example
|
|
73
|
+
// const result = await checkProjectSyntax()
|
|
74
|
+
|
|
75
|
+
// if (result.errors.length === 0) {
|
|
76
|
+
// console.log(
|
|
77
|
+
// `No syntax errors found (${result.filesChecked} files checked)`
|
|
78
|
+
// )
|
|
79
|
+
// } else {
|
|
80
|
+
// console.error(
|
|
81
|
+
// `Found ${result.errors.length} syntax error(s):`
|
|
82
|
+
// )
|
|
83
|
+
|
|
84
|
+
// for (const error of result.errors) {
|
|
85
|
+
// console.error(
|
|
86
|
+
// `${error.file}:${error.line}:${error.column} - ${error.message}`
|
|
87
|
+
// )
|
|
88
|
+
// }
|
|
89
|
+
|
|
90
|
+
// process.exitCode = 1
|
|
91
|
+
// }
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Etcd3 } from "etcd3"
|
|
2
|
+
import YAML from 'yaml'
|
|
3
|
+
|
|
4
|
+
import * as errHdl from '#hUti/error-handler.mjs'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Handle uncaught errors
|
|
8
|
+
*/
|
|
9
|
+
process.on("uncaughtException", async (err) => {
|
|
10
|
+
})
|
|
11
|
+
process.on("unhandledRejection", async (err) => {
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* User credentials for accessing and reading configurations from
|
|
17
|
+
* the configuration server.
|
|
18
|
+
* @type {string}
|
|
19
|
+
* @return {array} [user, password, port]
|
|
20
|
+
*/
|
|
21
|
+
export const etcdCred = errHdl.validateConfigCred(process.env.SBK_CONFIG_CRED)
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Configuration Reader Client Options
|
|
25
|
+
* @type {object}
|
|
26
|
+
*/
|
|
27
|
+
export const client = new Etcd3({
|
|
28
|
+
hosts: `http://etcd:${etcdCred[2]}`,
|
|
29
|
+
auth: {
|
|
30
|
+
username: etcdCred[0],
|
|
31
|
+
password: etcdCred[1]
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Getting configurations against a Etcd key
|
|
37
|
+
* @param {string}
|
|
38
|
+
* @return {object | string}
|
|
39
|
+
*/
|
|
40
|
+
export const getConfigs = async (path, isYaml = false) => {
|
|
41
|
+
try {
|
|
42
|
+
const yamlText = (await client.get(path))?.toString()
|
|
43
|
+
if ( path.split('/').includes('yaml') ) {
|
|
44
|
+
return isYaml ? yamlText : Object.freeze( YAML.parse( yamlText ) )
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
return yamlText
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
} catch (err) {
|
|
51
|
+
throw new Error(`Unable to read configuration: ${err.message}`)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Getting all keys having prefix path
|
|
57
|
+
* @param {string} path
|
|
58
|
+
* @param {Boolean} fullPath
|
|
59
|
+
* @return {array}
|
|
60
|
+
*/
|
|
61
|
+
export const getConfigKeys = async (path, fullPath = false) => {
|
|
62
|
+
try {
|
|
63
|
+
const entries = await client.getAll()
|
|
64
|
+
.prefix(path)
|
|
65
|
+
.strings()
|
|
66
|
+
|
|
67
|
+
const keys = Object.keys(entries)
|
|
68
|
+
|
|
69
|
+
return fullPath ? keys : keys.map( e => e.split('/').pop() )
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
throw new Error(`Unable to read configuration keys: ${err.message}`)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create Etcd key with value
|
|
78
|
+
* @param {string} path
|
|
79
|
+
* @param {string} data
|
|
80
|
+
*/
|
|
81
|
+
export const putConfig = async(path, data) => {
|
|
82
|
+
await client.put(path)
|
|
83
|
+
.value(data)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Delete Etcd key
|
|
88
|
+
* @param {string} path
|
|
89
|
+
* @return {string}
|
|
90
|
+
*/
|
|
91
|
+
export const delConfig = async(path) => {
|
|
92
|
+
const deletedData = await client
|
|
93
|
+
.delete()
|
|
94
|
+
.key(path)
|
|
95
|
+
.getPrevious()
|
|
96
|
+
|
|
97
|
+
return deletedData.map(item => ({
|
|
98
|
+
...item,
|
|
99
|
+
key: Buffer.from(item.key).toString("utf8"),
|
|
100
|
+
value: Buffer.from(item.value).toString("utf8"),
|
|
101
|
+
}))
|
|
102
|
+
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
export const getConfigAll = async (path, fullPath = false) => {
|
|
107
|
+
try {
|
|
108
|
+
const entries = await client.getAll()
|
|
109
|
+
.prefix(path)
|
|
110
|
+
.strings()
|
|
111
|
+
|
|
112
|
+
return fullPath
|
|
113
|
+
? entries
|
|
114
|
+
: Object.fromEntries(
|
|
115
|
+
Object.entries(entries).map(([key, value]) => [
|
|
116
|
+
key.replace(path, ""),
|
|
117
|
+
value
|
|
118
|
+
])
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
throw new Error(`Unable to read configuration keys: ${err.message}`)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
export const watchConfigs = async (path, callback) => {
|
|
128
|
+
try {
|
|
129
|
+
const watcher = await client
|
|
130
|
+
.watch()
|
|
131
|
+
.prefix(path)
|
|
132
|
+
.create()
|
|
133
|
+
|
|
134
|
+
watcher.on("put", event => {
|
|
135
|
+
const evVal = path.split('/').includes('yaml')
|
|
136
|
+
? Object.freeze( YAML.parse(event.value.toString()) )
|
|
137
|
+
: event.value?.toString()
|
|
138
|
+
callback(
|
|
139
|
+
"put",
|
|
140
|
+
event.key?.toString(),
|
|
141
|
+
evVal
|
|
142
|
+
)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
watcher.on("delete", event => {
|
|
146
|
+
callback(
|
|
147
|
+
"delete",
|
|
148
|
+
event.key.toString(),
|
|
149
|
+
null
|
|
150
|
+
)
|
|
151
|
+
})
|
|
152
|
+
return watcher
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
throw new Error(`Unable to watck configuration: ${err.message}`)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import dns from 'node:dns'
|
|
2
|
+
import {STATUS_CODES} from 'node:http'
|
|
3
|
+
import { Errors } from "moleculer"
|
|
4
|
+
import { COMMIT_HASH } from "../../commit-hash.mjs"
|
|
5
|
+
import { checkProjectSyntax } from '#hUti/check-syntax.mjs'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
const NODE_VER_REQ = 22
|
|
9
|
+
const envRegx = /^(development|production|testing)$/
|
|
10
|
+
const sysClassification = { development: 'dev', testing: 'tst', production: 'prd' }
|
|
11
|
+
const commitHsRegx = /^[a-f0-9]{7}$/i
|
|
12
|
+
const configCredRegex = /^[a-zA-Z0-9]{3,16} [a-zA-Z0-9!@#$^_-]{3,16} [0-9]{4,5}$/
|
|
13
|
+
const serviceNmRegx = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/
|
|
14
|
+
const portRegx = /^876[0-9]$/
|
|
15
|
+
const logLvlRegx = /^(fatal|error|warn|info|debug|trace)$/i
|
|
16
|
+
const telemetryRegx = /^(true|false)$/i
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Retain the lines from the error stack that contain "file:///"
|
|
21
|
+
* @param {string} error
|
|
22
|
+
* @return {string}
|
|
23
|
+
*/
|
|
24
|
+
export const extractFileLines = (stack) => {
|
|
25
|
+
if (!String(stack).includes("file:///")) {
|
|
26
|
+
return stack
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const str = String(stack)
|
|
30
|
+
.split(/\r?\n/)
|
|
31
|
+
|
|
32
|
+
return [
|
|
33
|
+
str[0],
|
|
34
|
+
...str.filter(line => line.includes("file:///"))
|
|
35
|
+
].join("\n")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validate Node version
|
|
40
|
+
* @return {number}
|
|
41
|
+
*
|
|
42
|
+
* @throws error
|
|
43
|
+
*/
|
|
44
|
+
export const checkMinNoveVer = () => {
|
|
45
|
+
const NODE_VER_CUR = parseInt(process.versions.node.split('.')[0])
|
|
46
|
+
if(NODE_VER_CUR < NODE_VER_REQ){
|
|
47
|
+
throw new Error(`Node.js >=${NODE_VER_REQ}.x.x is required but current is ${NODE_VER_CUR}.x.x.`)
|
|
48
|
+
}
|
|
49
|
+
return NODE_VER_REQ
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validate APP_ENV entry
|
|
55
|
+
* @return {string}
|
|
56
|
+
*
|
|
57
|
+
* @throws error
|
|
58
|
+
*/
|
|
59
|
+
export const isValidAppEnv = (env) => {
|
|
60
|
+
if (!envRegx.test(env)) {
|
|
61
|
+
throw new Error(`APP_ENV is '${env}', it must be 'development', 'production', or 'testing'`)
|
|
62
|
+
}
|
|
63
|
+
return sysClassification[env]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Validate git commit hash
|
|
69
|
+
* @return {string}
|
|
70
|
+
*
|
|
71
|
+
* @throws error
|
|
72
|
+
*/
|
|
73
|
+
export const isValidCommitHash = (hs) => {
|
|
74
|
+
if(!commitHsRegx.test(hs)){
|
|
75
|
+
throw new Error(`Git commit hash is '${hs}', it must be 7-character hex string.`)
|
|
76
|
+
}
|
|
77
|
+
return hs
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Validate namespace
|
|
83
|
+
* @retutn {string}
|
|
84
|
+
*/
|
|
85
|
+
export const validateNamespace = (env) => {
|
|
86
|
+
return `${isValidAppEnv(env)}-sbk-${isValidCommitHash(COMMIT_HASH)}`.toUpperCase()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Validate the string for establis Etcd3 client connection
|
|
92
|
+
* @return {string}
|
|
93
|
+
*
|
|
94
|
+
* @throws error
|
|
95
|
+
*/
|
|
96
|
+
export const validateConfigCred = (env) => {
|
|
97
|
+
if(!configCredRegex.test(env)){
|
|
98
|
+
throw new Error(`Environment variable SBK_CONFIG_CRED is "${env}". It must be a valid Etcd user credential in the format "user password port".`)
|
|
99
|
+
}
|
|
100
|
+
return env.split(" ")
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Validate that the MAX_L1_TTL is defined and does not exceed 120 seconds
|
|
106
|
+
* @return {number} between 0 to 120
|
|
107
|
+
*
|
|
108
|
+
* @throws error
|
|
109
|
+
*/
|
|
110
|
+
export const validateL1TTL = (ttl = 120) => {
|
|
111
|
+
const num = Number(ttl)
|
|
112
|
+
if (!Number.isInteger(num) || num < 0 || num > 120) {
|
|
113
|
+
throw new Error(`Configuration variable 'MAX_L1_TTL' is ${ttl}, it must be between 0 and 120`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return num
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Validate that the MAX_L2_TTL is defined and does not exceed 600 seconds
|
|
122
|
+
* @return {number} between 0 to 600
|
|
123
|
+
*
|
|
124
|
+
* @throws error
|
|
125
|
+
*/
|
|
126
|
+
export const validateL2TTL = (ttl = 600) => {
|
|
127
|
+
const num = Number(ttl)
|
|
128
|
+
if (!Number.isInteger(num) || num < 0 || num > 600) {
|
|
129
|
+
throw new Error(`Configuration variable 'MAX_L2_TTL' is ${ttl}, it must be between 0 and 600`)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return num
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Validate configuration variable SBK_PORT for Apigateway port number
|
|
138
|
+
* @return {number} between 8760 to 8769
|
|
139
|
+
*
|
|
140
|
+
* @throws error
|
|
141
|
+
*/
|
|
142
|
+
export const validatePort = (port) => {
|
|
143
|
+
if ( port !== undefined && !(portRegx.test(port)) ) {
|
|
144
|
+
throw new Error(`Configuration variable 'PORT' is ${port}, it must be between 8760 and 8769`)
|
|
145
|
+
}
|
|
146
|
+
return Number(port || 8765)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Validate environment variable SBK_LOG_LEVEL
|
|
152
|
+
* @return {string}
|
|
153
|
+
*
|
|
154
|
+
* @throws error
|
|
155
|
+
*/
|
|
156
|
+
export const validateLogLevel = (env) => {
|
|
157
|
+
if ( !(logLvlRegx.test(env)) ) {
|
|
158
|
+
throw new Error(`LOG_LEVEL is ${env}, it must be fatal | error | warn | info | debug | trace `)
|
|
159
|
+
}
|
|
160
|
+
return env
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Validate hosts entry
|
|
166
|
+
* @param {string} dnsTxt
|
|
167
|
+
* @param {Boolean} isOptional
|
|
168
|
+
* @return {boolean}
|
|
169
|
+
*
|
|
170
|
+
* @throws error
|
|
171
|
+
* @throws warn
|
|
172
|
+
*/
|
|
173
|
+
export const validateHostEntry = (dnsTxt, isOptional = false) => {
|
|
174
|
+
return new Promise((resolve, reject) => {
|
|
175
|
+
dns.lookup(dnsTxt, (err, address) => {
|
|
176
|
+
if (err || !address) {
|
|
177
|
+
if (isOptional){
|
|
178
|
+
console.warn("\x1b[33m%s\x1b[0m", `SBKWRN ❖ Missing hosts entry for "${dnsTxt}".`)
|
|
179
|
+
return resolve(false)
|
|
180
|
+
}
|
|
181
|
+
return reject(new Error(`Missing hosts entry for "${dnsTxt}".`))
|
|
182
|
+
}
|
|
183
|
+
resolve(true)
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Validates a service name against a predefined regex pattern.
|
|
191
|
+
*
|
|
192
|
+
* @param {string} name - The service name to validate.
|
|
193
|
+
* @returns {boolean} Returns true if the service name is valid.
|
|
194
|
+
*
|
|
195
|
+
* @throws error
|
|
196
|
+
*/
|
|
197
|
+
export const validateServiceName = (schema, dirName) => {
|
|
198
|
+
const pkgName = dirName.replace(/^\$/, '')
|
|
199
|
+
if(!serviceNmRegx.test(pkgName)) {
|
|
200
|
+
throw new Error(`Invalid service name "${schema.name}".\n Only lowercase letters (a–z), digits (0–9), and hyphens (-) are allowed.`)
|
|
201
|
+
}
|
|
202
|
+
if(schema.name.replace(/^\$/, '') != pkgName){
|
|
203
|
+
throw new Error(`Service name and package name must be the same, but found "${schema.name.replace(/^\$/, '')}" and "${pkgName}"`)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* convert package name like "kafka-produce"
|
|
208
|
+
* to moleculer supported service name "kafka.produce"
|
|
209
|
+
*/
|
|
210
|
+
schema.name = schema.name.replace(/-/g, ".")
|
|
211
|
+
return true
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Telemetry enable or disable inputs
|
|
217
|
+
* @param {string}
|
|
218
|
+
* @return {boolean}
|
|
219
|
+
*/
|
|
220
|
+
export const validateTelemetry = (env) => {
|
|
221
|
+
if ( !(telemetryRegx.test(env)) ) {
|
|
222
|
+
throw new Error(`TELEMETRY is ${env}, it must be true | false `)
|
|
223
|
+
}
|
|
224
|
+
return env.toLowerCase() === "true" ? true : false
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Validate config revision
|
|
229
|
+
* @param {string}
|
|
230
|
+
* @return {object}
|
|
231
|
+
*/
|
|
232
|
+
export const validateConfRevision = (env) => {
|
|
233
|
+
env = env || '{"pub":0,"edg":0,"mem":{"system":0,"gateway":0}}'
|
|
234
|
+
return JSON.parse(env)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Graceful shutdown handler
|
|
240
|
+
*/
|
|
241
|
+
export const gracefulShutdown = async (broker, signal, err=null) => {
|
|
242
|
+
// broker.logger.info({tag:"SBK", message: `${signal} detected, Stopping the broker...`})
|
|
243
|
+
try {
|
|
244
|
+
await broker.stop()
|
|
245
|
+
if(err) {
|
|
246
|
+
const { message, stack } = err
|
|
247
|
+
// broker.logger.error({tag: "SBK", message: `Broker stopped after detecting the ${signal} signal.`}, {err})
|
|
248
|
+
broker.logger.error({tag: "SBK", message, stack: extractFileLines(stack)})
|
|
249
|
+
process.exit(1)
|
|
250
|
+
}
|
|
251
|
+
else{
|
|
252
|
+
broker.logger.info({tag: "SBK", message: "Broker stopped gracefully."})
|
|
253
|
+
process.exit(0)
|
|
254
|
+
}
|
|
255
|
+
} catch (brokerStoppingErr) {
|
|
256
|
+
if(broker) {
|
|
257
|
+
broker.logger.error({tag: "SBK"}, {brokerStoppingErr}, "Unable to stop the broker.")
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
const { message, stack } = err
|
|
261
|
+
if (err instanceof SyntaxError) {
|
|
262
|
+
const result = await checkProjectSyntax()
|
|
263
|
+
if(result.errors.length > 0){
|
|
264
|
+
for (const error of result.errors) {
|
|
265
|
+
const {file, line, column} = error
|
|
266
|
+
console.error(
|
|
267
|
+
"\x1b[31m%s\x1b[0m",
|
|
268
|
+
` ❖❖ SBKERR ❖❖ \nSyntaxError: ${err.message}\n at ${file.replace(/^.*node_modules\//, "../")}:${line}:${column}`
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
console.error("\x1b[31m%s\x1b[0m", ` ❖❖ SBKERR ❖❖ \n ${extractFileLines(stack)}`)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
process.exit(1)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Captures errors thrown during request processing, logs relevant details
|
|
285
|
+
* (message, stack trace, request context), and formats a standardized
|
|
286
|
+
* error response before sending it to the client.
|
|
287
|
+
*
|
|
288
|
+
* @type {Object}
|
|
289
|
+
*/
|
|
290
|
+
export const ErrorFormatterMW = {
|
|
291
|
+
localAction: (next) => {
|
|
292
|
+
return async(ctx) => {
|
|
293
|
+
try {
|
|
294
|
+
return await next(ctx)
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
if(!err?.data?.requestID) {
|
|
298
|
+
const requestID = ctx?.requestID
|
|
299
|
+
const status = err?.code || 500
|
|
300
|
+
const type = err?.type || 'INTERNAL_ERROR'
|
|
301
|
+
const instance = ctx?.action?.name
|
|
302
|
+
? `${ctx?.action?.name}`
|
|
303
|
+
: `event.${ctx?.event?.name}`
|
|
304
|
+
const title = err?.message || ''
|
|
305
|
+
const errors = Array.isArray(err?.data)
|
|
306
|
+
? err.data.map(({ nodeID, action, ...rest }) => rest)
|
|
307
|
+
: err?.data || []
|
|
308
|
+
|
|
309
|
+
const stack = err?.stack || ""
|
|
310
|
+
const timestamp = new Date().toISOString()
|
|
311
|
+
|
|
312
|
+
if(err.code){
|
|
313
|
+
ctx.broker.logger.error({tag: "SBK", requestID, type, instance, errors, stack: ""})
|
|
314
|
+
}
|
|
315
|
+
else{
|
|
316
|
+
ctx.broker.logger.error({tag: "SBK", requestID, type, instance, errors, stack: extractFileLines(stack)})
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
throw new Errors.MoleculerError(
|
|
321
|
+
title,
|
|
322
|
+
status,
|
|
323
|
+
type,
|
|
324
|
+
{
|
|
325
|
+
type,
|
|
326
|
+
title,
|
|
327
|
+
status,
|
|
328
|
+
timestamp,
|
|
329
|
+
requestID,
|
|
330
|
+
errors,
|
|
331
|
+
}
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
else{
|
|
335
|
+
throw err // IMPORTANT: rethrow
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
export const httpErrorSchema = {
|
|
344
|
+
type: "object",
|
|
345
|
+
properties: {
|
|
346
|
+
type: {
|
|
347
|
+
type: "string",
|
|
348
|
+
description: "Application-specific error type",
|
|
349
|
+
examples: [
|
|
350
|
+
"NOT_FOUND",
|
|
351
|
+
"UNAUTHORIZED",
|
|
352
|
+
"VALIDATION_ERROR",
|
|
353
|
+
"INTERNAL_ERROR",
|
|
354
|
+
]
|
|
355
|
+
},
|
|
356
|
+
title: {
|
|
357
|
+
type: "string",
|
|
358
|
+
description: "Short, human-readable summary of the problem",
|
|
359
|
+
example: "test error"
|
|
360
|
+
},
|
|
361
|
+
status: {
|
|
362
|
+
type: "integer",
|
|
363
|
+
description: "HTTP status code",
|
|
364
|
+
examples: [400, 401, 422, 500]
|
|
365
|
+
},
|
|
366
|
+
detail: {
|
|
367
|
+
type: "string",
|
|
368
|
+
description: "Human-readable explanation of the problem",
|
|
369
|
+
example: "test error"
|
|
370
|
+
},
|
|
371
|
+
timestamp: {
|
|
372
|
+
type: "string",
|
|
373
|
+
description: "Timestamp when the error occurred",
|
|
374
|
+
example: "2026-04-25T18:55:45.035Z"
|
|
375
|
+
},
|
|
376
|
+
requestID: {
|
|
377
|
+
type: "string",
|
|
378
|
+
description: "Unique request identifier for tracing",
|
|
379
|
+
example: "110e025a-7362-4694-87da-63aada9cae53"
|
|
380
|
+
},
|
|
381
|
+
instance: {
|
|
382
|
+
type: "string",
|
|
383
|
+
description: "Request method and URI that caused the error",
|
|
384
|
+
example: "GET /api/test"
|
|
385
|
+
},
|
|
386
|
+
errors: {
|
|
387
|
+
type: "array",
|
|
388
|
+
default: [],
|
|
389
|
+
items: {
|
|
390
|
+
type: "object",
|
|
391
|
+
properties: {
|
|
392
|
+
path: {
|
|
393
|
+
type: "string",
|
|
394
|
+
example: "params.id"
|
|
395
|
+
},
|
|
396
|
+
message: {
|
|
397
|
+
type: "string",
|
|
398
|
+
example: "id should be integer"
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
required: ["path", "message"],
|
|
402
|
+
additionalProperties: false
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
required: [
|
|
407
|
+
"type",
|
|
408
|
+
"title",
|
|
409
|
+
"status",
|
|
410
|
+
"detail",
|
|
411
|
+
"timestamp",
|
|
412
|
+
"requestID",
|
|
413
|
+
"instance",
|
|
414
|
+
"errors"
|
|
415
|
+
],
|
|
416
|
+
additionalProperties: false
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Normalizes error objects into a consistent HTTP response structure.
|
|
421
|
+
* Ensures all errors include a standard set of fields such as status code,
|
|
422
|
+
* message, and optional metadata for client consumption.
|
|
423
|
+
*/
|
|
424
|
+
export const httpErrorFormatter = (req, res, err)=> {
|
|
425
|
+
const defaultDetails = {
|
|
426
|
+
400: "The request is malformed.",
|
|
427
|
+
401: "Authentication is required.",
|
|
428
|
+
403: "You do not have permission to perform this action.",
|
|
429
|
+
404: "The requested resource was not found.",
|
|
430
|
+
405: "The requested HTTP method is not allowed.",
|
|
431
|
+
409: "The request conflicts with the current state of the resource.",
|
|
432
|
+
415: "The request content type is not supported.",
|
|
433
|
+
422: "One or more parameters are invalid.",
|
|
434
|
+
429: "Too many requests have been made.",
|
|
435
|
+
500: "An unexpected server error occurred.",
|
|
436
|
+
502: "An invalid response was received from an upstream service.",
|
|
437
|
+
503: "The service is temporarily unavailable.",
|
|
438
|
+
504: "The request timed out."
|
|
439
|
+
}
|
|
440
|
+
const code = err?.data?.status || err?.code || 500
|
|
441
|
+
|
|
442
|
+
const errorBody = JSON.stringify({
|
|
443
|
+
type: err?.data?.type || err?.type || "about:blank",
|
|
444
|
+
title: err?.data?.title || err?.message,
|
|
445
|
+
status: code,
|
|
446
|
+
detail: err?.data?.detail || defaultDetails[code] || STATUS_CODES[code] || "Unknown error",
|
|
447
|
+
timestamp: err?.data?.timestamp || new Date().toISOString(),
|
|
448
|
+
requestID: err?.data?.requestID || req.$ctx?.requestID,
|
|
449
|
+
instance: err?.data?.instance || `${req.method} ${req.$ctx?.params?.req?.originalUrl}`,
|
|
450
|
+
errors: err?.data?.errors || []
|
|
451
|
+
})
|
|
452
|
+
res.setHeader("Content-Type", "application/json")
|
|
453
|
+
res.writeHead(code)
|
|
454
|
+
res.end(errorBody)
|
|
455
|
+
|
|
456
|
+
}
|