Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion jsdoc-conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"theme_opts": {
"default_theme": "dark",
"title": "<img src='https://raw.githubusercontent.com/parse-community/parse-server/alpha/.github/parse-server-logo.png' class='logo'/>",
"create_style": "header, .sidebar-section-title, .sidebar-title { color: #139cee !important } .logo { margin-left : 40px; margin-right: 40px }"
"create_style": "header, .sidebar-section-title, .sidebar-title { color: #139cee !important } .logo { margin-left : 40px; margin-right: 40px; height: auto; max-width: 100%; object-fit: contain; }"
}
},
"markdown": {
Expand Down
8 changes: 5 additions & 3 deletions resources/buildConfigDefinitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,17 @@ const nestedOptionEnvPrefix = {
IdempotencyOptions: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_',
LiveQueryOptions: 'PARSE_SERVER_LIVEQUERY_',
LiveQueryServerOptions: 'PARSE_LIVE_QUERY_SERVER_',
LogClientEvent: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_',
LogLevel: 'PARSE_SERVER_LOG_LEVEL_',
LogLevels: 'PARSE_SERVER_LOG_LEVELS_',
PagesCustomUrlsOptions: 'PARSE_SERVER_PAGES_CUSTOM_URL_',
PagesOptions: 'PARSE_SERVER_PAGES_',
PagesRoute: 'PARSE_SERVER_PAGES_ROUTE_',
ParseServerOptions: 'PARSE_SERVER_',
PasswordPolicyOptions: 'PARSE_SERVER_PASSWORD_POLICY_',
SecurityOptions: 'PARSE_SERVER_SECURITY_',
SchemaOptions: 'PARSE_SERVER_SCHEMA_',
LogLevels: 'PARSE_SERVER_LOG_LEVELS_',
RateLimitOptions: 'PARSE_SERVER_RATE_LIMIT_',
SchemaOptions: 'PARSE_SERVER_SCHEMA_',
SecurityOptions: 'PARSE_SERVER_SECURITY_',
};

function last(array) {
Expand Down
239 changes: 239 additions & 0 deletions spec/MongoStorageAdapter.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -824,4 +824,243 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
expect(roleIndexes.find(idx => idx.name === 'name_1')).toBeDefined();
});
});

describe('logClientEvents', () => {
it('should log MongoDB client events when configured', async () => {
const logger = require('../lib/logger').logger;
const logSpy = spyOn(logger, 'warn');

const logClientEvents = [
{
name: 'serverDescriptionChanged',
keys: ['address'],
logLevel: 'warn',
},
];

const adapter = new MongoStorageAdapter({
uri: databaseURI,
mongoOptions: { logClientEvents },
});

// Connect to trigger event listeners setup
await adapter.connect();

// Manually trigger the event to test the listener
const mockEvent = {
address: 'localhost:27017',
previousDescription: { type: 'Unknown' },
newDescription: { type: 'Standalone' },
};

adapter.client.emit('serverDescriptionChanged', mockEvent);

// Verify the log was called with the correct message
expect(logSpy).toHaveBeenCalledWith(
jasmine.stringMatching(/MongoDB client event serverDescriptionChanged:.*"address":"localhost:27017"/)
);

await adapter.handleShutdown();
});

it('should log entire event when keys are not specified', async () => {
const logger = require('../lib/logger').logger;
const logSpy = spyOn(logger, 'info');

const logClientEvents = [
{
name: 'connectionPoolReady',
logLevel: 'info',
},
];

const adapter = new MongoStorageAdapter({
uri: databaseURI,
mongoOptions: { logClientEvents },
});

await adapter.connect();

const mockEvent = {
address: 'localhost:27017',
options: { maxPoolSize: 100 },
};

adapter.client.emit('connectionPoolReady', mockEvent);

expect(logSpy).toHaveBeenCalledWith(
jasmine.stringMatching(/MongoDB client event connectionPoolReady:.*"address":"localhost:27017".*"options"/)
);

await adapter.handleShutdown();
});

it('should extract nested keys using dot notation', async () => {
const logger = require('../lib/logger').logger;
const logSpy = spyOn(logger, 'warn');

const logClientEvents = [
{
name: 'topologyDescriptionChanged',
keys: ['previousDescription.type', 'newDescription.type', 'newDescription.servers.size'],
logLevel: 'warn',
},
];

const adapter = new MongoStorageAdapter({
uri: databaseURI,
mongoOptions: { logClientEvents },
});

await adapter.connect();

const mockEvent = {
topologyId: 1,
previousDescription: { type: 'Unknown' },
newDescription: {
type: 'ReplicaSetWithPrimary',
servers: { size: 3 },
},
};

adapter.client.emit('topologyDescriptionChanged', mockEvent);

expect(logSpy).toHaveBeenCalledWith(
jasmine.stringMatching(/MongoDB client event topologyDescriptionChanged:.*"previousDescription.type":"Unknown".*"newDescription.type":"ReplicaSetWithPrimary".*"newDescription.servers.size":3/)
);

await adapter.handleShutdown();
});

it('should handle invalid log level gracefully', async () => {
const logger = require('../lib/logger').logger;
const infoSpy = spyOn(logger, 'info');

const logClientEvents = [
{
name: 'connectionPoolReady',
keys: ['address'],
logLevel: 'invalidLogLevel', // Invalid log level
},
];

const adapter = new MongoStorageAdapter({
uri: databaseURI,
mongoOptions: { logClientEvents },
});

await adapter.connect();

const mockEvent = {
address: 'localhost:27017',
};

adapter.client.emit('connectionPoolReady', mockEvent);

// Should fallback to 'info' level
expect(infoSpy).toHaveBeenCalledWith(
jasmine.stringMatching(/MongoDB client event connectionPoolReady:.*"address":"localhost:27017"/)
);

await adapter.handleShutdown();
});

it('should handle Map and Set instances in events', async () => {
const logger = require('../lib/logger').logger;
const warnSpy = spyOn(logger, 'warn');

const logClientEvents = [
{
name: 'customEvent',
logLevel: 'warn',
},
];

const adapter = new MongoStorageAdapter({
uri: databaseURI,
mongoOptions: { logClientEvents },
});

await adapter.connect();

const mockEvent = {
mapData: new Map([['key1', 'value1'], ['key2', 'value2']]),
setData: new Set([1, 2, 3]),
};

adapter.client.emit('customEvent', mockEvent);

// Should serialize Map and Set properly
expect(warnSpy).toHaveBeenCalledWith(
jasmine.stringMatching(/MongoDB client event customEvent:.*"mapData":\{"key1":"value1","key2":"value2"\}.*"setData":\[1,2,3\]/)
);

await adapter.handleShutdown();
});

it('should handle missing keys in event object', async () => {
const logger = require('../lib/logger').logger;
const infoSpy = spyOn(logger, 'info');

const logClientEvents = [
{
name: 'testEvent',
keys: ['nonexistent.nested.key', 'another.missing'],
logLevel: 'info',
},
];

const adapter = new MongoStorageAdapter({
uri: databaseURI,
mongoOptions: { logClientEvents },
});

await adapter.connect();

const mockEvent = {
actualField: 'value',
};

adapter.client.emit('testEvent', mockEvent);

// Should handle missing keys gracefully with undefined values
expect(infoSpy).toHaveBeenCalledWith(
jasmine.stringMatching(/MongoDB client event testEvent:/)
);

await adapter.handleShutdown();
});

it('should handle circular references gracefully', async () => {
const logger = require('../lib/logger').logger;
const infoSpy = spyOn(logger, 'info');

const logClientEvents = [
{
name: 'circularEvent',
logLevel: 'info',
},
];

const adapter = new MongoStorageAdapter({
uri: databaseURI,
mongoOptions: { logClientEvents },
});

await adapter.connect();

// Create circular reference
const mockEvent = { name: 'test' };
mockEvent.self = mockEvent;

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

// Should handle circular reference with [Circular] marker
expect(infoSpy).toHaveBeenCalledWith(
jasmine.stringMatching(/MongoDB client event circularEvent:.*\[Circular\]/)
);

await adapter.handleShutdown();
});
});
});
65 changes: 65 additions & 0 deletions spec/Utils.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,69 @@ describe('Utils', () => {
});
});
});

describe('getCircularReplacer', () => {
it('should handle Map instances', () => {
const obj = {
name: 'test',
mapData: new Map([
['key1', 'value1'],
['key2', 'value2']
])
};
const result = JSON.stringify(obj, Utils.getCircularReplacer());
expect(result).toBe('{"name":"test","mapData":{"key1":"value1","key2":"value2"}}');
});

it('should handle Set instances', () => {
const obj = {
name: 'test',
setData: new Set([1, 2, 3])
};
const result = JSON.stringify(obj, Utils.getCircularReplacer());
expect(result).toBe('{"name":"test","setData":[1,2,3]}');
});

it('should handle circular references', () => {
const obj = { name: 'test', value: 123 };
obj.self = obj;
const result = JSON.stringify(obj, Utils.getCircularReplacer());
expect(result).toBe('{"name":"test","value":123,"self":"[Circular]"}');
});

it('should handle nested circular references', () => {
const obj = {
name: 'parent',
child: {
name: 'child'
}
};
obj.child.parent = obj;
const result = JSON.stringify(obj, Utils.getCircularReplacer());
expect(result).toBe('{"name":"parent","child":{"name":"child","parent":"[Circular]"}}');
});

it('should handle mixed Map, Set, and circular references', () => {
const obj = {
mapData: new Map([['key', 'value']]),
setData: new Set([1, 2]),
regular: 'data'
};
obj.circular = obj;
const result = JSON.stringify(obj, Utils.getCircularReplacer());
expect(result).toBe('{"mapData":{"key":"value"},"setData":[1,2],"regular":"data","circular":"[Circular]"}');
});

it('should handle normal objects without modification', () => {
const obj = {
name: 'test',
number: 42,
nested: {
key: 'value'
}
};
const result = JSON.stringify(obj, Utils.getCircularReplacer());
expect(result).toBe('{"name":"test","number":42,"nested":{"key":"value"}}');
});
});
});
Loading
Loading