diff --git a/jsdoc-conf.json b/jsdoc-conf.json index b410d239b0..e90f82556b 100644 --- a/jsdoc-conf.json +++ b/jsdoc-conf.json @@ -30,7 +30,7 @@ "theme_opts": { "default_theme": "dark", "title": "", - "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": { diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js index 5b9084f863..f4813975f4 100644 --- a/resources/buildConfigDefinitions.js +++ b/resources/buildConfigDefinitions.js @@ -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) { diff --git a/spec/MongoStorageAdapter.spec.js b/spec/MongoStorageAdapter.spec.js index 7d0d220cff..8b14973243 100644 --- a/spec/MongoStorageAdapter.spec.js +++ b/spec/MongoStorageAdapter.spec.js @@ -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(); + }); + }); }); diff --git a/spec/Utils.spec.js b/spec/Utils.spec.js index 14747af6aa..b1277c6bfe 100644 --- a/spec/Utils.spec.js +++ b/spec/Utils.spec.js @@ -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"}}'); + }); + }); }); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 39b335d52e..57f7543085 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -1,16 +1,16 @@ // @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 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 { - parseObjectToMongoObjectForCreate, mongoObjectToParseObject, + parseObjectToMongoObjectForCreate, transformKey, - transformWhere, - transformUpdate, transformPointerString, + transformUpdate, + transformWhere, } from './MongoTransform'; // @flow-disable-next import Parse from 'parse/node'; @@ -18,6 +18,7 @@ import Parse from 'parse/node'; import _ from 'lodash'; import defaults from '../../../defaults'; import logger from '../../../logger'; +import Utils from '../../../Utils'; // @flow-disable-next const mongodb = require('mongodb'); @@ -132,6 +133,7 @@ export class MongoStorageAdapter implements StorageAdapter { _mongoOptions: Object; _onchange: any; _stream: any; + _logClientEvents: ?Array; // Public connectionPromise: ?Promise; database: any; @@ -154,6 +156,7 @@ export class MongoStorageAdapter implements StorageAdapter { this.enableSchemaHooks = !!mongoOptions.enableSchemaHooks; this.schemaCacheTtl = mongoOptions.schemaCacheTtl; this.disableIndexFieldValidation = !!mongoOptions.disableIndexFieldValidation; + this._logClientEvents = mongoOptions.logClientEvents; // 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, // because other components (like DatabaseController) need access to these options @@ -162,6 +165,7 @@ export class MongoStorageAdapter implements StorageAdapter { 'schemaCacheTtl', 'maxTimeMS', 'disableIndexFieldValidation', + 'logClientEvents', 'createIndexUserUsername', 'createIndexUserUsernameCaseInsensitive', 'createIndexUserEmail', @@ -203,6 +207,31 @@ export class MongoStorageAdapter implements StorageAdapter { client.on('close', () => { 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.database = database; }) diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 96d1b9b32b..6930020de7 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -1083,6 +1083,59 @@ module.exports.FileUploadOptions = { default: ['^(?![xXsS]?[hH][tT][mM][lL]?$)'], }, }; +/* The available log levels for Parse Server logging. Valid values are:
- `'error'` - Error level (highest priority)
- `'warn'` - Warning level
- `'info'` - Info level (default)
- `'verbose'` - Verbose level
- `'debug'` - Debug level
- `'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 = { appName: { 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.', 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: { env: 'PARSE_SERVER_DATABASE_MAX_CONNECTING', help: @@ -1403,30 +1462,32 @@ module.exports.AuthAdapter = { module.exports.LogLevels = { cloudFunctionError: { 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', }, cloudFunctionSuccess: { 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', }, triggerAfter: { env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_AFTER', 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', }, triggerBeforeError: { env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_ERROR', 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', }, triggerBeforeSuccess: { env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_SUCCESS', 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', }, }; diff --git a/src/Options/docs.js b/src/Options/docs.js index ade153cdb3..d3e1f258d9 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -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.

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.

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 * @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 {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 {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} 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. @@ -303,9 +321,9 @@ /** * @interface LogLevels - * @property {String} cloudFunctionError Log level used by the Cloud Code Functions on error. Default is `error`. - * @property {String} cloudFunctionSuccess Log level used by the Cloud Code Functions on success. Default is `info`. - * @property {String} triggerAfter Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`. - * @property {String} triggerBeforeError Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`. - * @property {String} triggerBeforeSuccess Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`. + * @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`. 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`. 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`. 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`. See [LogLevel](LogLevel.html) for available values. */ diff --git a/src/Options/index.js b/src/Options/index.js index 35b5821987..090dba62e7 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -608,6 +608,32 @@ export interface FileUploadOptions { enableForPublic: ?boolean; } +/* The available log levels for Parse Server logging. Valid values are:
- `'error'` - Error level (highest priority)
- `'warn'` - Warning level
- `'info'` - Info level (default)
- `'verbose'` - Verbose level
- `'debug'` - Debug level
- `'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 { /* 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 */ @@ -725,6 +751,8 @@ export interface DatabaseOptions { 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. */ disableIndexFieldValidation: ?boolean; + /* An array of MongoDB client event configurations to enable logging of specific events. */ + logClientEvents: ?(LogClientEvent[]); } export interface AuthAdapter { @@ -736,23 +764,23 @@ export interface AuthAdapter { } 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 */ 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 */ 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 */ 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 */ 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 */ cloudFunctionError: ?string; diff --git a/src/Utils.js b/src/Utils.js index 72b49aeeb2..f46e09de1e 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -410,6 +410,40 @@ class Utils { '%' + 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;