@mhmdhammoud/meritt-utils 1.5.2 → 1.5.4

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.
@@ -3,41 +3,82 @@ name: NPM Publish on Release
3
3
  on:
4
4
  push:
5
5
  branches: [master]
6
+
6
7
  jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - name: Checkout code
12
+ uses: actions/checkout@v4
13
+
14
+ - name: Setup Node.js
15
+ uses: actions/setup-node@v4
16
+ with:
17
+ node-version: '20'
18
+ cache: 'npm'
19
+
20
+ - name: Install dependencies
21
+ run: npm ci
22
+
23
+ - name: Run type check
24
+ run: npm run test:types
25
+
26
+ - name: Run linting
27
+ run: npm run lint
28
+
29
+ - name: Run tests
30
+ run: npm test
31
+
7
32
  build:
33
+ needs: test
8
34
  runs-on: ubuntu-latest
9
35
  steps:
10
- - uses: actions/checkout@v3
11
- - uses: actions/setup-node@v3
36
+ - name: Checkout code
37
+ uses: actions/checkout@v4
38
+
39
+ - name: Setup Node.js
40
+ uses: actions/setup-node@v4
12
41
  with:
13
- node-version: 16
14
- - run: npm ci
42
+ node-version: '20'
43
+ cache: 'npm'
44
+
45
+ - name: Install dependencies
46
+ run: npm ci
47
+
48
+ - name: Build project
49
+ run: npm run build
50
+
51
+ - name: Upload build artifacts
52
+ uses: actions/upload-artifact@v4
53
+ with:
54
+ name: dist
55
+ path: dist/
15
56
 
16
57
  publish-npm:
17
- needs: build
58
+ needs: [test, build]
18
59
  runs-on: ubuntu-latest
60
+ if: success()
19
61
  steps:
20
- - uses: actions/checkout@v3
21
- - uses: actions/setup-node@v3
62
+ - name: Checkout code
63
+ uses: actions/checkout@v4
64
+
65
+ - name: Setup Node.js
66
+ uses: actions/setup-node@v4
22
67
  with:
23
- node-version: 16
68
+ node-version: '20'
24
69
  registry-url: https://registry.npmjs.org/
25
- - run: npm ci
26
- - run: npm publish
27
- env:
28
- NODE_AUTH_TOKEN: ${{secrets.npm_token}}
29
- notify:
30
- needs: [publish-npm]
31
- runs-on: ubuntu-latest
32
- steps:
33
- - name: Notify by Email
34
- uses: dawidd6/action-send-mail@v2
70
+ cache: 'npm'
71
+
72
+ - name: Install dependencies
73
+ run: npm ci
74
+
75
+ - name: Download build artifacts
76
+ uses: actions/download-artifact@v4
35
77
  with:
36
- server_address: ${{ secrets.EMAIL_HOST }}
37
- server_port: 465
38
- username: ${{ secrets.EMAIL_USERNAME }}
39
- password: ${{ secrets.EMAIL_PASSWORD }}
40
- subject: ${{ github.event.head_commit.message }} ${{ github.job }} job of ${{ github.repository }} has ${{ job.status }}
41
- body: ${{ github.job }} job in worflow ${{ github.workflow }} of ${{ github.repository }} has ${{ job.status }}
42
- to: mohammad.hammoud.lb@hotmail.com,steef12009@gmail.com
43
- from: Github Action
78
+ name: dist
79
+ path: dist/
80
+
81
+ - name: Publish to NPM
82
+ run: npm publish
83
+ env:
84
+ NODE_AUTH_TOKEN: ${{ secrets.npm_token }}
@@ -1,29 +1,83 @@
1
- name: Changes
1
+ name: CI Pipeline
2
+
2
3
  on:
3
4
  push:
4
5
  branches: [dev]
6
+ pull_request:
7
+ branches: [dev, master]
8
+
5
9
  jobs:
6
- type-check:
10
+ lint-and-test:
7
11
  runs-on: ubuntu-latest
8
12
  steps:
9
- - uses: actions/checkout@v3
10
- - uses: actions/setup-node@v3
13
+ - name: Checkout code
14
+ uses: actions/checkout@v4
15
+
16
+ - name: Setup Node.js
17
+ uses: actions/setup-node@v4
11
18
  with:
12
- node-version: 16
13
- - run: npm ci
14
- - run: npm run test:types
15
- notify:
16
- needs: [type-check]
19
+ node-version: '20'
20
+ cache: 'npm'
21
+
22
+ - name: Install dependencies
23
+ run: npm ci
24
+
25
+ - name: Run type check
26
+ run: npm run test:types
27
+
28
+ - name: Run linting
29
+ run: npm run lint
30
+
31
+ - name: Run tests with coverage
32
+ run: npm run test:coverage
33
+
34
+ - name: Upload coverage reports
35
+ uses: actions/upload-artifact@v4
36
+ if: always()
37
+ with:
38
+ name: coverage-report
39
+ path: coverage/
40
+
41
+ build:
42
+ needs: lint-and-test
43
+ runs-on: ubuntu-latest
44
+ steps:
45
+ - name: Checkout code
46
+ uses: actions/checkout@v4
47
+
48
+ - name: Setup Node.js
49
+ uses: actions/setup-node@v4
50
+ with:
51
+ node-version: '20'
52
+ cache: 'npm'
53
+
54
+ - name: Install dependencies
55
+ run: npm ci
56
+
57
+ - name: Build project
58
+ run: npm run build
59
+
60
+ - name: Upload build artifacts
61
+ uses: actions/upload-artifact@v4
62
+ with:
63
+ name: dist
64
+ path: dist/
65
+
66
+ security-audit:
17
67
  runs-on: ubuntu-latest
18
68
  steps:
19
- - name: Notify by Email
20
- uses: dawidd6/action-send-mail@v2
69
+ - name: Checkout code
70
+ uses: actions/checkout@v4
71
+
72
+ - name: Setup Node.js
73
+ uses: actions/setup-node@v4
21
74
  with:
22
- server_address: ${{ secrets.EMAIL_HOST }}
23
- server_port: 465
24
- username: ${{ secrets.EMAIL_USERNAME }}
25
- password: ${{ secrets.EMAIL_PASSWORD }}
26
- subject: ${{ github.event.head_commit.message }} ${{ github.job }} job of ${{ github.repository }} has ${{ job.status }}
27
- body: ${{ github.job }} job in worflow ${{ github.workflow }} of ${{ github.repository }} has ${{ job.status }}
28
- to: mohammad.hammoud.lb@hotmail.com,steef12009@gmail.com
29
- from: Github Action
75
+ node-version: '20'
76
+ cache: 'npm'
77
+
78
+ - name: Install dependencies
79
+ run: npm ci
80
+
81
+ - name: Run security audit
82
+ run: npm audit --audit-level=moderate
83
+
package/.prettierrc ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "semi": false,
3
+ "singleQuote": true,
4
+ "trailingComma": "es5",
5
+ "tabWidth": 2,
6
+ "useTabs": true,
7
+ "printWidth": 80
8
+ }
@@ -15,13 +15,23 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
15
15
  }) : function(o, v) {
16
16
  o["default"] = v;
17
17
  });
18
- var __importStar = (this && this.__importStar) || function (mod) {
19
- if (mod && mod.__esModule) return mod;
20
- var result = {};
21
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
- __setModuleDefault(result, mod);
23
- return result;
24
- };
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
25
35
  Object.defineProperty(exports, "__esModule", { value: true });
26
36
  const logger_1 = __importStar(require("../lib/logger"));
27
37
  const pino_1 = require("pino");
@@ -63,7 +63,7 @@ declare class Crypto {
63
63
  * crypto.generateKeys()
64
64
  * ```
65
65
  * */
66
- generateKeys: () => Record<'publicKey' | 'privateKey', number>;
66
+ generateKeys: () => Record<"publicKey" | "privateKey", number>;
67
67
  /**
68
68
  *
69
69
  * @param publicKey - The public key number
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Elasticsearch transport for Pino with connection lifecycle resilience.
3
+ *
4
+ * Based on pino-elasticsearch with a fix for GitHub issue #140:
5
+ * When maxRetries are exceeded and Elasticsearch nodes are DEAD, the bulk helper
6
+ * destroys the splitter stream, causing logs to stop permanently until restart.
7
+ *
8
+ * This implementation overrides splitter.destroy to BOTH resurrect the connection
9
+ * pool AND reinitialize the bulk handler, so logging continues after ES recovers.
10
+ *
11
+ * @see https://github.com/pinojs/pino-elasticsearch/issues/140
12
+ * @see https://github.com/pinojs/pino-elasticsearch/issues/72
13
+ */
14
+ import type { ClientOptions } from '@elastic/elasticsearch';
15
+ export interface ElasticTransportOptions extends Pick<ClientOptions, 'node' | 'auth' | 'cloud' | 'caFingerprint' | 'Connection' | 'ConnectionPool' | 'maxRetries' | 'requestTimeout'> {
16
+ sniffOnConnectionFault?: boolean;
17
+ index?: string | ((logTime: string) => string);
18
+ flushBytes?: number;
19
+ 'flush-bytes'?: number;
20
+ flushInterval?: number;
21
+ 'flush-interval'?: number;
22
+ esVersion?: number;
23
+ 'es-version'?: number;
24
+ rejectUnauthorized?: boolean;
25
+ tls?: ClientOptions['tls'];
26
+ }
27
+ export declare const createElasticTransport: (opts?: ElasticTransportOptions) => NodeJS.ReadWriteStream;
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ /**
3
+ * Elasticsearch transport for Pino with connection lifecycle resilience.
4
+ *
5
+ * Based on pino-elasticsearch with a fix for GitHub issue #140:
6
+ * When maxRetries are exceeded and Elasticsearch nodes are DEAD, the bulk helper
7
+ * destroys the splitter stream, causing logs to stop permanently until restart.
8
+ *
9
+ * This implementation overrides splitter.destroy to BOTH resurrect the connection
10
+ * pool AND reinitialize the bulk handler, so logging continues after ES recovers.
11
+ *
12
+ * @see https://github.com/pinojs/pino-elasticsearch/issues/140
13
+ * @see https://github.com/pinojs/pino-elasticsearch/issues/72
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.createElasticTransport = void 0;
17
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
18
+ const split = require('split2');
19
+ const elasticsearch_1 = require("@elastic/elasticsearch");
20
+ function setDateTimeString(value) {
21
+ if (value !== null && typeof value === 'object' && 'time' in value) {
22
+ const t = value.time;
23
+ if ((typeof t === 'string' && t.length > 0) ||
24
+ (typeof t === 'number' && t >= 0)) {
25
+ return new Date(t).toISOString();
26
+ }
27
+ }
28
+ return new Date().toISOString();
29
+ }
30
+ function getIndexName(index, time) {
31
+ if (typeof index === 'function') {
32
+ return index(time);
33
+ }
34
+ return index.replace('%{DATE}', time.substring(0, 10));
35
+ }
36
+ function initializeBulkHandler(opts, client, splitter) {
37
+ var _a, _b, _c, _d, _e, _f, _g;
38
+ const esVersion = Number((_b = (_a = opts.esVersion) !== null && _a !== void 0 ? _a : opts['es-version']) !== null && _b !== void 0 ? _b : 7);
39
+ const index = (_c = opts.index) !== null && _c !== void 0 ? _c : 'pino';
40
+ const buildIndexName = typeof index === 'function' ? index : null;
41
+ const opType = esVersion >= 7 ? undefined : undefined;
42
+ // CRITICAL FIX (issue #140): When bulk helper destroys stream after retries exhausted,
43
+ // we must BOTH resurrect the pool AND reinitialize the bulk handler so logging continues.
44
+ // connectionPool.resurrect exists at runtime (elastic-transport) but may not be in types
45
+ const pool = client.connectionPool;
46
+ const splitterWithDestroy = splitter;
47
+ splitterWithDestroy.destroy = function () {
48
+ if (typeof pool.resurrect === 'function') {
49
+ pool.resurrect({ name: 'elasticsearch-js' });
50
+ }
51
+ // Reinitialize bulk handler - without this, logging stops permanently until restart
52
+ initializeBulkHandler(opts, client, splitter);
53
+ };
54
+ const indexName = (time = new Date().toISOString()) => buildIndexName ? buildIndexName(time) : getIndexName(index, time);
55
+ const bulkInsert = client.helpers.bulk({
56
+ datasource: splitter,
57
+ flushBytes: (_e = (_d = opts.flushBytes) !== null && _d !== void 0 ? _d : opts['flush-bytes']) !== null && _e !== void 0 ? _e : 1000,
58
+ flushInterval: (_g = (_f = opts.flushInterval) !== null && _f !== void 0 ? _f : opts['flush-interval']) !== null && _g !== void 0 ? _g : 3000,
59
+ refreshOnCompletion: indexName(),
60
+ onDocument(doc) {
61
+ var _a, _b;
62
+ const d = doc;
63
+ const date = (_b = (_a = d.time) !== null && _a !== void 0 ? _a : d['@timestamp']) !== null && _b !== void 0 ? _b : new Date().toISOString();
64
+ if (opType === 'create') {
65
+ d['@timestamp'] = date;
66
+ }
67
+ return {
68
+ index: {
69
+ _index: indexName(date),
70
+ op_type: opType,
71
+ },
72
+ };
73
+ },
74
+ onDrop(doc) {
75
+ const error = new Error('Dropped document');
76
+ error.document = doc;
77
+ splitter.emit('insertError', error);
78
+ },
79
+ });
80
+ bulkInsert.then((stats) => splitter.emit('insert', stats), (err) => splitter.emit('error', err));
81
+ }
82
+ const createElasticTransport = (opts = {}) => {
83
+ const splitter = split(function (line) {
84
+ let value;
85
+ try {
86
+ value = JSON.parse(line);
87
+ }
88
+ catch (error) {
89
+ this.emit('unknown', line, error);
90
+ return;
91
+ }
92
+ if (typeof value === 'boolean') {
93
+ this.emit('unknown', line, 'Boolean value ignored');
94
+ return;
95
+ }
96
+ if (value === null) {
97
+ this.emit('unknown', line, 'Null value ignored');
98
+ return;
99
+ }
100
+ if (typeof value !== 'object') {
101
+ value = { data: value, time: setDateTimeString(value) };
102
+ }
103
+ else {
104
+ const obj = value;
105
+ if (obj['@timestamp'] === undefined) {
106
+ ;
107
+ obj.time = setDateTimeString(obj);
108
+ }
109
+ }
110
+ return value;
111
+ }, { autoDestroy: true });
112
+ const clientOpts = {
113
+ node: opts.node,
114
+ auth: opts.auth,
115
+ cloud: opts.cloud,
116
+ tls: { rejectUnauthorized: opts.rejectUnauthorized, ...opts.tls },
117
+ maxRetries: opts.maxRetries,
118
+ requestTimeout: opts.requestTimeout,
119
+ sniffOnConnectionFault: opts.sniffOnConnectionFault,
120
+ };
121
+ if (opts.caFingerprint) {
122
+ clientOpts.caFingerprint = opts.caFingerprint;
123
+ }
124
+ if (opts.Connection) {
125
+ clientOpts.Connection = opts.Connection;
126
+ }
127
+ if (opts.ConnectionPool) {
128
+ clientOpts.ConnectionPool = opts.ConnectionPool;
129
+ }
130
+ const client = new elasticsearch_1.Client(clientOpts);
131
+ client.diagnostic.on('resurrect', () => {
132
+ initializeBulkHandler(opts, client, splitter);
133
+ });
134
+ initializeBulkHandler(opts, client, splitter);
135
+ return splitter;
136
+ };
137
+ exports.createElasticTransport = createElasticTransport;
@@ -15,21 +15,28 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
15
15
  }) : function(o, v) {
16
16
  o["default"] = v;
17
17
  });
18
- var __importStar = (this && this.__importStar) || function (mod) {
19
- if (mod && mod.__esModule) return mod;
20
- var result = {};
21
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
- __setModuleDefault(result, mod);
23
- return result;
24
- };
25
- var __importDefault = (this && this.__importDefault) || function (mod) {
26
- return (mod && mod.__esModule) ? mod : { "default": mod };
27
- };
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
28
35
  Object.defineProperty(exports, "__esModule", { value: true });
29
- exports.isValidLogLevel = void 0;
36
+ exports.isValidLogLevel = isValidLogLevel;
30
37
  const pino_1 = require("pino");
31
38
  const dotenv = __importStar(require("dotenv"));
32
- const pino_elasticsearch_1 = __importDefault(require("pino-elasticsearch"));
39
+ const elastic_transport_1 = require("./elastic-transport");
33
40
  dotenv.config();
34
41
  /**
35
42
  * Pino logger backend - singleton
@@ -48,8 +55,13 @@ let shutdownHandlersRegistered = false;
48
55
  * @throws Error if required environment variables are missing.
49
56
  */
50
57
  function validateElasticsearchEnv() {
51
- const required = ['ELASTICSEARCH_NODE', 'ELASTICSEARCH_USERNAME', 'ELASTICSEARCH_PASSWORD', 'SERVER_NICKNAME'];
52
- const missing = required.filter(key => !process.env[key]);
58
+ const required = [
59
+ 'ELASTICSEARCH_NODE',
60
+ 'ELASTICSEARCH_USERNAME',
61
+ 'ELASTICSEARCH_PASSWORD',
62
+ 'SERVER_NICKNAME',
63
+ ];
64
+ const missing = required.filter((key) => !process.env[key]);
53
65
  if (missing.length > 0) {
54
66
  throw new Error(`Missing required Elasticsearch environment variables: ${missing.join(', ')}`);
55
67
  }
@@ -175,9 +187,8 @@ function getLogger(elasticConfig) {
175
187
  if (elasticConfig) {
176
188
  Object.assign(esConfig, elasticConfig);
177
189
  }
178
- // Create transport and store reference for cleanup
179
- // Cast to PinoElasticOptions since our ElasticConfig includes ClientOptions properties
180
- esTransport = (0, pino_elasticsearch_1.default)(esConfig);
190
+ // Create transport with connection lifecycle fix (pino-elasticsearch #140)
191
+ esTransport = (0, elastic_transport_1.createElasticTransport)(esConfig);
181
192
  // Handle Elasticsearch connection errors
182
193
  esTransport.on('error', (err) => {
183
194
  console.error('[Logger] Elasticsearch transport error:', err.message);
@@ -220,7 +231,6 @@ function isValidLogLevel(level) {
220
231
  }
221
232
  return true;
222
233
  }
223
- exports.isValidLogLevel = isValidLogLevel;
224
234
  /**
225
235
  * Logger Wrapper.
226
236
  * Wraps a Pino logger instance and provides logging methods.
package/dist/lib/pdf.d.ts CHANGED
@@ -9,7 +9,7 @@ declare class Pdf {
9
9
  * // => 'Hello-World'
10
10
  * ```
11
11
  * */
12
- getBase64Images: (url: string) => string;
12
+ getBase64Images: (_url: string) => string;
13
13
  }
14
14
  declare const pdf: Pdf;
15
15
  export default pdf;
package/dist/lib/pdf.js CHANGED
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ // import { AxiosInstance } from '../utilities'
3
4
  /*
4
5
  Author : Mustafa Halabi https://github.com/mustafahalabi
5
6
  Date : 2023-06-24
@@ -18,7 +19,7 @@ class Pdf {
18
19
  * // => 'Hello-World'
19
20
  * ```
20
21
  * */
21
- this.getBase64Images = (url) => {
22
+ this.getBase64Images = (_url) => {
22
23
  return '';
23
24
  };
24
25
  }
@@ -0,0 +1,64 @@
1
+ // @ts-check
2
+ import eslint from '@eslint/js'
3
+ import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
4
+ import globals from 'globals'
5
+ import tseslint from 'typescript-eslint'
6
+
7
+ export default tseslint.config(
8
+ {
9
+ ignores: [
10
+ 'eslint.config.mjs',
11
+ 'dist/**',
12
+ 'node_modules/**',
13
+ 'coverage/**',
14
+ '*.d.ts',
15
+ ],
16
+ },
17
+ eslint.configs.recommended,
18
+ ...tseslint.configs.recommendedTypeChecked,
19
+ eslintPluginPrettierRecommended,
20
+ {
21
+ languageOptions: {
22
+ globals: {
23
+ ...globals.node,
24
+ },
25
+ sourceType: 'module',
26
+ parserOptions: {
27
+ projectService: true,
28
+ tsconfigRootDir: import.meta.dirname,
29
+ },
30
+ },
31
+ },
32
+ {
33
+ rules: {
34
+ '@typescript-eslint/no-explicit-any': 'off',
35
+ '@typescript-eslint/no-floating-promises': 'warn',
36
+ '@typescript-eslint/no-unsafe-argument': 'off',
37
+ '@typescript-eslint/no-unsafe-assignment': 'off',
38
+ '@typescript-eslint/no-unsafe-return': 'off',
39
+ '@typescript-eslint/no-unsafe-member-access': 'off',
40
+ '@typescript-eslint/no-unsafe-call': 'off',
41
+ '@typescript-eslint/ban-ts-comment': 'off',
42
+ '@typescript-eslint/require-await': 'off',
43
+ '@typescript-eslint/await-thenable': 'off',
44
+ '@typescript-eslint/no-unsafe-enum-comparison': 'off',
45
+ '@typescript-eslint/no-base-to-string': 'off',
46
+ '@typescript-eslint/no-redundant-type-constituents': 'off',
47
+ '@typescript-eslint/no-unused-vars': [
48
+ 'error',
49
+ {
50
+ argsIgnorePattern: '^_',
51
+ varsIgnorePattern: '^_',
52
+ },
53
+ ],
54
+ 'no-console': 'error',
55
+ 'prettier/prettier': ['error', { semi: false }],
56
+ },
57
+ },
58
+ {
59
+ files: ['src/lib/logger.ts'],
60
+ rules: {
61
+ 'no-console': 'off',
62
+ },
63
+ }
64
+ )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhmdhammoud/meritt-utils",
3
- "version": "1.5.2",
3
+ "version": "1.5.4",
4
4
  "description": "",
5
5
  "main": "./dist/index.js",
6
6
  "private": false,
@@ -25,25 +25,31 @@
25
25
  "test:coverage": "jest --coverage"
26
26
  },
27
27
  "devDependencies": {
28
- "@types/jest": "^27.5.2",
29
- "@typescript-eslint/eslint-plugin": "^5.17.0",
30
- "@typescript-eslint/parser": "^5.17.0",
31
- "eslint": "^8.12.0",
32
- "eslint-plugin-tsdoc": "^0.2.14",
33
- "husky": "^8.0.0",
28
+ "@eslint/js": "^9.31.0",
29
+ "@types/jest": "^29.5.12",
30
+ "eslint": "^9.31.0",
31
+ "eslint-config-prettier": "^10.1.8",
32
+ "eslint-plugin-prettier": "^5.5.3",
33
+ "eslint-plugin-tsdoc": "^0.5.2",
34
+ "globals": "^16.0.0",
35
+ "husky": "^9.1.6",
34
36
  "jest": "^29.7.0",
35
37
  "pino-pretty": "^10.3.1",
38
+ "prettier": "^3.3.3",
36
39
  "ts-jest": "^29.1.1",
37
- "ts-node-dev": "^1.1.8",
38
- "typescript": "^4.6.3"
40
+ "ts-node-dev": "^2.0.0",
41
+ "typescript": "^5.3.3",
42
+ "typescript-eslint": "^8.20.0"
39
43
  },
40
44
  "author": "Mhmdhammoud",
41
45
  "license": "ISC",
42
46
  "dependencies": {
47
+ "@elastic/elasticsearch": "^8.17.0",
43
48
  "axios": "^1.4.0",
44
49
  "dotenv": "^16.4.1",
45
50
  "imagesloaded": "^5.0.0",
46
51
  "pino": "^8.19.0",
47
- "pino-elasticsearch": "^8.0.0"
52
+ "pino-elasticsearch": "^8.0.0",
53
+ "split2": "^4.2.0"
48
54
  }
49
55
  }
@@ -1,4 +1,4 @@
1
- import {Colorful} from '../lib'
1
+ import { Colorful } from '../lib'
2
2
 
3
3
  describe('Colorful Class', () => {
4
4
  describe('rgbToHex method', () => {
@@ -1,4 +1,4 @@
1
- import {Formatter} from '../lib'
1
+ import { Formatter } from '../lib'
2
2
 
3
3
  describe('Formatter', () => {
4
4
  describe('toUpperFirst method', () => {
@@ -1,5 +1,5 @@
1
- import Logger, {isValidLogLevel} from '../lib/logger'
2
- import {pino} from 'pino'
1
+ import Logger, { isValidLogLevel } from '../lib/logger'
2
+ import { pino } from 'pino'
3
3
 
4
4
  jest.mock('pino')
5
5
 
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Elasticsearch transport for Pino with connection lifecycle resilience.
3
+ *
4
+ * Based on pino-elasticsearch with a fix for GitHub issue #140:
5
+ * When maxRetries are exceeded and Elasticsearch nodes are DEAD, the bulk helper
6
+ * destroys the splitter stream, causing logs to stop permanently until restart.
7
+ *
8
+ * This implementation overrides splitter.destroy to BOTH resurrect the connection
9
+ * pool AND reinitialize the bulk handler, so logging continues after ES recovers.
10
+ *
11
+ * @see https://github.com/pinojs/pino-elasticsearch/issues/140
12
+ * @see https://github.com/pinojs/pino-elasticsearch/issues/72
13
+ */
14
+
15
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
16
+ const split = require('split2') as (
17
+ fn: (line: string) => unknown,
18
+ opts?: { autoDestroy?: boolean }
19
+ ) => NodeJS.ReadWriteStream
20
+ import { Readable } from 'stream'
21
+ import { Client } from '@elastic/elasticsearch'
22
+ import type { ClientOptions } from '@elastic/elasticsearch'
23
+
24
+ export interface ElasticTransportOptions extends Pick<
25
+ ClientOptions,
26
+ | 'node'
27
+ | 'auth'
28
+ | 'cloud'
29
+ | 'caFingerprint'
30
+ | 'Connection'
31
+ | 'ConnectionPool'
32
+ | 'maxRetries'
33
+ | 'requestTimeout'
34
+ > {
35
+ sniffOnConnectionFault?: boolean
36
+ index?: string | ((logTime: string) => string)
37
+ flushBytes?: number
38
+ 'flush-bytes'?: number
39
+ flushInterval?: number
40
+ 'flush-interval'?: number
41
+ esVersion?: number
42
+ 'es-version'?: number
43
+ rejectUnauthorized?: boolean
44
+ tls?: ClientOptions['tls']
45
+ }
46
+
47
+ interface LogDocument {
48
+ time?: string
49
+ '@timestamp'?: string
50
+ [k: string]: unknown
51
+ }
52
+
53
+ function setDateTimeString(value: unknown): string {
54
+ if (value !== null && typeof value === 'object' && 'time' in value) {
55
+ const t = (value as { time: unknown }).time
56
+ if (
57
+ (typeof t === 'string' && t.length > 0) ||
58
+ (typeof t === 'number' && t >= 0)
59
+ ) {
60
+ return new Date(t).toISOString()
61
+ }
62
+ }
63
+ return new Date().toISOString()
64
+ }
65
+
66
+ function getIndexName(
67
+ index: string | ((logTime: string) => string),
68
+ time: string
69
+ ): string {
70
+ if (typeof index === 'function') {
71
+ return index(time)
72
+ }
73
+ return index.replace('%{DATE}', time.substring(0, 10))
74
+ }
75
+
76
+ function initializeBulkHandler(
77
+ opts: ElasticTransportOptions,
78
+ client: Client,
79
+ splitter: NodeJS.ReadWriteStream
80
+ ): void {
81
+ const esVersion = Number(opts.esVersion ?? opts['es-version'] ?? 7)
82
+ const index = opts.index ?? 'pino'
83
+ const buildIndexName = typeof index === 'function' ? index : null
84
+ const opType = esVersion >= 7 ? undefined : undefined
85
+
86
+ // CRITICAL FIX (issue #140): When bulk helper destroys stream after retries exhausted,
87
+ // we must BOTH resurrect the pool AND reinitialize the bulk handler so logging continues.
88
+ // connectionPool.resurrect exists at runtime (elastic-transport) but may not be in types
89
+ const pool = client.connectionPool as {
90
+ resurrect?: (opts: { name: string }) => void
91
+ }
92
+ const splitterWithDestroy = splitter as NodeJS.ReadWriteStream & {
93
+ destroy: (err?: Error) => void
94
+ }
95
+ splitterWithDestroy.destroy = function () {
96
+ if (typeof pool.resurrect === 'function') {
97
+ pool.resurrect({ name: 'elasticsearch-js' })
98
+ }
99
+ // Reinitialize bulk handler - without this, logging stops permanently until restart
100
+ initializeBulkHandler(opts, client, splitter)
101
+ }
102
+
103
+ const indexName = (time = new Date().toISOString()) =>
104
+ buildIndexName ? buildIndexName(time) : getIndexName(index as string, time)
105
+
106
+ const bulkInsert = client.helpers.bulk({
107
+ datasource: splitter as unknown as Readable,
108
+ flushBytes: opts.flushBytes ?? opts['flush-bytes'] ?? 1000,
109
+ flushInterval: opts.flushInterval ?? opts['flush-interval'] ?? 3000,
110
+ refreshOnCompletion: indexName(),
111
+ onDocument(doc: unknown) {
112
+ const d = doc as LogDocument
113
+ const date = d.time ?? d['@timestamp'] ?? new Date().toISOString()
114
+ if (opType === 'create') {
115
+ d['@timestamp'] = date
116
+ }
117
+ return {
118
+ index: {
119
+ _index: indexName(date),
120
+ op_type: opType,
121
+ },
122
+ }
123
+ },
124
+ onDrop(doc: unknown) {
125
+ const error = new Error('Dropped document') as Error & {
126
+ document: unknown
127
+ }
128
+ error.document = doc
129
+ splitter.emit('insertError', error)
130
+ },
131
+ })
132
+
133
+ bulkInsert.then(
134
+ (stats) => splitter.emit('insert', stats),
135
+ (err) => splitter.emit('error', err)
136
+ )
137
+ }
138
+
139
+ export const createElasticTransport = (
140
+ opts: ElasticTransportOptions = {}
141
+ ): NodeJS.ReadWriteStream => {
142
+ const splitter = split(
143
+ function (this: NodeJS.ReadWriteStream, line: string) {
144
+ let value: unknown
145
+
146
+ try {
147
+ value = JSON.parse(line) as unknown
148
+ } catch (error) {
149
+ this.emit('unknown', line, error)
150
+ return
151
+ }
152
+
153
+ if (typeof value === 'boolean') {
154
+ this.emit('unknown', line, 'Boolean value ignored')
155
+ return
156
+ }
157
+ if (value === null) {
158
+ this.emit('unknown', line, 'Null value ignored')
159
+ return
160
+ }
161
+ if (typeof value !== 'object') {
162
+ value = { data: value, time: setDateTimeString(value) }
163
+ } else {
164
+ const obj = value as Record<string, unknown>
165
+ if (obj['@timestamp'] === undefined) {
166
+ ;(obj as LogDocument).time = setDateTimeString(obj)
167
+ }
168
+ }
169
+ return value
170
+ },
171
+ { autoDestroy: true }
172
+ )
173
+
174
+ const clientOpts: ClientOptions = {
175
+ node: opts.node,
176
+ auth: opts.auth,
177
+ cloud: opts.cloud,
178
+ tls: { rejectUnauthorized: opts.rejectUnauthorized, ...opts.tls },
179
+ maxRetries: opts.maxRetries,
180
+ requestTimeout: opts.requestTimeout,
181
+ sniffOnConnectionFault: opts.sniffOnConnectionFault,
182
+ }
183
+
184
+ if (opts.caFingerprint) {
185
+ clientOpts.caFingerprint = opts.caFingerprint
186
+ }
187
+ if (opts.Connection) {
188
+ clientOpts.Connection = opts.Connection
189
+ }
190
+ if (opts.ConnectionPool) {
191
+ clientOpts.ConnectionPool = opts.ConnectionPool
192
+ }
193
+
194
+ const client = new Client(clientOpts)
195
+
196
+ client.diagnostic.on('resurrect', () => {
197
+ initializeBulkHandler(opts, client, splitter)
198
+ })
199
+
200
+ initializeBulkHandler(opts, client, splitter)
201
+
202
+ return splitter
203
+ }
@@ -28,7 +28,7 @@ class ImageFull {
28
28
  return new Promise((resolve) => {
29
29
  imagesLoaded(
30
30
  document.querySelectorAll(selector),
31
- {background: true},
31
+ { background: true },
32
32
  (event) => {
33
33
  resolve(event)
34
34
  }
package/src/lib/index.ts CHANGED
@@ -1,6 +1,6 @@
1
- export {default as Crypto} from './cypto'
2
- export {default as Formatter} from './formatter'
3
- export {default as Pdf} from './formatter'
4
- export {default as Colorful} from './colorful'
5
- export {default as ImageFull} from './imagefull'
6
- export {default as Logger} from './logger'
1
+ export { default as Crypto } from './cypto'
2
+ export { default as Formatter } from './formatter'
3
+ export { default as Pdf } from './formatter'
4
+ export { default as Colorful } from './colorful'
5
+ export { default as ImageFull } from './imagefull'
6
+ export { default as Logger } from './logger'
package/src/lib/logger.ts CHANGED
@@ -1,7 +1,7 @@
1
- import {Logger as PinoLogger, pino, stdTimeFunctions} from 'pino'
1
+ import { Logger as PinoLogger, pino, stdTimeFunctions } from 'pino'
2
2
  import * as dotenv from 'dotenv'
3
- import pinoElastic, {Options as PinoElasticOptions} from 'pino-elasticsearch'
4
- import {LOG_LEVEL, LogEvent, ElasticConfig} from '../types'
3
+ import { createElasticTransport } from './elastic-transport'
4
+ import { LOG_LEVEL, LogEvent, ElasticConfig } from '../types'
5
5
 
6
6
  dotenv.config()
7
7
 
@@ -13,7 +13,7 @@ let pinoLogger: PinoLogger
13
13
  /**
14
14
  * Elasticsearch transport instance - kept for cleanup
15
15
  */
16
- let esTransport: ReturnType<typeof pinoElastic> | null = null
16
+ let esTransport: NodeJS.ReadWriteStream | null = null
17
17
 
18
18
  /**
19
19
  * Flag to track if shutdown handlers are registered
@@ -25,8 +25,13 @@ let shutdownHandlersRegistered = false
25
25
  * @throws Error if required environment variables are missing.
26
26
  */
27
27
  function validateElasticsearchEnv(): void {
28
- const required = ['ELASTICSEARCH_NODE', 'ELASTICSEARCH_USERNAME', 'ELASTICSEARCH_PASSWORD', 'SERVER_NICKNAME']
29
- const missing = required.filter(key => !process.env[key])
28
+ const required = [
29
+ 'ELASTICSEARCH_NODE',
30
+ 'ELASTICSEARCH_USERNAME',
31
+ 'ELASTICSEARCH_PASSWORD',
32
+ 'SERVER_NICKNAME',
33
+ ]
34
+ const missing = required.filter((key) => !process.env[key])
30
35
 
31
36
  if (missing.length > 0) {
32
37
  throw new Error(
@@ -43,7 +48,11 @@ function validateElasticsearchEnv(): void {
43
48
  * @returns The parsed integer or the default value.
44
49
  * @throws Error if the value is not a valid number.
45
50
  */
46
- function parseIntEnv(envValue: string | undefined, defaultValue: number, varName: string): number {
51
+ function parseIntEnv(
52
+ envValue: string | undefined,
53
+ defaultValue: number,
54
+ varName: string
55
+ ): number {
47
56
  if (!envValue) {
48
57
  return defaultValue
49
58
  }
@@ -177,8 +186,8 @@ function getLogger(elasticConfig?: ElasticConfig): PinoLogger {
177
186
  index: process.env.SERVER_NICKNAME,
178
187
  node: process.env.ELASTICSEARCH_NODE,
179
188
  auth: {
180
- username: process.env.ELASTICSEARCH_USERNAME as string,
181
- password: process.env.ELASTICSEARCH_PASSWORD as string,
189
+ username: process.env.ELASTICSEARCH_USERNAME,
190
+ password: process.env.ELASTICSEARCH_PASSWORD,
182
191
  },
183
192
  // Configurable flush settings
184
193
  flushInterval: flushIntervalMs,
@@ -193,18 +202,19 @@ function getLogger(elasticConfig?: ElasticConfig): PinoLogger {
193
202
  Object.assign(esConfig, elasticConfig)
194
203
  }
195
204
 
196
- // Create transport and store reference for cleanup
197
- // Cast to PinoElasticOptions since our ElasticConfig includes ClientOptions properties
198
- esTransport = pinoElastic(esConfig as PinoElasticOptions)
205
+ // Create transport with connection lifecycle fix (pino-elasticsearch #140)
206
+ esTransport = createElasticTransport(esConfig)
199
207
 
200
208
  // Handle Elasticsearch connection errors
201
- esTransport.on('error', (err) => {
209
+ esTransport.on('error', (err: Error) => {
202
210
  console.error('[Logger] Elasticsearch transport error:', err.message)
203
- console.error('[Logger] Logs may not be reaching Kibana. Check Elasticsearch connection.')
211
+ console.error(
212
+ '[Logger] Logs may not be reaching Kibana. Check Elasticsearch connection.'
213
+ )
204
214
  })
205
215
 
206
216
  // Handle insert errors (document indexing failures)
207
- esTransport.on('insertError', (err) => {
217
+ esTransport.on('insertError', (err: Error) => {
208
218
  console.error('[Logger] Elasticsearch insert error:', err.message)
209
219
  console.error('[Logger] Some logs failed to index to Elasticsearch.')
210
220
  })
package/src/lib/pdf.ts CHANGED
@@ -1,4 +1,4 @@
1
- import {AxiosInstance} from '../utilities'
1
+ // import { AxiosInstance } from '../utilities'
2
2
  /*
3
3
  Author : Mustafa Halabi https://github.com/mustafahalabi
4
4
  Date : 2023-06-24
@@ -16,7 +16,7 @@ class Pdf {
16
16
  * // => 'Hello-World'
17
17
  * ```
18
18
  * */
19
- getBase64Images = (url: string): string => {
19
+ getBase64Images = (_url: string): string => {
20
20
  return ''
21
21
  }
22
22
  }
@@ -1 +1 @@
1
- export {default as AxiosInstance} from './axios'
1
+ export { default as AxiosInstance } from './axios'
package/.eslintignore DELETED
@@ -1,2 +0,0 @@
1
- *.d.ts
2
- dist
package/.eslintrc.js DELETED
@@ -1,28 +0,0 @@
1
- module.exports = {
2
- plugins: ['@typescript-eslint/eslint-plugin', 'eslint-plugin-tsdoc'],
3
- extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
4
- parser: '@typescript-eslint/parser',
5
- parserOptions: {
6
- project: './tsconfig.json',
7
- tsconfigRootDir: __dirname,
8
- ecmaVersion: 2018,
9
- sourceType: 'module',
10
- },
11
- rules: {
12
- 'tsdoc/syntax': 'warn',
13
- '@typescript-eslint/ban-ts-comment': 'off',
14
- '@typescript-eslint/ban-types': 'off',
15
- '@typescript-eslint/no-namespace': 'off',
16
- '@typescript-eslint/no-unused-vars': [
17
- 'warn',
18
- {
19
- vars: 'all',
20
- args: 'after-used',
21
- ignoreRestSiblings: true,
22
- },
23
- ],
24
- '@typescript-eslint/no-explicit-any': 'off',
25
- 'no-async-promise-executor': 'off',
26
- },
27
- root: true,
28
- }