feat: Add MongoDB client event logging via database option logClientEvents (#9914)

This commit is contained in:
Manuel
2025-11-08 15:48:29 +01:00
committed by GitHub
parent 2424054221
commit b760733b98
9 changed files with 501 additions and 25 deletions

View File

@@ -30,7 +30,7 @@
"theme_opts": { "theme_opts": {
"default_theme": "dark", "default_theme": "dark",
"title": "<img src='https://raw.githubusercontent.com/parse-community/parse-server/alpha/.github/parse-server-logo.png' class='logo'/>", "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": { "markdown": {

View File

@@ -36,15 +36,17 @@ const nestedOptionEnvPrefix = {
IdempotencyOptions: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_', IdempotencyOptions: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_',
LiveQueryOptions: 'PARSE_SERVER_LIVEQUERY_', LiveQueryOptions: 'PARSE_SERVER_LIVEQUERY_',
LiveQueryServerOptions: 'PARSE_LIVE_QUERY_SERVER_', 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_', PagesCustomUrlsOptions: 'PARSE_SERVER_PAGES_CUSTOM_URL_',
PagesOptions: 'PARSE_SERVER_PAGES_', PagesOptions: 'PARSE_SERVER_PAGES_',
PagesRoute: 'PARSE_SERVER_PAGES_ROUTE_', PagesRoute: 'PARSE_SERVER_PAGES_ROUTE_',
ParseServerOptions: 'PARSE_SERVER_', ParseServerOptions: 'PARSE_SERVER_',
PasswordPolicyOptions: 'PARSE_SERVER_PASSWORD_POLICY_', PasswordPolicyOptions: 'PARSE_SERVER_PASSWORD_POLICY_',
SecurityOptions: 'PARSE_SERVER_SECURITY_',
SchemaOptions: 'PARSE_SERVER_SCHEMA_',
LogLevels: 'PARSE_SERVER_LOG_LEVELS_',
RateLimitOptions: 'PARSE_SERVER_RATE_LIMIT_', RateLimitOptions: 'PARSE_SERVER_RATE_LIMIT_',
SchemaOptions: 'PARSE_SERVER_SCHEMA_',
SecurityOptions: 'PARSE_SERVER_SECURITY_',
}; };
function last(array) { function last(array) {

View File

@@ -824,4 +824,243 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
expect(roleIndexes.find(idx => idx.name === 'name_1')).toBeDefined(); 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();
});
});
}); });

View File

@@ -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"}}');
});
});
}); });

View File

@@ -1,16 +1,16 @@
// @flow // @flow
import { format as formatUrl, parse as parseUrl } from '../../../vendor/mongodbUrl';
import type { QueryOptions, QueryType, SchemaType, StorageClass } from '../StorageAdapter';
import { StorageAdapter } from '../StorageAdapter';
import MongoCollection from './MongoCollection'; import MongoCollection from './MongoCollection';
import MongoSchemaCollection from './MongoSchemaCollection'; import MongoSchemaCollection from './MongoSchemaCollection';
import { StorageAdapter } from '../StorageAdapter';
import type { SchemaType, QueryType, StorageClass, QueryOptions } from '../StorageAdapter';
import { parse as parseUrl, format as formatUrl } from '../../../vendor/mongodbUrl';
import { import {
parseObjectToMongoObjectForCreate,
mongoObjectToParseObject, mongoObjectToParseObject,
parseObjectToMongoObjectForCreate,
transformKey, transformKey,
transformWhere,
transformUpdate,
transformPointerString, transformPointerString,
transformUpdate,
transformWhere,
} from './MongoTransform'; } from './MongoTransform';
// @flow-disable-next // @flow-disable-next
import Parse from 'parse/node'; import Parse from 'parse/node';
@@ -18,6 +18,7 @@ import Parse from 'parse/node';
import _ from 'lodash'; import _ from 'lodash';
import defaults from '../../../defaults'; import defaults from '../../../defaults';
import logger from '../../../logger'; import logger from '../../../logger';
import Utils from '../../../Utils';
// @flow-disable-next // @flow-disable-next
const mongodb = require('mongodb'); const mongodb = require('mongodb');
@@ -132,6 +133,7 @@ export class MongoStorageAdapter implements StorageAdapter {
_mongoOptions: Object; _mongoOptions: Object;
_onchange: any; _onchange: any;
_stream: any; _stream: any;
_logClientEvents: ?Array<any>;
// Public // Public
connectionPromise: ?Promise<any>; connectionPromise: ?Promise<any>;
database: any; database: any;
@@ -154,6 +156,7 @@ export class MongoStorageAdapter implements StorageAdapter {
this.enableSchemaHooks = !!mongoOptions.enableSchemaHooks; this.enableSchemaHooks = !!mongoOptions.enableSchemaHooks;
this.schemaCacheTtl = mongoOptions.schemaCacheTtl; this.schemaCacheTtl = mongoOptions.schemaCacheTtl;
this.disableIndexFieldValidation = !!mongoOptions.disableIndexFieldValidation; this.disableIndexFieldValidation = !!mongoOptions.disableIndexFieldValidation;
this._logClientEvents = mongoOptions.logClientEvents;
// Remove Parse Server-specific options that should not be passed to MongoDB client // Remove Parse Server-specific options that should not be passed to MongoDB client
// Note: We only delete from this._mongoOptions, not from the original mongoOptions object, // Note: We only delete from this._mongoOptions, not from the original mongoOptions object,
// because other components (like DatabaseController) need access to these options // because other components (like DatabaseController) need access to these options
@@ -162,6 +165,7 @@ export class MongoStorageAdapter implements StorageAdapter {
'schemaCacheTtl', 'schemaCacheTtl',
'maxTimeMS', 'maxTimeMS',
'disableIndexFieldValidation', 'disableIndexFieldValidation',
'logClientEvents',
'createIndexUserUsername', 'createIndexUserUsername',
'createIndexUserUsernameCaseInsensitive', 'createIndexUserUsernameCaseInsensitive',
'createIndexUserEmail', 'createIndexUserEmail',
@@ -203,6 +207,31 @@ export class MongoStorageAdapter implements StorageAdapter {
client.on('close', () => { client.on('close', () => {
delete this.connectionPromise; delete this.connectionPromise;
}); });
// Set up client event logging if configured
if (this._logClientEvents && Array.isArray(this._logClientEvents)) {
this._logClientEvents.forEach(eventConfig => {
client.on(eventConfig.name, event => {
let logData = {};
if (!eventConfig.keys || eventConfig.keys.length === 0) {
logData = event;
} else {
eventConfig.keys.forEach(keyPath => {
logData[keyPath] = _.get(event, keyPath);
});
}
// Validate log level exists, fallback to 'info'
const logLevel = typeof logger[eventConfig.logLevel] === 'function' ? eventConfig.logLevel : 'info';
// Safe JSON serialization with Map/Set and circular reference support
const logMessage = `MongoDB client event ${eventConfig.name}: ${JSON.stringify(logData, Utils.getCircularReplacer())}`;
logger[logLevel](logMessage);
});
});
}
this.client = client; this.client = client;
this.database = database; this.database = database;
}) })

View File

@@ -1083,6 +1083,59 @@ module.exports.FileUploadOptions = {
default: ['^(?![xXsS]?[hH][tT][mM][lL]?$)'], default: ['^(?![xXsS]?[hH][tT][mM][lL]?$)'],
}, },
}; };
/* The available log levels for Parse Server logging. Valid values are:<br>- `'error'` - Error level (highest priority)<br>- `'warn'` - Warning level<br>- `'info'` - Info level (default)<br>- `'verbose'` - Verbose level<br>- `'debug'` - Debug level<br>- `'silly'` - Silly level (lowest priority) */
module.exports.LogLevel = {
debug: {
env: 'PARSE_SERVER_LOG_LEVEL_DEBUG',
help: 'Debug level',
required: true,
},
error: {
env: 'PARSE_SERVER_LOG_LEVEL_ERROR',
help: 'Error level - highest priority',
required: true,
},
info: {
env: 'PARSE_SERVER_LOG_LEVEL_INFO',
help: 'Info level - default',
required: true,
},
silly: {
env: 'PARSE_SERVER_LOG_LEVEL_SILLY',
help: 'Silly level - lowest priority',
required: true,
},
verbose: {
env: 'PARSE_SERVER_LOG_LEVEL_VERBOSE',
help: 'Verbose level',
required: true,
},
warn: {
env: 'PARSE_SERVER_LOG_LEVEL_WARN',
help: 'Warning level',
required: true,
},
};
module.exports.LogClientEvent = {
keys: {
env: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_KEYS',
help:
'Optional array of dot-notation paths to extract specific data from the event object. If not provided or empty, the entire event object will be logged.',
action: parsers.arrayParser,
},
logLevel: {
env: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_LOG_LEVEL',
help:
"The log level to use for this event. See [LogLevel](LogLevel.html) for available values. Defaults to `'info'`.",
default: 'info',
},
name: {
env: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_NAME',
help:
'The MongoDB driver event name to listen for. See the [MongoDB driver events documentation](https://www.mongodb.com/docs/drivers/node/current/fundamentals/monitoring/) for available events.',
required: true,
},
};
module.exports.DatabaseOptions = { module.exports.DatabaseOptions = {
appName: { appName: {
env: 'PARSE_SERVER_DATABASE_APP_NAME', env: 'PARSE_SERVER_DATABASE_APP_NAME',
@@ -1219,6 +1272,12 @@ module.exports.DatabaseOptions = {
'The MongoDB driver option to specify the size (in milliseconds) of the latency window for selecting among multiple suitable MongoDB instances.', 'The MongoDB driver option to specify the size (in milliseconds) of the latency window for selecting among multiple suitable MongoDB instances.',
action: parsers.numberParser('localThresholdMS'), action: parsers.numberParser('localThresholdMS'),
}, },
logClientEvents: {
env: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS',
help: 'An array of MongoDB client event configurations to enable logging of specific events.',
action: parsers.arrayParser,
type: 'LogClientEvent[]',
},
maxConnecting: { maxConnecting: {
env: 'PARSE_SERVER_DATABASE_MAX_CONNECTING', env: 'PARSE_SERVER_DATABASE_MAX_CONNECTING',
help: help:
@@ -1403,30 +1462,32 @@ module.exports.AuthAdapter = {
module.exports.LogLevels = { module.exports.LogLevels = {
cloudFunctionError: { cloudFunctionError: {
env: 'PARSE_SERVER_LOG_LEVELS_CLOUD_FUNCTION_ERROR', env: 'PARSE_SERVER_LOG_LEVELS_CLOUD_FUNCTION_ERROR',
help: 'Log level used by the Cloud Code Functions on error. Default is `error`.', help:
'Log level used by the Cloud Code Functions on error. Default is `error`. See [LogLevel](LogLevel.html) for available values.',
default: 'error', default: 'error',
}, },
cloudFunctionSuccess: { cloudFunctionSuccess: {
env: 'PARSE_SERVER_LOG_LEVELS_CLOUD_FUNCTION_SUCCESS', env: 'PARSE_SERVER_LOG_LEVELS_CLOUD_FUNCTION_SUCCESS',
help: 'Log level used by the Cloud Code Functions on success. Default is `info`.', help:
'Log level used by the Cloud Code Functions on success. Default is `info`. See [LogLevel](LogLevel.html) for available values.',
default: 'info', default: 'info',
}, },
triggerAfter: { triggerAfter: {
env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_AFTER', env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_AFTER',
help: help:
'Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`.', 'Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`. See [LogLevel](LogLevel.html) for available values.',
default: 'info', default: 'info',
}, },
triggerBeforeError: { triggerBeforeError: {
env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_ERROR', env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_ERROR',
help: help:
'Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`.', 'Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`. See [LogLevel](LogLevel.html) for available values.',
default: 'error', default: 'error',
}, },
triggerBeforeSuccess: { triggerBeforeSuccess: {
env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_SUCCESS', env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_SUCCESS',
help: help:
'Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`.', 'Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`. See [LogLevel](LogLevel.html) for available values.',
default: 'info', default: 'info',
}, },
}; };

View File

@@ -238,6 +238,23 @@
* @property {String[]} fileExtensions Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern.<br><br>It is recommended to restrict the file upload extensions as much as possible. HTML files are especially problematic as they may be used by an attacker who uploads a HTML form to look legitimate under your app's domain name, or to compromise the session token of another user via accessing the browser's local storage.<br><br>Defaults to `^(?![xXsS]?[hH][tT][mM][lL]?$)` which allows any file extension except those MIME types that are mapped to `text/html` and are rendered as website by a web browser. * @property {String[]} fileExtensions Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern.<br><br>It is recommended to restrict the file upload extensions as much as possible. HTML files are especially problematic as they may be used by an attacker who uploads a HTML form to look legitimate under your app's domain name, or to compromise the session token of another user via accessing the browser's local storage.<br><br>Defaults to `^(?![xXsS]?[hH][tT][mM][lL]?$)` which allows any file extension except those MIME types that are mapped to `text/html` and are rendered as website by a web browser.
*/ */
/**
* @interface LogLevel
* @property {StringLiteral} debug Debug level
* @property {StringLiteral} error Error level - highest priority
* @property {StringLiteral} info Info level - default
* @property {StringLiteral} silly Silly level - lowest priority
* @property {StringLiteral} verbose Verbose level
* @property {StringLiteral} warn Warning level
*/
/**
* @interface LogClientEvent
* @property {String[]} keys Optional array of dot-notation paths to extract specific data from the event object. If not provided or empty, the entire event object will be logged.
* @property {String} logLevel The log level to use for this event. See [LogLevel](LogLevel.html) for available values. Defaults to `'info'`.
* @property {String} name The MongoDB driver event name to listen for. See the [MongoDB driver events documentation](https://www.mongodb.com/docs/drivers/node/current/fundamentals/monitoring/) for available events.
*/
/** /**
* @interface DatabaseOptions * @interface DatabaseOptions
* @property {String} appName The MongoDB driver option to specify the name of the application that created this MongoClient instance. * @property {String} appName The MongoDB driver option to specify the name of the application that created this MongoClient instance.
@@ -262,6 +279,7 @@
* @property {Number} heartbeatFrequencyMS The MongoDB driver option to specify the frequency in milliseconds at which the driver checks the state of the MongoDB deployment. * @property {Number} heartbeatFrequencyMS The MongoDB driver option to specify the frequency in milliseconds at which the driver checks the state of the MongoDB deployment.
* @property {Boolean} loadBalanced The MongoDB driver option to instruct the driver it is connecting to a load balancer fronting a mongos like service. * @property {Boolean} loadBalanced The MongoDB driver option to instruct the driver it is connecting to a load balancer fronting a mongos like service.
* @property {Number} localThresholdMS The MongoDB driver option to specify the size (in milliseconds) of the latency window for selecting among multiple suitable MongoDB instances. * @property {Number} localThresholdMS The MongoDB driver option to specify the size (in milliseconds) of the latency window for selecting among multiple suitable MongoDB instances.
* @property {LogClientEvent[]} logClientEvents An array of MongoDB client event configurations to enable logging of specific events.
* @property {Number} maxConnecting The MongoDB driver option to specify the maximum number of connections that may be in the process of being established concurrently by the connection pool. * @property {Number} maxConnecting The MongoDB driver option to specify the maximum number of connections that may be in the process of being established concurrently by the connection pool.
* @property {Number} maxIdleTimeMS The MongoDB driver option to specify the amount of time in milliseconds that a connection can remain idle in the connection pool before being removed and closed. * @property {Number} maxIdleTimeMS The MongoDB driver option to specify the amount of time in milliseconds that a connection can remain idle in the connection pool before being removed and closed.
* @property {Number} maxPoolSize The MongoDB driver option to set the maximum number of opened, cached, ready-to-use database connections maintained by the driver. * @property {Number} maxPoolSize The MongoDB driver option to set the maximum number of opened, cached, ready-to-use database connections maintained by the driver.
@@ -303,9 +321,9 @@
/** /**
* @interface LogLevels * @interface LogLevels
* @property {String} cloudFunctionError Log level used by the Cloud Code Functions on error. Default is `error`. * @property {String} cloudFunctionError Log level used by the Cloud Code Functions on error. Default is `error`. See [LogLevel](LogLevel.html) for available values.
* @property {String} cloudFunctionSuccess Log level used by the Cloud Code Functions on success. Default is `info`. * @property {String} cloudFunctionSuccess Log level used by the Cloud Code Functions on success. Default is `info`. See [LogLevel](LogLevel.html) for available values.
* @property {String} triggerAfter Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`. * @property {String} triggerAfter Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`. See [LogLevel](LogLevel.html) for available values.
* @property {String} triggerBeforeError Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`. * @property {String} triggerBeforeError Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`. See [LogLevel](LogLevel.html) for available values.
* @property {String} triggerBeforeSuccess Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`. * @property {String} triggerBeforeSuccess Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`. See [LogLevel](LogLevel.html) for available values.
*/ */

View File

@@ -608,6 +608,32 @@ export interface FileUploadOptions {
enableForPublic: ?boolean; enableForPublic: ?boolean;
} }
/* The available log levels for Parse Server logging. Valid values are:<br>- `'error'` - Error level (highest priority)<br>- `'warn'` - Warning level<br>- `'info'` - Info level (default)<br>- `'verbose'` - Verbose level<br>- `'debug'` - Debug level<br>- `'silly'` - Silly level (lowest priority) */
export interface LogLevel {
/* Error level - highest priority */
error: 'error';
/* Warning level */
warn: 'warn';
/* Info level - default */
info: 'info';
/* Verbose level */
verbose: 'verbose';
/* Debug level */
debug: 'debug';
/* Silly level - lowest priority */
silly: 'silly';
}
export interface LogClientEvent {
/* The MongoDB driver event name to listen for. See the [MongoDB driver events documentation](https://www.mongodb.com/docs/drivers/node/current/fundamentals/monitoring/) for available events. */
name: string;
/* Optional array of dot-notation paths to extract specific data from the event object. If not provided or empty, the entire event object will be logged. */
keys: ?(string[]);
/* The log level to use for this event. See [LogLevel](LogLevel.html) for available values. Defaults to `'info'`.
:DEFAULT: info */
logLevel: ?string;
}
export interface DatabaseOptions { export interface DatabaseOptions {
/* Enables database real-time hooks to update single schema cache. Set to `true` if using multiple Parse Servers instances connected to the same database. Failing to do so will cause a schema change to not propagate to all instances and re-syncing will only happen when the instances restart. To use this feature with MongoDB, a replica set cluster with [change stream](https://docs.mongodb.com/manual/changeStreams/#availability) support is required. /* Enables database real-time hooks to update single schema cache. Set to `true` if using multiple Parse Servers instances connected to the same database. Failing to do so will cause a schema change to not propagate to all instances and re-syncing will only happen when the instances restart. To use this feature with MongoDB, a replica set cluster with [change stream](https://docs.mongodb.com/manual/changeStreams/#availability) support is required.
:DEFAULT: false */ :DEFAULT: false */
@@ -725,6 +751,8 @@ export interface DatabaseOptions {
createIndexRoleName: ?boolean; createIndexRoleName: ?boolean;
/* Set to `true` to disable validation of index fields. When disabled, indexes can be created even if the fields do not exist in the schema. This can be useful when creating indexes on fields that will be added later. */ /* Set to `true` to disable validation of index fields. When disabled, indexes can be created even if the fields do not exist in the schema. This can be useful when creating indexes on fields that will be added later. */
disableIndexFieldValidation: ?boolean; disableIndexFieldValidation: ?boolean;
/* An array of MongoDB client event configurations to enable logging of specific events. */
logClientEvents: ?(LogClientEvent[]);
} }
export interface AuthAdapter { export interface AuthAdapter {
@@ -736,23 +764,23 @@ export interface AuthAdapter {
} }
export interface LogLevels { export interface LogLevels {
/* Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`. /* Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`. See [LogLevel](LogLevel.html) for available values.
:DEFAULT: info :DEFAULT: info
*/ */
triggerAfter: ?string; triggerAfter: ?string;
/* Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`. /* Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`. See [LogLevel](LogLevel.html) for available values.
:DEFAULT: info :DEFAULT: info
*/ */
triggerBeforeSuccess: ?string; triggerBeforeSuccess: ?string;
/* Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`. /* Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`. See [LogLevel](LogLevel.html) for available values.
:DEFAULT: error :DEFAULT: error
*/ */
triggerBeforeError: ?string; triggerBeforeError: ?string;
/* Log level used by the Cloud Code Functions on success. Default is `info`. /* Log level used by the Cloud Code Functions on success. Default is `info`. See [LogLevel](LogLevel.html) for available values.
:DEFAULT: info :DEFAULT: info
*/ */
cloudFunctionSuccess: ?string; cloudFunctionSuccess: ?string;
/* Log level used by the Cloud Code Functions on error. Default is `error`. /* Log level used by the Cloud Code Functions on error. Default is `error`. See [LogLevel](LogLevel.html) for available values.
:DEFAULT: error :DEFAULT: error
*/ */
cloudFunctionError: ?string; cloudFunctionError: ?string;

View File

@@ -410,6 +410,40 @@ class Utils {
'%' + char.charCodeAt(0).toString(16).toUpperCase() '%' + char.charCodeAt(0).toString(16).toUpperCase()
); );
} }
/**
* Creates a JSON replacer function that handles Map, Set, and circular references.
* This replacer can be used with JSON.stringify to safely serialize complex objects.
*
* @returns {Function} A replacer function for JSON.stringify that:
* - Converts Map instances to plain objects
* - Converts Set instances to arrays
* - Replaces circular references with '[Circular]' marker
*
* @example
* const obj = { name: 'test', map: new Map([['key', 'value']]) };
* obj.self = obj; // circular reference
* JSON.stringify(obj, Utils.getCircularReplacer());
* // Output: {"name":"test","map":{"key":"value"},"self":"[Circular]"}
*/
static getCircularReplacer() {
const seen = new WeakSet();
return (key, value) => {
if (value instanceof Map) {
return Object.fromEntries(value);
}
if (value instanceof Set) {
return Array.from(value);
}
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular]';
}
seen.add(value);
}
return value;
};
}
} }
module.exports = Utils; module.exports = Utils;