@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.
@@ -0,0 +1,121 @@
1
+ import { fileURLToPath, pathToFileURL } from "url"
2
+ import * as path from "path"
3
+ import * as fs from "fs"
4
+ import os from "os"
5
+ import { createRequire } from 'module'
6
+ import chokidar from "chokidar"
7
+
8
+
9
+ const __filename = fileURLToPath(import.meta.url)
10
+ const __dirname = path.dirname(__filename)
11
+ const require = createRequire(import.meta.url)
12
+
13
+
14
+ /**
15
+ * Project absolute path
16
+ */
17
+ export const ABSOLUTE_PATH = path.join(__filename.split('/samanbayaka/')[0], "samanbayaka")
18
+
19
+
20
+ /**
21
+ * Demo services directory
22
+ * @type {path}
23
+ */
24
+ export const DEMO_SERVICES_DIR = path.join(ABSOLUTE_PATH, 'services', 'demo')
25
+
26
+
27
+ /**
28
+ * Feature services absolute path
29
+ */
30
+ const SERVICES_DIR = process.cwd()
31
+ // /node_modules/.test(__dirname)
32
+ // ? __dirname.split('node_modules')[0]
33
+ // : __dirname.replace("/helper/utility","")
34
+
35
+ console.log("=================================", process.cwd())
36
+
37
+ /**
38
+ * Assets path
39
+ */
40
+ export const absoluteAssetPath = (dirName) => {
41
+ return path
42
+ .join(
43
+ ABSOLUTE_PATH,
44
+ dirName,
45
+ )
46
+ }
47
+
48
+
49
+ /**
50
+ * Read package.json file and return object
51
+ * @param {strig} filePath
52
+ * @return {object} package object
53
+ */
54
+ const readPkg = (filePath) => {
55
+ return JSON.parse(fs
56
+ .readFileSync(
57
+ path
58
+ .join(filePath, "package.json"),
59
+ "utf8"
60
+ )
61
+ )
62
+ }
63
+
64
+
65
+ /**
66
+ * Read package.json of services and convert to object
67
+ * @type {object}
68
+ */
69
+ const pkgService = readPkg(SERVICES_DIR)
70
+
71
+ /**
72
+ * Getting service name
73
+ */
74
+ export const serviceDtls = {
75
+ name: pkgService.name,
76
+ version: pkgService.version,
77
+ id: [pkgService.name, os.hostname(), process.pid].join("-"),
78
+ osPid: [os.hostname(), process.pid].join("-")
79
+ }
80
+
81
+
82
+ /**
83
+ * Read package.json of samanbayaka and convert to object
84
+ * @type {object}
85
+ */
86
+ const pkgMain = readPkg(ABSOLUTE_PATH)
87
+
88
+
89
+ /**
90
+ * Create and object of name and version
91
+ * @type {Object}
92
+ */
93
+ export const pkgInfo = {
94
+ name: pkgMain.name.split('-')[0]
95
+ .split(" ")
96
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
97
+ .join(" "),
98
+ fullName: pkgMain.name,
99
+ version: pkgMain.version
100
+ }
101
+
102
+
103
+ /**
104
+ * File chage watcher in the service directory
105
+ */
106
+ export const createFileWatcher = () => {
107
+ return chokidar.watch(SERVICES_DIR, {
108
+ ignoreInitial: true,
109
+ awaitWriteFinish: {
110
+ stabilityThreshold: 2000, // wait 2000ms after last change
111
+ pollInterval: 50
112
+ },
113
+ ignored: [
114
+ /(^|[\/\\])\../,
115
+ /(^|[\/\\])node_modules([\/\\]|$)/,
116
+ /~$/,
117
+ /\.swp$/,
118
+ /\.tmp$/
119
+ ]
120
+ })
121
+ }
@@ -0,0 +1,47 @@
1
+ import * as errHdl from "./error-handler.mjs"
2
+
3
+ export const validateGlobalConfigs = async(prefix) => {
4
+ try{
5
+ const nodeVer = errHdl.checkMinNoveVer()
6
+ const configHdl = await import("./config-handler.mjs")
7
+ const { serviceDtls, createFileWatcher } = await import("#hUti/file-handler.mjs")
8
+ const globalConfigs = await configHdl.getConfigAll(prefix)
9
+
10
+
11
+ const isServiceRemoved = (((await configHdl.getConfigs( `/config/sbk/members/${serviceDtls.name}` )) || "false") == "false")
12
+ if ( serviceDtls.name != "@samanbayaka/core" && isServiceRemoved ) {
13
+ throw new Error (`The service "${serviceDtls.name}" is disallowed by CONFIGADMIN.`)
14
+ }
15
+
16
+ return Object.freeze({
17
+ nodeVer,
18
+ configCred: configHdl.etcdCred,
19
+ maxL1TTL: errHdl.validateL1TTL(globalConfigs?.MAX_L1_TTL),
20
+ maxL2TTL: errHdl.validateL2TTL(globalConfigs?.MAX_L2_TTL),
21
+ logLevel: errHdl.validateLogLevel(globalConfigs?.LOG_LEVEL),
22
+ telemetry: errHdl.validateTelemetry(globalConfigs?.TELEMETRY),
23
+ configRevision: errHdl.validateConfRevision(globalConfigs?.REVISION),
24
+ serviceDtls,
25
+ createFileWatcher,
26
+ hostEntry: {
27
+ nats: await errHdl.validateHostEntry('nats'),
28
+ redis: await errHdl.validateHostEntry('redis'),
29
+ etcd: await errHdl.validateHostEntry('etcd'),
30
+ redpanda: await errHdl.validateHostEntry('redpanda', true),
31
+ openobserve: await errHdl.validateHostEntry('openobserve', true),
32
+ },
33
+ namespaceTxt: errHdl.validateNamespace(globalConfigs.APP_ENV),
34
+ })
35
+ }
36
+ catch(err){
37
+ // console.error("\x1b[31m%s\x1b[0m", ` ❖❖ SBKERR ❖❖ \n ${errHdl.extractFileLines(err.stack)}`)
38
+ console.error("\x1b[31m%s\x1b[0m", ` ❖❖ SBKERR ❖❖ ${err.message}`)
39
+ process.exit(1)
40
+ }
41
+ }
42
+
43
+
44
+ /**
45
+ * Validate the Samanbayaka global configurations
46
+ */
47
+ export const SBK_GLOBAL_CONFIGS = await validateGlobalConfigs("/config/sbk/global/envs/")
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Generate Moleculer params definitions from the OpenAPI schema to enable request parameter validation using AJV
3
+ * @param {object} schema
4
+ */
5
+ export const openApiToMoleculerParams = (schema) => {
6
+ if( schema?.openapi?.parameters || schema?.openapi?.requestBody ){
7
+
8
+ /**
9
+ * Moleculer params schema for request validation
10
+ * @type {Object}
11
+ */
12
+ schema.params = {
13
+ type: "object",
14
+ properties: {
15
+ params: {
16
+ type: "object",
17
+ properties: {
18
+ },
19
+ required: [],
20
+ additionalProperties: false,
21
+ },
22
+ query: {
23
+ type: "object",
24
+ properties: {
25
+
26
+ },
27
+ required: [],
28
+ },
29
+ body: {}
30
+ },
31
+ required:[],
32
+ additionalProperties: false,
33
+ }
34
+
35
+ /**
36
+ * Enforce the presence of a request body in the request based on OpenAPI schema definition
37
+ */
38
+ if( schema?.openapi?.requestBody?.required ){
39
+ schema.params.required.push("body")
40
+ }
41
+
42
+ /**
43
+ * Generate request body params from the OpenAPI schema definition.
44
+ */
45
+ if( schema?.openapi?.requestBody && schema?.openapi?.requestBody?.content ){
46
+ schema.params.properties.body.anyOf = Object.entries(schema?.openapi?.requestBody?.content)
47
+ .map(el=>el[1].schema)
48
+
49
+ }
50
+
51
+ /**
52
+ * Generate request path & query params from the OpenAPI schema definition.
53
+ */
54
+ if( schema?.openapi?.parameters ) {
55
+ schema.params.properties.params.properties = schema?.openapi?.parameters
56
+ .filter(el=> el.in == "path")
57
+ .reduce((obj, item) => {
58
+ obj[item.name] = item.schema
59
+ return obj
60
+ }, {})
61
+ schema.params.properties.params.required = Object.keys(schema.params.properties.params.properties)
62
+
63
+ if( schema.params.properties.params.required.length > 0 ){
64
+ schema.params.required.push("params")
65
+ }
66
+
67
+ schema.params.properties.query.properties = schema?.openapi?.parameters
68
+ .filter(el=> el.in == "query").reduce((obj, item) => {
69
+ obj[item.name] = item.schema
70
+ return obj
71
+ }, {})
72
+
73
+ if( Object.keys(schema.params.properties.query.properties).length > 0 ){
74
+ schema.params.required.push("query")
75
+ }
76
+
77
+ schema.params.properties.query.required = schema?.openapi?.parameters
78
+ .filter(el=> el.in == "query")
79
+ .filter(el => el.required)
80
+ .map(el => el.name )
81
+ }
82
+ }
83
+
84
+ }
@@ -0,0 +1,35 @@
1
+ import { createPublicKey, randomUUID } from "node:crypto"
2
+ import { v5 as uuidv5 } from "uuid"
3
+ import jwt from "jsonwebtoken"
4
+
5
+ import * as configHdl from '#hUti/config-handler.mjs'
6
+
7
+ const privateKey = await configHdl.getConfigs(`/config/sbk/sys/certs/SERVER_KEY`)
8
+ const cert = await configHdl.getConfigs(`/config/sbk/sys/certs/SERVER_CRT`)
9
+ const publicKey = createPublicKey(cert).export({
10
+ type: "spki",
11
+ format: "pem",
12
+ })
13
+
14
+ const MY_NAMESPACE = randomUUID()
15
+ const sub = uuidv5("samanbayaka", MY_NAMESPACE)
16
+ const typ = "Bearer"
17
+
18
+ export const signJWT = (data, expiresIn = "30s") => {
19
+ return jwt.sign(
20
+ {
21
+ sub,
22
+ typ,
23
+ data,
24
+ },
25
+ privateKey,
26
+ {
27
+ algorithm: 'RS256',
28
+ expiresIn
29
+ },
30
+ )
31
+ }
32
+
33
+ export const verifyJWT = (token) => {
34
+ return jwt.verify(token, publicKey)
35
+ }
@@ -0,0 +1,129 @@
1
+ import { NodeSDK } from "@opentelemetry/sdk-node"
2
+ import { resourceFromAttributes } from "@opentelemetry/resources"
3
+ import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"
4
+
5
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"
6
+ import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http"
7
+ import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics"
8
+ import { context, trace, metrics, SpanStatusCode } from "@opentelemetry/api"
9
+
10
+
11
+ const resource = resourceFromAttributes({
12
+ [SemanticResourceAttributes.SERVICE_NAME]: "moleculer-sbk"
13
+ })
14
+
15
+
16
+ // import { ConsoleSpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base"
17
+ // export const sdk = new NodeSDK({
18
+ // spanProcessors: [
19
+ // new SimpleSpanProcessor(new ConsoleSpanExporter())
20
+ // ]
21
+
22
+ // })
23
+
24
+
25
+ export const createOpenTelemetryExporters = (config) => {
26
+ const sdk = new NodeSDK({
27
+ resource,
28
+
29
+ traceExporter: new OTLPTraceExporter({
30
+ url: `http://${config.host}:${config.port}/api/default/v1/traces`,
31
+ headers: {
32
+ Authorization: config.token
33
+ }
34
+ }),
35
+
36
+ metricReaders: [new PeriodicExportingMetricReader({
37
+ exporter: new OTLPMetricExporter({
38
+ url: `http://${config.host}:${config.port}/api/default/v1/metrics`,
39
+ headers: {
40
+ Authorization: config.token
41
+ }
42
+ }),
43
+ exportIntervalMillis: 5000,
44
+ exportTimeoutMillis: 5000
45
+ })],
46
+
47
+ autoGeneratePropagators: true, // Recommended
48
+
49
+ })
50
+ sdk.start()
51
+ return sdk
52
+ }
53
+
54
+
55
+ const tracer = trace.getTracer("moleculer")
56
+
57
+ export const OpenTelemetryMW = (isEnabled) => {
58
+ const meter = metrics.getMeter("moleculer")
59
+
60
+ const requestCounter = meter.createCounter("moleculer_requests_total", {
61
+ description: "Total number of requests"
62
+ })
63
+ const errorCounter = meter.createCounter("moleculer_errors_total", {
64
+ description: "Total number of errors"
65
+ })
66
+ const durationHistogram = meter.createHistogram("moleculer_request_duration_ms", {
67
+ description: "Request duration in ms"
68
+ })
69
+
70
+ return {
71
+ name: "otel",
72
+
73
+ localAction(next, action) {
74
+ if ( !isEnabled ) return next
75
+ return async (ctx) => {
76
+ const startTime = Date.now()
77
+
78
+ const span = tracer.startSpan(`action:${action.name}`, {
79
+ attributes: {
80
+ "moleculer.service": action.service.name,
81
+ "moleculer.action": action.name,
82
+ "moleculer.nodeID": ctx.nodeID
83
+ }
84
+ })
85
+
86
+ return context.with(trace.setSpan(context.active(), span), async () => {
87
+ try {
88
+ const result = await next(ctx)
89
+
90
+ /**
91
+ * Metrics
92
+ */
93
+ requestCounter.add(1, {
94
+ action: action.name,
95
+ service: action.service.name
96
+ })
97
+
98
+ durationHistogram.record(Date.now() - startTime, {
99
+ action: action.name
100
+ })
101
+
102
+ span.setStatus({ code: SpanStatusCode.OK })
103
+ return result
104
+
105
+ } catch (err) {
106
+ /**
107
+ * Metrics
108
+ */
109
+ errorCounter.add(1, {
110
+ action: action.name,
111
+ service: action.service.name
112
+ })
113
+
114
+ span.recordException(err)
115
+ span.setStatus({
116
+ code: SpanStatusCode.ERROR,
117
+ message: err.message
118
+ })
119
+
120
+ throw err
121
+
122
+ } finally {
123
+ span.end()
124
+ }
125
+ })
126
+ }
127
+ }
128
+ }
129
+ }