Skip to content

Commit 9ecb509

Browse files
authored
Merge branch 'alpha' into feat-add-deprecation-explain
Signed-off-by: Manuel <5673677+mtrezza@users.noreply.github.com>
2 parents 0d82310 + 15c8b1a commit 9ecb509

File tree

12 files changed

+513
-30
lines changed

12 files changed

+513
-30
lines changed

changelogs/CHANGELOG_alpha.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# [8.5.0-alpha.4](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.3...8.5.0-alpha.4) (2025-11-08)
2+
3+
4+
### Features
5+
6+
* Add MongoDB client event logging via database option `logClientEvents` ([#9914](https://github.com/parse-community/parse-server/issues/9914)) ([b760733](https://github.com/parse-community/parse-server/commit/b760733b98bcfc9c09ac9780066602e1fda108fe))
7+
18
# [8.5.0-alpha.3](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.2...8.5.0-alpha.3) (2025-11-07)
29

310

jsdoc-conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"theme_opts": {
3131
"default_theme": "dark",
3232
"title": "<img src='https://raw.githubusercontent.com/parse-community/parse-server/alpha/.github/parse-server-logo.png' class='logo'/>",
33-
"create_style": "header, .sidebar-section-title, .sidebar-title { color: #139cee !important } .logo { margin-left : 40px; margin-right: 40px }"
33+
"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; }"
3434
}
3535
},
3636
"markdown": {

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "parse-server",
3-
"version": "8.5.0-alpha.3",
3+
"version": "8.5.0-alpha.4",
44
"description": "An express module providing a Parse-compatible API server",
55
"main": "lib/index.js",
66
"repository": {

resources/buildConfigDefinitions.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,17 @@ const nestedOptionEnvPrefix = {
3636
IdempotencyOptions: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_',
3737
LiveQueryOptions: 'PARSE_SERVER_LIVEQUERY_',
3838
LiveQueryServerOptions: 'PARSE_LIVE_QUERY_SERVER_',
39+
LogClientEvent: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_',
40+
LogLevel: 'PARSE_SERVER_LOG_LEVEL_',
41+
LogLevels: 'PARSE_SERVER_LOG_LEVELS_',
3942
PagesCustomUrlsOptions: 'PARSE_SERVER_PAGES_CUSTOM_URL_',
4043
PagesOptions: 'PARSE_SERVER_PAGES_',
4144
PagesRoute: 'PARSE_SERVER_PAGES_ROUTE_',
4245
ParseServerOptions: 'PARSE_SERVER_',
4346
PasswordPolicyOptions: 'PARSE_SERVER_PASSWORD_POLICY_',
44-
SecurityOptions: 'PARSE_SERVER_SECURITY_',
45-
SchemaOptions: 'PARSE_SERVER_SCHEMA_',
46-
LogLevels: 'PARSE_SERVER_LOG_LEVELS_',
4747
RateLimitOptions: 'PARSE_SERVER_RATE_LIMIT_',
48+
SchemaOptions: 'PARSE_SERVER_SCHEMA_',
49+
SecurityOptions: 'PARSE_SERVER_SECURITY_',
4850
};
4951

5052
function last(array) {

spec/MongoStorageAdapter.spec.js

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -824,4 +824,243 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
824824
expect(roleIndexes.find(idx => idx.name === 'name_1')).toBeDefined();
825825
});
826826
});
827+
828+
describe('logClientEvents', () => {
829+
it('should log MongoDB client events when configured', async () => {
830+
const logger = require('../lib/logger').logger;
831+
const logSpy = spyOn(logger, 'warn');
832+
833+
const logClientEvents = [
834+
{
835+
name: 'serverDescriptionChanged',
836+
keys: ['address'],
837+
logLevel: 'warn',
838+
},
839+
];
840+
841+
const adapter = new MongoStorageAdapter({
842+
uri: databaseURI,
843+
mongoOptions: { logClientEvents },
844+
});
845+
846+
// Connect to trigger event listeners setup
847+
await adapter.connect();
848+
849+
// Manually trigger the event to test the listener
850+
const mockEvent = {
851+
address: 'localhost:27017',
852+
previousDescription: { type: 'Unknown' },
853+
newDescription: { type: 'Standalone' },
854+
};
855+
856+
adapter.client.emit('serverDescriptionChanged', mockEvent);
857+
858+
// Verify the log was called with the correct message
859+
expect(logSpy).toHaveBeenCalledWith(
860+
jasmine.stringMatching(/MongoDB client event serverDescriptionChanged:.*"address":"localhost:27017"/)
861+
);
862+
863+
await adapter.handleShutdown();
864+
});
865+
866+
it('should log entire event when keys are not specified', async () => {
867+
const logger = require('../lib/logger').logger;
868+
const logSpy = spyOn(logger, 'info');
869+
870+
const logClientEvents = [
871+
{
872+
name: 'connectionPoolReady',
873+
logLevel: 'info',
874+
},
875+
];
876+
877+
const adapter = new MongoStorageAdapter({
878+
uri: databaseURI,
879+
mongoOptions: { logClientEvents },
880+
});
881+
882+
await adapter.connect();
883+
884+
const mockEvent = {
885+
address: 'localhost:27017',
886+
options: { maxPoolSize: 100 },
887+
};
888+
889+
adapter.client.emit('connectionPoolReady', mockEvent);
890+
891+
expect(logSpy).toHaveBeenCalledWith(
892+
jasmine.stringMatching(/MongoDB client event connectionPoolReady:.*"address":"localhost:27017".*"options"/)
893+
);
894+
895+
await adapter.handleShutdown();
896+
});
897+
898+
it('should extract nested keys using dot notation', async () => {
899+
const logger = require('../lib/logger').logger;
900+
const logSpy = spyOn(logger, 'warn');
901+
902+
const logClientEvents = [
903+
{
904+
name: 'topologyDescriptionChanged',
905+
keys: ['previousDescription.type', 'newDescription.type', 'newDescription.servers.size'],
906+
logLevel: 'warn',
907+
},
908+
];
909+
910+
const adapter = new MongoStorageAdapter({
911+
uri: databaseURI,
912+
mongoOptions: { logClientEvents },
913+
});
914+
915+
await adapter.connect();
916+
917+
const mockEvent = {
918+
topologyId: 1,
919+
previousDescription: { type: 'Unknown' },
920+
newDescription: {
921+
type: 'ReplicaSetWithPrimary',
922+
servers: { size: 3 },
923+
},
924+
};
925+
926+
adapter.client.emit('topologyDescriptionChanged', mockEvent);
927+
928+
expect(logSpy).toHaveBeenCalledWith(
929+
jasmine.stringMatching(/MongoDB client event topologyDescriptionChanged:.*"previousDescription.type":"Unknown".*"newDescription.type":"ReplicaSetWithPrimary".*"newDescription.servers.size":3/)
930+
);
931+
932+
await adapter.handleShutdown();
933+
});
934+
935+
it('should handle invalid log level gracefully', async () => {
936+
const logger = require('../lib/logger').logger;
937+
const infoSpy = spyOn(logger, 'info');
938+
939+
const logClientEvents = [
940+
{
941+
name: 'connectionPoolReady',
942+
keys: ['address'],
943+
logLevel: 'invalidLogLevel', // Invalid log level
944+
},
945+
];
946+
947+
const adapter = new MongoStorageAdapter({
948+
uri: databaseURI,
949+
mongoOptions: { logClientEvents },
950+
});
951+
952+
await adapter.connect();
953+
954+
const mockEvent = {
955+
address: 'localhost:27017',
956+
};
957+
958+
adapter.client.emit('connectionPoolReady', mockEvent);
959+
960+
// Should fallback to 'info' level
961+
expect(infoSpy).toHaveBeenCalledWith(
962+
jasmine.stringMatching(/MongoDB client event connectionPoolReady:.*"address":"localhost:27017"/)
963+
);
964+
965+
await adapter.handleShutdown();
966+
});
967+
968+
it('should handle Map and Set instances in events', async () => {
969+
const logger = require('../lib/logger').logger;
970+
const warnSpy = spyOn(logger, 'warn');
971+
972+
const logClientEvents = [
973+
{
974+
name: 'customEvent',
975+
logLevel: 'warn',
976+
},
977+
];
978+
979+
const adapter = new MongoStorageAdapter({
980+
uri: databaseURI,
981+
mongoOptions: { logClientEvents },
982+
});
983+
984+
await adapter.connect();
985+
986+
const mockEvent = {
987+
mapData: new Map([['key1', 'value1'], ['key2', 'value2']]),
988+
setData: new Set([1, 2, 3]),
989+
};
990+
991+
adapter.client.emit('customEvent', mockEvent);
992+
993+
// Should serialize Map and Set properly
994+
expect(warnSpy).toHaveBeenCalledWith(
995+
jasmine.stringMatching(/MongoDB client event customEvent:.*"mapData":\{"key1":"value1","key2":"value2"\}.*"setData":\[1,2,3\]/)
996+
);
997+
998+
await adapter.handleShutdown();
999+
});
1000+
1001+
it('should handle missing keys in event object', async () => {
1002+
const logger = require('../lib/logger').logger;
1003+
const infoSpy = spyOn(logger, 'info');
1004+
1005+
const logClientEvents = [
1006+
{
1007+
name: 'testEvent',
1008+
keys: ['nonexistent.nested.key', 'another.missing'],
1009+
logLevel: 'info',
1010+
},
1011+
];
1012+
1013+
const adapter = new MongoStorageAdapter({
1014+
uri: databaseURI,
1015+
mongoOptions: { logClientEvents },
1016+
});
1017+
1018+
await adapter.connect();
1019+
1020+
const mockEvent = {
1021+
actualField: 'value',
1022+
};
1023+
1024+
adapter.client.emit('testEvent', mockEvent);
1025+
1026+
// Should handle missing keys gracefully with undefined values
1027+
expect(infoSpy).toHaveBeenCalledWith(
1028+
jasmine.stringMatching(/MongoDB client event testEvent:/)
1029+
);
1030+
1031+
await adapter.handleShutdown();
1032+
});
1033+
1034+
it('should handle circular references gracefully', async () => {
1035+
const logger = require('../lib/logger').logger;
1036+
const infoSpy = spyOn(logger, 'info');
1037+
1038+
const logClientEvents = [
1039+
{
1040+
name: 'circularEvent',
1041+
logLevel: 'info',
1042+
},
1043+
];
1044+
1045+
const adapter = new MongoStorageAdapter({
1046+
uri: databaseURI,
1047+
mongoOptions: { logClientEvents },
1048+
});
1049+
1050+
await adapter.connect();
1051+
1052+
// Create circular reference
1053+
const mockEvent = { name: 'test' };
1054+
mockEvent.self = mockEvent;
1055+
1056+
adapter.client.emit('circularEvent', mockEvent);
1057+
1058+
// Should handle circular reference with [Circular] marker
1059+
expect(infoSpy).toHaveBeenCalledWith(
1060+
jasmine.stringMatching(/MongoDB client event circularEvent:.*\[Circular\]/)
1061+
);
1062+
1063+
await adapter.handleShutdown();
1064+
});
1065+
});
8271066
});

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
});

0 commit comments

Comments
 (0)