From 844fd723e7676f48a5eab97d6a3409515521782d Mon Sep 17 00:00:00 2001 From: Sam Curry Date: Wed, 6 May 2026 11:25:50 +0800 Subject: [PATCH] fix: omitNilFormat preserves info on objects with numeric `length` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch omitNilFormat from es-toolkit/compat's omitBy to a hand-rolled loop. es-toolkit/compat's omitBy treats any object with a numeric `length` (e.g. a pg error spread via splat) as array-like — it returns {} and drops winston's LEVEL/MESSAGE symbols, breaking downstream formats like colorize. Bump to 2.0.1. Refs toss/es-toolkit#1706. Co-Authored-By: Claude Opus 4.7 (1M context) --- package-lock.json | 4 ++-- package.json | 2 +- src/omit-nil-format.spec.ts | 37 +++++++++++++++++++++++++++++++++++++ src/omit-nil-format.ts | 16 ++++++++++++++-- 4 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 src/omit-nil-format.spec.ts diff --git a/package-lock.json b/package-lock.json index f896898..fafc551 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@makerx/node-winston", - "version": "2.0.0", + "version": "2.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@makerx/node-winston", - "version": "2.0.0", + "version": "2.0.1", "license": "MIT", "dependencies": { "@colors/colors": "^1.6.0", diff --git a/package.json b/package.json index c271e17..01d3ead 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@makerx/node-winston", - "version": "2.0.0", + "version": "2.0.1", "private": false, "description": "A set of winston formats, console transport and logger creation functions", "author": "MakerX", diff --git a/src/omit-nil-format.spec.ts b/src/omit-nil-format.spec.ts new file mode 100644 index 0000000..c56c57e --- /dev/null +++ b/src/omit-nil-format.spec.ts @@ -0,0 +1,37 @@ +import { TransformableInfo } from 'logform' +import { LEVEL, MESSAGE } from 'triple-beam' +import { describe, expect, it } from 'vitest' +import { omitNilFormat } from './omit-nil-format' + +const runOmitNil = (info: Record) => { + const transform = omitNilFormat() + const input = { [LEVEL]: 'info', [MESSAGE]: 'msg', level: 'info', message: 'msg', ...info } as TransformableInfo + return transform.transform(input, {}) as Record +} + +describe('omitNilFormat', () => { + it('removes null and undefined values', () => { + const result = runOmitNil({ a: 1, b: null, c: undefined, d: 'x' }) + + expect(result).toMatchObject({ a: 1, d: 'x' }) + expect(result).not.toHaveProperty('b') + expect(result).not.toHaveProperty('c') + }) + + it('keeps falsy-but-not-nil values', () => { + const result = runOmitNil({ zero: 0, empty: '', no: false }) + + expect(result).toMatchObject({ zero: 0, empty: '', no: false }) + }) + + it('preserves the LEVEL and MESSAGE symbols (regression: pg error length splat)', () => { + // pg errors carry a numeric `length` property; older es-toolkit/compat + // omitBy treated this object as array-like and stripped triple-beam symbols, + // breaking downstream colorize. + const result = runOmitNil({ length: 4, name: 'error', code: '23505' }) + + expect(result[LEVEL]).toBe('info') + expect(result[MESSAGE]).toBe('msg') + expect(result).toMatchObject({ length: 4, name: 'error', code: '23505' }) + }) +}) diff --git a/src/omit-nil-format.ts b/src/omit-nil-format.ts index a9a2ddc..f2322dc 100644 --- a/src/omit-nil-format.ts +++ b/src/omit-nil-format.ts @@ -1,5 +1,17 @@ -import { isNil, omitBy } from 'es-toolkit/compat' import { TransformableInfo } from 'logform' import { format } from 'winston' -export const omitNilFormat = format((info) => omitBy(info, isNil) as TransformableInfo) +// Hand-rolled rather than using es-toolkit/compat's omitBy: that treats any +// object with a numeric `length` property (e.g. a pg error spread via splat) +// as array-like, returns {}, and drops winston's LEVEL/MESSAGE symbols — +// breaking downstream formats like colorize. See toss/es-toolkit#1706. +export const omitNilFormat = format((info) => { + const out: Record = {} + for (const key of Object.keys(info)) { + if (info[key] != null) out[key] = info[key] + } + for (const sym of Object.getOwnPropertySymbols(info)) { + if (info[sym] != null) out[sym] = info[sym] + } + return out as TransformableInfo +})