@mhmdhammoud/meritt-utils 1.6.2 → 1.6.3

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/ReleaseNotes.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changes
2
2
 
3
+ ## Version 1.6.3
4
+
5
+ ### Elasticsearch Proxy Logging Fix
6
+
7
+ - Disabled Elasticsearch node sniffing by default. Sniffing is unsafe when the
8
+ configured endpoint is an HTTPS proxy because discovered nodes may advertise
9
+ internal addresses or a different protocol.
10
+ - Preserved explicit opt-in through `sniffOnConnectionFault: true` for direct
11
+ multi-node Elasticsearch deployments.
12
+ - Sanitized dots and whitespace from recursive context field names.
13
+ - Replaced empty context field names with `unnamed_field`.
14
+ - Added regression coverage for proxy-safe transport configuration and context
15
+ field sanitization.
16
+
3
17
  ## Version 1.5.3
4
18
 
5
19
  ### Logger Improvements - Critical Bug Fixes & Production Reliability
@@ -34,7 +48,9 @@
34
48
  - Handles `SIGTERM` and `SIGINT` signals
35
49
  - 5-second timeout prevents hanging forever
36
50
  - Proper async/await handling to ensure flush completes
37
- - **Auto-reconnection**: `sniffOnConnectionFault: true` enables automatic reconnection when Elasticsearch nodes fail
51
+ - **Node discovery**: `sniffOnConnectionFault` is available for direct
52
+ multi-node Elasticsearch deployments. It should remain disabled behind a
53
+ proxy because discovered node addresses may not be externally reachable.
38
54
  - **Retry logic**: Configurable retry attempts with timeout for failed requests
39
55
 
40
56
  #### Type Safety
@@ -15,6 +15,19 @@ describe('elastic transport resilience', () => {
15
15
  jest.useRealTimers();
16
16
  MockedClient.mockReset();
17
17
  });
18
+ test('disables node sniffing by default', () => {
19
+ MockedClient.mockImplementation(() => ({ bulk: jest.fn() }));
20
+ (0, elastic_transport_1.createElasticTransport)({ index: 'logs' });
21
+ expect(MockedClient).toHaveBeenCalledWith(expect.objectContaining({ sniffOnConnectionFault: false }));
22
+ });
23
+ test('allows node sniffing only when explicitly enabled', () => {
24
+ MockedClient.mockImplementation(() => ({ bulk: jest.fn() }));
25
+ (0, elastic_transport_1.createElasticTransport)({
26
+ index: 'logs',
27
+ sniffOnConnectionFault: true,
28
+ });
29
+ expect(MockedClient).toHaveBeenCalledWith(expect.objectContaining({ sniffOnConnectionFault: true }));
30
+ });
18
31
  test('reports bulk failures without emitting terminal stream errors', async () => {
19
32
  const bulk = jest
20
33
  .fn()
@@ -132,4 +132,25 @@ describe('route and format logs', () => {
132
132
  _id: expect.anything(),
133
133
  }));
134
134
  });
135
+ test('sanitize empty, dotted, and whitespace context field names', () => {
136
+ //@ts-ignore
137
+ jest.spyOn(pino_1.pino, 'destination').mockReturnValue(PINO_DESTINATION);
138
+ //@ts-ignore
139
+ pino_1.pino.mockReturnValue(PINO);
140
+ const logger = new logger_1.default(LOGGER_NAME);
141
+ logger.info(LOG_EVENT, {
142
+ '': 'empty',
143
+ 'field.with.dots': 'dotted',
144
+ ' field name ': 'spaced',
145
+ nested: { ' ': 'nested-empty' },
146
+ });
147
+ expect(PINO.info).toHaveBeenCalledWith(expect.objectContaining({
148
+ context: {
149
+ unnamed_field: 'empty',
150
+ fieldwithdots: 'dotted',
151
+ fieldname: 'spaced',
152
+ nested: { unnamed_field: 'nested-empty' },
153
+ },
154
+ }));
155
+ });
135
156
  });
@@ -204,6 +204,7 @@ function createBulkSender(opts, client, splitter) {
204
204
  };
205
205
  }
206
206
  const createElasticTransport = (opts = {}) => {
207
+ var _a;
207
208
  const splitter = split(function (line) {
208
209
  let value;
209
210
  try {
@@ -240,7 +241,8 @@ const createElasticTransport = (opts = {}) => {
240
241
  tls: { rejectUnauthorized: opts.rejectUnauthorized, ...opts.tls },
241
242
  maxRetries: opts.maxRetries,
242
243
  requestTimeout: opts.requestTimeout,
243
- sniffOnConnectionFault: opts.sniffOnConnectionFault,
244
+ // Keep the configured proxy endpoint unless callers explicitly opt in.
245
+ sniffOnConnectionFault: (_a = opts.sniffOnConnectionFault) !== null && _a !== void 0 ? _a : false,
244
246
  };
245
247
  if (opts.caFingerprint) {
246
248
  clientOpts.caFingerprint = opts.caFingerprint;
@@ -59,7 +59,8 @@ const RESERVED_ES_FIELD_ALIASES = {
59
59
  };
60
60
  /** Convert arbitrary field names into ES-safe flattened keys */
61
61
  const toSafeElasticFieldName = (key) => {
62
- const normalized = toSnakeCase(key);
62
+ const cleaned = key.replace(/[.\s]+/g, '');
63
+ const normalized = toSnakeCase(cleaned) || 'unnamed_field';
63
64
  const mapped = RESERVED_ES_FIELD_ALIASES[normalized];
64
65
  if (mapped) {
65
66
  return mapped;
@@ -290,8 +291,8 @@ function getLogger(elasticConfig) {
290
291
  // Retry configuration for connection resilience
291
292
  maxRetries: maxRetries,
292
293
  requestTimeout: requestTimeout,
293
- // Automatically reconnect on connection faults
294
- sniffOnConnectionFault: true,
294
+ // Sniffing can bypass proxies and discover unreachable internal nodes.
295
+ sniffOnConnectionFault: false,
295
296
  };
296
297
  if (elasticConfig) {
297
298
  Object.assign(esConfig, elasticConfig);
@@ -58,7 +58,8 @@ export interface ElasticConfig {
58
58
  requestTimeout?: number;
59
59
  /**
60
60
  * Whether to sniff for additional Elasticsearch nodes on connection fault.
61
- * Enables automatic reconnection when a node fails.
61
+ * Defaults to false because sniffing can bypass a proxy and discover
62
+ * internal node addresses that are unreachable or use a different protocol.
62
63
  */
63
64
  sniffOnConnectionFault?: boolean;
64
65
  }
package/package.json CHANGED
@@ -1,13 +1,19 @@
1
1
  {
2
2
  "name": "@mhmdhammoud/meritt-utils",
3
- "version": "1.6.2",
3
+ "version": "1.6.3",
4
4
  "description": "",
5
5
  "main": "./dist/index.js",
6
6
  "private": false,
7
7
  "typings": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "imagesloaded.d.ts",
11
+ "README.md",
12
+ "ReleaseNotes.md"
13
+ ],
8
14
  "repository": {
9
15
  "type": "git",
10
- "url": "git@github.com:Mhmdhammoud/meritt-utils.git"
16
+ "url": "git+ssh://git@github.com/Mhmdhammoud/meritt-utils.git"
11
17
  },
12
18
  "publishConfig": {
13
19
  "access": "public"
@@ -18,7 +24,7 @@
18
24
  "watch": "tsc --watch",
19
25
  "build": "tsc",
20
26
  "test:types": "tsc --noemit",
21
- "prepare": "husky install",
27
+ "prepare": "husky",
22
28
  "lint": "eslint src --fix",
23
29
  "prepublish": "npm run build",
24
30
  "test": "jest",
@@ -1,84 +0,0 @@
1
- name: NPM Publish on Release
2
-
3
- on:
4
- push:
5
- branches: [master]
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
-
32
- build:
33
- needs: test
34
- runs-on: ubuntu-latest
35
- steps:
36
- - name: Checkout code
37
- uses: actions/checkout@v4
38
-
39
- - name: Setup Node.js
40
- uses: actions/setup-node@v4
41
- with:
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/
56
-
57
- publish-npm:
58
- needs: [test, build]
59
- runs-on: ubuntu-latest
60
- if: success()
61
- steps:
62
- - name: Checkout code
63
- uses: actions/checkout@v4
64
-
65
- - name: Setup Node.js
66
- uses: actions/setup-node@v4
67
- with:
68
- node-version: '20'
69
- registry-url: https://registry.npmjs.org/
70
- cache: 'npm'
71
-
72
- - name: Install dependencies
73
- run: npm ci
74
-
75
- - name: Download build artifacts
76
- uses: actions/download-artifact@v4
77
- with:
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,83 +0,0 @@
1
- name: CI Pipeline
2
-
3
- on:
4
- push:
5
- branches: [dev]
6
- pull_request:
7
- branches: [dev, master]
8
-
9
- jobs:
10
- lint-and-test:
11
- runs-on: ubuntu-latest
12
- steps:
13
- - name: Checkout code
14
- uses: actions/checkout@v4
15
-
16
- - name: Setup Node.js
17
- uses: actions/setup-node@v4
18
- with:
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:
67
- runs-on: ubuntu-latest
68
- steps:
69
- - name: Checkout code
70
- uses: actions/checkout@v4
71
-
72
- - name: Setup Node.js
73
- uses: actions/setup-node@v4
74
- with:
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/.husky/pre-commit DELETED
@@ -1,62 +0,0 @@
1
- #!/usr/bin/env sh
2
- . "$(dirname -- "$0")/_/husky.sh"
3
-
4
- red='\033[0;31m'
5
- green='\033[0;32m'
6
- yellow='\033[0;33m'
7
- blue='\033[0;34m'
8
- magenta='\033[0;35m'
9
- cyan='\033[0;36m'
10
- clear='\033[0m'
11
- echo "
12
-
13
- __ __ ______ _____ _____ _______ _______ _____ ________ __
14
- | \/ | ____| __ \|_ _|__ __|__ __| | __ \| ____\ \ / /
15
- | \ / | |__ | |__) | | | | | | | | | | | |__ \ \ / /
16
- | |\/| | __| | _ / | | | | | | | | | | __| \ \/ /
17
- | | | | |____| | \ \ _| |_ | | | | | |__| | |____ \ /
18
- |_| |_|______|_| \_\_____| |_| |_| |_____/|______| \/
19
-
20
-
21
- "
22
- echo -e "Let's hope you are not ${red}DUMB${clear}!"
23
-
24
- echo -ne '\n'
25
-
26
- echo -e "${yellow}Or Are You ??${clear}!"
27
- echo -ne '\n'
28
- echo -e "${yellow}Running pre-commit hook...${clear}"
29
- echo -ne '\n'
30
- echo -ne '\n'
31
-
32
- echo -ne '##################### (25%)\r'
33
- sleep 0.3
34
- echo -ne '############################## (35%)\r'
35
- sleep 0.3
36
- echo -ne '###################################### (45%)\r'
37
- sleep 0.3
38
- echo -ne '############################################################ (77%)\r'
39
- sleep 0.3
40
- echo -ne '################################################################## (100%)\r'
41
-
42
- echo -ne '\n'
43
- echo -ne '\n'
44
- echo -ne '\n'
45
-
46
- echo -e "${yellow}Running types...${clear}"
47
- echo -ne '\n'
48
- npm run test:types
49
- echo -ne '\n'
50
-
51
- echo -e "${green}types passed...${clear} ✅"
52
- echo -ne '\n'
53
- echo -e "${yellow}Running linting...${clear}"
54
-
55
- npm run lint
56
- echo -e "${green}linting passed...${clear} ✅"
57
-
58
- echo -ne '\n'
59
-
60
- echo -e "${yellow}Seems like you are a${clear} ${green}CLEVER DEVELOPER${clear} 👌"
61
- echo -ne '\n'
62
- echo -e "${green}GOOD TO GO${clear} 🚀🚀🚀"
package/.prettierrc DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "semi": false,
3
- "singleQuote": true,
4
- "trailingComma": "es5",
5
- "tabWidth": 2,
6
- "useTabs": true,
7
- "printWidth": 80
8
- }
package/.prettierrc.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "trailingComma": "es5",
3
- "tabWidth": 2,
4
- "semi": false,
5
- "singleQuote": true,
6
- "useTabs": true,
7
- "bracketSpacing": false
8
- }
package/ISSUES.md DELETED
File without changes
package/eslint.config.mjs DELETED
@@ -1,64 +0,0 @@
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/example.env DELETED
@@ -1,6 +0,0 @@
1
- ELASTICSEARCH_USERNAME=
2
- ELASTICSEARCH_PASSWORD=
3
- ELASTICSEARCH_NODE=
4
- SERVER_NICKNAME=
5
- NODE_ENV=
6
- LOG_LEVEL=info
package/jest.config.js DELETED
@@ -1,13 +0,0 @@
1
- /** @type {import('ts-jest').JestConfigWithTsJest} */
2
- module.exports = {
3
- preset: 'ts-jest',
4
- testEnvironment: 'node',
5
- coverageDirectory: 'coverage',
6
- coverageProvider: 'v8',
7
- collectCoverageFrom: ['src/**/*.ts', '!test/**/*.ts'],
8
- transform: {
9
- '^.+\\.(ts)$': 'ts-jest',
10
- },
11
- transformIgnorePatterns: [],
12
- testMatch: ['<rootDir>/src/**/__tests__/**/*.test.ts'],
13
- }
@@ -1,77 +0,0 @@
1
- import { Colorful } from '../lib'
2
-
3
- describe('Colorful Class', () => {
4
- describe('rgbToHex method', () => {
5
- it('should convert RGB to HEX', () => {
6
- const rgbColor = 'rgb(255, 255, 255)'
7
- const hexColor = Colorful.rgbToHex(rgbColor)
8
- expect(hexColor).toBe('#ffffff')
9
- })
10
-
11
- it('should convert RGBA to HEX', () => {
12
- const rgbaColor = 'rgba(255, 0, 0, 0.5)'
13
- const hexColor = Colorful.rgbToHex(rgbaColor)
14
- expect(hexColor).toBe('#ff0000')
15
- })
16
-
17
- it('should throw an error for invalid color format', () => {
18
- const invalidColor = 'invalidColor'
19
- expect(() => Colorful.rgbToHex(invalidColor)).toThrowError(
20
- `Invalid color format '${invalidColor}'. Please provide a valid RGB or RGBA color.`
21
- )
22
- })
23
- })
24
-
25
- describe('hexToRgb method', () => {
26
- it('should convert HEX to RGB', () => {
27
- const hexColor = '#ffffff'
28
- const rgbColor = Colorful.hexToRgb(hexColor)
29
- expect(rgbColor).toBe('rgb(255, 255, 255)')
30
- })
31
- })
32
-
33
- describe('isLightColor method', () => {
34
- it('should return true for a light color (HEX format)', () => {
35
- const lightHexColor = '#f0f0f0'
36
- const isLight = Colorful.isLightColor(lightHexColor)
37
- expect(isLight).toBe(true)
38
- })
39
-
40
- it('should return false for a dark color (HEX format)', () => {
41
- const darkHexColor = '#333333'
42
- const isLight = Colorful.isLightColor(darkHexColor)
43
- expect(isLight).toBe(false)
44
- })
45
-
46
- it('should return true for a light color (RGBA format)', () => {
47
- const lightRgbaColor = 'rgba(255, 255, 255, 0.5)'
48
- const isLight = Colorful.isLightColor(lightRgbaColor)
49
- expect(isLight).toBe(true)
50
- })
51
-
52
- it('should return false for a dark color (RGBA format)', () => {
53
- const darkRgbaColor = 'rgba(0, 0, 0, 0.8)'
54
- const isLight = Colorful.isLightColor(darkRgbaColor)
55
- expect(isLight).toBe(false)
56
- })
57
-
58
- it('should return true for a light color (RGB format)', () => {
59
- const lightRgbColor = 'rgb(200, 220, 255)'
60
- const isLight = Colorful.isLightColor(lightRgbColor)
61
- expect(isLight).toBe(true)
62
- })
63
-
64
- it('should return false for a dark color (RGB format)', () => {
65
- const darkRgbColor = 'rgb(10, 20, 30)'
66
- const isLight = Colorful.isLightColor(darkRgbColor)
67
- expect(isLight).toBe(false)
68
- })
69
-
70
- it('should throw an error for an invalid color format', () => {
71
- const invalidColor = 'invalid-color'
72
- expect(() => Colorful.isLightColor(invalidColor)).toThrowError(
73
- "Invalid color format 'invalid-color'. Please provide a valid HEX, RGB, or RGBA color."
74
- )
75
- })
76
- })
77
- })
@@ -1,94 +0,0 @@
1
- import { createElasticTransport } from '../lib/elastic-transport'
2
- import { Client } from '@elastic/elasticsearch'
3
- import { EventEmitter } from 'events'
4
-
5
- jest.mock('@elastic/elasticsearch', () => ({
6
- Client: jest.fn(),
7
- }))
8
-
9
- const MockedClient = Client as jest.Mock
10
-
11
- const writeLog = (
12
- transport: NodeJS.WritableStream,
13
- doc: Record<string, unknown>
14
- ) => transport.write(`${JSON.stringify(doc)}\n`)
15
-
16
- const waitForEvent = <T = unknown>(
17
- emitter: EventEmitter,
18
- eventName: string
19
- ): Promise<T> =>
20
- new Promise((resolve) => {
21
- emitter.once(eventName, (value) => resolve(value as T))
22
- })
23
-
24
- describe('elastic transport resilience', () => {
25
- beforeEach(() => {
26
- jest.useRealTimers()
27
- MockedClient.mockReset()
28
- })
29
-
30
- test('reports bulk failures without emitting terminal stream errors', async () => {
31
- const bulk = jest
32
- .fn()
33
- .mockRejectedValueOnce(new Error('ES unavailable'))
34
- .mockResolvedValueOnce({ errors: false, items: [] })
35
- MockedClient.mockImplementation(() => ({ bulk }))
36
-
37
- const transport = createElasticTransport({
38
- index: 'logs',
39
- flushBytes: 1,
40
- flushInterval: 10000,
41
- requestTimeout: 10,
42
- })
43
- const streamError = jest.fn()
44
- transport.on('error', streamError)
45
-
46
- const failed = waitForEvent<Error>(transport, 'bulkError')
47
- writeLog(transport, { message: 'first' })
48
- await failed
49
-
50
- const inserted = waitForEvent<{ successful: number; failed: number }>(
51
- transport,
52
- 'insert'
53
- )
54
- writeLog(transport, { message: 'second' })
55
-
56
- await expect(inserted).resolves.toEqual({ successful: 1, failed: 0 })
57
- expect(streamError).not.toHaveBeenCalled()
58
- expect(bulk).toHaveBeenCalledTimes(2)
59
- })
60
-
61
- test('watchdog releases a stuck bulk request so later logs can flush', async () => {
62
- jest.useFakeTimers()
63
-
64
- const bulk = jest
65
- .fn()
66
- .mockReturnValueOnce(new Promise(() => undefined))
67
- .mockResolvedValueOnce({ errors: false, items: [] })
68
- MockedClient.mockImplementation(() => ({ bulk }))
69
-
70
- const transport = createElasticTransport({
71
- index: 'logs',
72
- flushBytes: 1,
73
- flushInterval: 10000,
74
- requestTimeout: 1,
75
- })
76
-
77
- const timedOut = waitForEvent<Error>(transport, 'bulkError')
78
- writeLog(transport, { message: 'stuck' })
79
-
80
- await jest.advanceTimersByTimeAsync(5001)
81
- await expect(timedOut).resolves.toMatchObject({
82
- message: 'Elasticsearch bulk request timed out after 5001ms',
83
- })
84
-
85
- const inserted = waitForEvent<{ successful: number; failed: number }>(
86
- transport,
87
- 'insert'
88
- )
89
- writeLog(transport, { message: 'recovered' })
90
-
91
- await expect(inserted).resolves.toEqual({ successful: 1, failed: 0 })
92
- expect(bulk).toHaveBeenCalledTimes(2)
93
- })
94
- })