Skip to content

Commit b56ea1e

Browse files
committed
refactor
1 parent fdfefd7 commit b56ea1e

File tree

4 files changed

+120
-33
lines changed

4 files changed

+120
-33
lines changed

spec/MongoStorageAdapter.spec.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,9 +1031,9 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
10311031
await adapter.handleShutdown();
10321032
});
10331033

1034-
it('should handle circular references with fallback warning', async () => {
1034+
it('should handle circular references gracefully', async () => {
10351035
const logger = require('../lib/logger').logger;
1036-
const warnSpy = spyOn(logger, 'warn');
1036+
const infoSpy = spyOn(logger, 'info');
10371037

10381038
const logClientEvents = [
10391039
{
@@ -1055,9 +1055,9 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
10551055

10561056
adapter.client.emit('circularEvent', mockEvent);
10571057

1058-
// Should fallback to warning when JSON.stringify fails
1059-
expect(warnSpy).toHaveBeenCalledWith(
1060-
jasmine.stringMatching(/MongoDB client event circularEvent logged with error:/)
1058+
// Should handle circular reference with [Circular] marker
1059+
expect(infoSpy).toHaveBeenCalledWith(
1060+
jasmine.stringMatching(/MongoDB client event circularEvent:.*\[Circular\]/)
10611061
);
10621062

10631063
await adapter.handleShutdown();

spec/Utils.spec.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,69 @@ describe('Utils', () => {
5757
});
5858
});
5959
});
60+
61+
describe('getCircularReplacer', () => {
62+
it('should handle Map instances', () => {
63+
const obj = {
64+
name: 'test',
65+
mapData: new Map([
66+
['key1', 'value1'],
67+
['key2', 'value2']
68+
])
69+
};
70+
const result = JSON.stringify(obj, Utils.getCircularReplacer());
71+
expect(result).toBe('{"name":"test","mapData":{"key1":"value1","key2":"value2"}}');
72+
});
73+
74+
it('should handle Set instances', () => {
75+
const obj = {
76+
name: 'test',
77+
setData: new Set([1, 2, 3])
78+
};
79+
const result = JSON.stringify(obj, Utils.getCircularReplacer());
80+
expect(result).toBe('{"name":"test","setData":[1,2,3]}');
81+
});
82+
83+
it('should handle circular references', () => {
84+
const obj = { name: 'test', value: 123 };
85+
obj.self = obj;
86+
const result = JSON.stringify(obj, Utils.getCircularReplacer());
87+
expect(result).toBe('{"name":"test","value":123,"self":"[Circular]"}');
88+
});
89+
90+
it('should handle nested circular references', () => {
91+
const obj = {
92+
name: 'parent',
93+
child: {
94+
name: 'child'
95+
}
96+
};
97+
obj.child.parent = obj;
98+
const result = JSON.stringify(obj, Utils.getCircularReplacer());
99+
expect(result).toBe('{"name":"parent","child":{"name":"child","parent":"[Circular]"}}');
100+
});
101+
102+
it('should handle mixed Map, Set, and circular references', () => {
103+
const obj = {
104+
mapData: new Map([['key', 'value']]),
105+
setData: new Set([1, 2]),
106+
regular: 'data'
107+
};
108+
obj.circular = obj;
109+
const result = JSON.stringify(obj, Utils.getCircularReplacer());
110+
expect(result).toBe('{"mapData":{"key":"value"},"setData":[1,2],"regular":"data","circular":"[Circular]"}');
111+
});
112+
113+
it('should handle normal objects without modification', () => {
114+
const obj = {
115+
name: 'test',
116+
number: 42,
117+
nested: {
118+
key: 'value'
119+
}
120+
};
121+
const result = JSON.stringify(obj, Utils.getCircularReplacer());
122+
expect(result).toBe('{"name":"test","number":42,"nested":{"key":"value"}}');
123+
});
124+
});
60125
});

src/Adapters/Storage/Mongo/MongoStorageAdapter.js

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import Parse from 'parse/node';
1818
import _ from 'lodash';
1919
import defaults from '../../../defaults';
2020
import logger from '../../../logger';
21+
import Utils from '../../../Utils';
2122

2223
// @flow-disable-next
2324
const mongodb = require('mongodb');
@@ -211,35 +212,22 @@ export class MongoStorageAdapter implements StorageAdapter {
211212
if (this._logClientEvents && Array.isArray(this._logClientEvents)) {
212213
this._logClientEvents.forEach(eventConfig => {
213214
client.on(eventConfig.name, event => {
214-
try {
215-
let logData = {};
216-
if (!eventConfig.keys || eventConfig.keys.length === 0) {
217-
logData = event;
218-
} else {
219-
eventConfig.keys.forEach(keyPath => {
220-
logData[keyPath] = _.get(event, keyPath);
221-
});
222-
}
223-
224-
// Validate log level exists, fallback to 'info'
225-
const logLevel = typeof logger[eventConfig.logLevel] === 'function' ? eventConfig.logLevel : 'info';
226-
227-
// Safe JSON serialization with Map/Set support
228-
const logMessage = `MongoDB client event ${eventConfig.name}: ${JSON.stringify(logData, (key, value) => {
229-
if (value instanceof Map) {
230-
return Object.fromEntries(value);
231-
}
232-
if (value instanceof Set) {
233-
return Array.from(value);
234-
}
235-
return value;
236-
})}`;
237-
238-
logger[logLevel](logMessage);
239-
} catch (error) {
240-
// Fallback if serialization completely fails
241-
logger.warn(`MongoDB client event ${eventConfig.name} logged with error: ${error.message}`);
215+
let logData = {};
216+
if (!eventConfig.keys || eventConfig.keys.length === 0) {
217+
logData = event;
218+
} else {
219+
eventConfig.keys.forEach(keyPath => {
220+
logData[keyPath] = _.get(event, keyPath);
221+
});
242222
}
223+
224+
// Validate log level exists, fallback to 'info'
225+
const logLevel = typeof logger[eventConfig.logLevel] === 'function' ? eventConfig.logLevel : 'info';
226+
227+
// Safe JSON serialization with Map/Set and circular reference support
228+
const logMessage = `MongoDB client event ${eventConfig.name}: ${JSON.stringify(logData, Utils.getCircularReplacer())}`;
229+
230+
logger[logLevel](logMessage);
243231
});
244232
});
245233
}

src/Utils.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,40 @@ class Utils {
410410
'%' + char.charCodeAt(0).toString(16).toUpperCase()
411411
);
412412
}
413+
414+
/**
415+
* Creates a JSON replacer function that handles Map, Set, and circular references.
416+
* This replacer can be used with JSON.stringify to safely serialize complex objects.
417+
*
418+
* @returns {Function} A replacer function for JSON.stringify that:
419+
* - Converts Map instances to plain objects
420+
* - Converts Set instances to arrays
421+
* - Replaces circular references with '[Circular]' marker
422+
*
423+
* @example
424+
* const obj = { name: 'test', map: new Map([['key', 'value']]) };
425+
* obj.self = obj; // circular reference
426+
* JSON.stringify(obj, Utils.getCircularReplacer());
427+
* // Output: {"name":"test","map":{"key":"value"},"self":"[Circular]"}
428+
*/
429+
static getCircularReplacer() {
430+
const seen = new WeakSet();
431+
return (key, value) => {
432+
if (value instanceof Map) {
433+
return Object.fromEntries(value);
434+
}
435+
if (value instanceof Set) {
436+
return Array.from(value);
437+
}
438+
if (typeof value === 'object' && value !== null) {
439+
if (seen.has(value)) {
440+
return '[Circular]';
441+
}
442+
seen.add(value);
443+
}
444+
return value;
445+
};
446+
}
413447
}
414448

415449
module.exports = Utils;

0 commit comments

Comments
 (0)