diff --git a/docs/blog/assets/version-4.1-is-here/banner.png b/docs/blog/assets/version-4.1-is-here/banner.png new file mode 100644 index 0000000000..555415de32 Binary files /dev/null and b/docs/blog/assets/version-4.1-is-here/banner.png differ diff --git a/docs/blog/version-4.1-release-notes.md b/docs/blog/version-4.1-release-notes.md new file mode 100644 index 0000000000..9749d5e6ba --- /dev/null +++ b/docs/blog/version-4.1-release-notes.md @@ -0,0 +1,49 @@ +--- +title: Version 4.1 release notes +author: Loïc Poullain +author_title: Creator of FoalTS. Software engineer. +author_url: https://loicpoullain.com +author_image_url: https://avatars1.githubusercontent.com/u/13604533?v=4 +image: blog/twitter-banners/version-4.1-release-notes.png +tags: [release] +--- + +![Banner](./assets/version-4.1-is-here/banner.png) + +Version 4.1 of [Foal](https://foalts.org/) is out! + + + +## Better logging + +Foal now features a true logging system. Full documentation can be found [here](../docs/common/logging). + +### New recommended configuration + +It is recommended to switch to this configuration to take full advantage of the new logging system. + +*config/default.json* +```json +{ + "settings": { + "loggerFormat": "foal" + } +} +``` + +*config/development.json* +```json +{ + "settings": { + "logger": { + "format": "dev" + } + } +} +``` + +## Request IDs + +On each request, a request ID is now generated randomly. It can be read through `ctx.request.id`. + +If the `X-Request-ID` header exists in the request, then the header value is used as the request identifier. diff --git a/docs/docs/common/images/dev-format.png b/docs/docs/common/images/dev-format.png new file mode 100644 index 0000000000..78a18bf843 Binary files /dev/null and b/docs/docs/common/images/dev-format.png differ diff --git a/docs/docs/common/images/json-format.png b/docs/docs/common/images/json-format.png new file mode 100644 index 0000000000..872f0393cd Binary files /dev/null and b/docs/docs/common/images/json-format.png differ diff --git a/docs/docs/common/images/raw-format.png b/docs/docs/common/images/raw-format.png new file mode 100644 index 0000000000..8a57629953 Binary files /dev/null and b/docs/docs/common/images/raw-format.png differ diff --git a/docs/docs/common/logging.md b/docs/docs/common/logging.md index 2f068f009d..f45302656f 100644 --- a/docs/docs/common/logging.md +++ b/docs/docs/common/logging.md @@ -2,259 +2,282 @@ title: Logging --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; +Foal provides an advanced built-in logger. This page shows how to use it. +## Recommended Configuration -## HTTP Request Logging - -FoalTS uses [morgan](https://www.npmjs.com/package/morgan) to log the HTTP requests. You can specify the output format using the environment variable `SETTINGS_LOGGER_FORMAT` or the `config/default.json` file: - - - - -```yaml -settings: - loggerFormat: tiny +*config/default.json* +```json +{ + "settings": { + "loggerFormat": "foal" + } +} ``` - - - +*config/development.json* ```json { "settings": { - "loggerFormat": "tiny" + "logger": { + "format": "dev" + } } } ``` - - +## Accessing and Using the Logger -```javascript -module.exports = { - settings: { - loggerFormat: "tiny" +To log a message anywhere in the application, you can inject the `Logger` service and use its `info` method. This methods takes two parameters: +- a required `message` string, +- and an optional `params` object if you wish to add additional data to the log. + +*Example with a controller* +```typescript +import { dependency, Logger, Post } from '@foal/core'; + +export class AuthController { + @dependency + logger: Logger; + + @Post('/signup') + signup() { + ... + this.logger.info('Someone signed up!'); } + } ``` - - +*Example with a hook* +```typescript +import { Hook, Logger } from '@foal/core'; -## Disabling HTTP Request Logging +export function LogUserId() { + return Hook((ctx, services) => { + const logger = services.get(Logger); + logger.info(`Logging user ID`, { userId: ctx.user.id }); + }); +} +``` -In some scenarios and environments, you might want to disable http request logging. You can achieve this by setting the `loggerFormat` configuration option to `none`. +## Levels of Logs - - +The logger supports four levels of logs: +- the `debug` level which is commonly used to log debugging data, +- the `info` level which logs informative data, +- the `warn` level which logs data that requires attention, +- and the `error` level which logs errors. -```yaml -settings: - loggerFormat: none +*Examples* +```typescript +this.logger.debug('This a debug message'); +this.logger.info('This an info message'); +this.logger.warn('This a warn message'); +this.logger.error('This an error message'); + +this.logger.log('debug', 'This a debug message'); ``` - - +By default, only the `info`, `warn` and `error` messages are logged in the console. If you wish to log all messages, you can update your configuration as follows: ```json { "settings": { - "loggerFormat": "none" + "logger": { + "logLevel": "DEBUG" + } } } ``` - - +| Value of `settings.logger.logLevel` | Levels of logs displayed | +| --- | --- | +| `DEBUG` | error, warn, info, debug | +| `INFO` | error, warn, info | +| `WARN` | error, warn | +| `ERROR` | error | + +## Log Ouput Formats + +Foal's logger lets you log your messages in three different ways: `raw` (default), `dev` and `json`. -```javascript -module.exports = { - settings: { - loggerFormat: "none" +*Example of configuration* +```json +{ + "settings": { + "logger": { + "format": "json" + } } } ``` - - +### The `dev` format + +With this format, the logged output contains a small timestamp, beautiful colors and the message. The logger also displays an `error` if one is passed as parameter and it prettifies the HTTP request logs. -## Disabling Error Logging +This format is adapted to a development environment and focuses on reducing noise. -In some scenarios, you might want to disable error logging (error stack traces that are displayed when an error is thrown in a controller or hook). You can achieve this by setting the `allErrors` configuration option to false. +![dev format](./images/dev-format.png) - - +### The `raw` format -```yaml -settings: - allErrors: false +This format aims to log much more information and is suitable for a production environment. + +The output contains a complete time stamp, the log level, the message and all parameters passed to the logger if any. + +![raw format](./images/raw-format.png) + +### The `json` format + +Similar to the `raw` one, this format prints the same information except that it is displayed with a JSON. This format is useful if you need to diggest the logs with another log tool (such as an aggregator for example). + +![raw format](./images/json-format.png) + +### Hiding logs: the `none` format + +If you wish to completly mask logs, you can use the `none` format. + +## HTTP Request Logging + +Each request received by Foal is logged with the INFO level. + +With the configuration key `settings.loggerFormat` set to `"foal"`, the messages start with `HTTP request -` and end with the request method and URL. The log parameters include the response status code and content length as well as the response time and the request method and URL. + +> Note: the query parameters are not logged to avoid logging sensitive data (such as an API key). + +### Adding other parameters to the logs + +If the default logged HTTP parameters are not sufficient in your case, you can extend them with the option `getHttpLogParams` in `createApp`: + +```typescript +import { createApp, getHttpLogParamsDefault } from '@foal/core'; + +const app = await createApp({ + getHttpLogParams: (tokens, req, res) => ({ + ...getHttpLogParamsDefault(tokens, req, res), + myCustomHeader: req.get('my-custom-header'), + }) +}) ``` - - +### Formatting the log message (deprecated) + +If you wish to customize the HTTP log messages, you can set the value of the `loggerFormat.loggerFormat` configuration to a format supported by [morgan](https://www.npmjs.com/package/morgan). With this technique, no parameters will be logged though. ```json { "settings": { - "allErrors": false + "loggerFormat": "tiny" } } ``` - - +### Disabling HTTP Request Logging -```javascript -module.exports = { - settings: { - allErrors: false +In some scenarios and environments, you might want to disable HTTP request logging. You can achieve this by setting the `loggerFormat` configuration option to `none`. + +```json +{ + "settings": { + "loggerFormat": "none" } } ``` - - +## Error Logging -## Logging Hook +When an error is thrown (or rejected) in a hook, controller or service and is not caught, the error is logged using the `Logger.error` method. -FoalTS provides a convenient hook for logging debug messages: `Log(message: string, options: LogOptions = {})`. +### Disabling Error Logging -```typescript -interface LogOptions { - body?: boolean; - params?: boolean; - headers?: string[]|boolean; - query?: boolean; -} -``` +In some scenarios, you might want to disable error logging. You can achieve this by setting the `allErrors` configuration option to false. -*Example:* -```typescript -import { Get, HttpResponseOK, Log } from '@foal/core'; - -@Log('AppController', { - body: true, - headers: [ 'X-CSRF-Token' ], - params: true, - query: true -}) -export class AppController { - @Get() - index() { - return new HttpResponseOK(); +```json +{ + "settings": { + "allErrors": false } } ``` -## Advanced Logging +## Log correlation (by HTTP request, user ID, etc) -If you need advanced logging, you might be interested in using [winston](https://www.npmjs.com/package/winston), a popular logger in the Node.js ecosystem. +When logs are generated in large quantities, we often like to aggregate them by request or user. This can be done using Foal's log context. -Here's an example on how to use it with Foal: +When receiving a request, Foal adds the request ID to the logger context. On each subsequent call to the logger, it will behave as if the request ID had been passed as a parameter. -*LoggerService* +*Example* ```typescript -import * as winston from 'winston'; - -export class LoggerService { - private logger: any; - - constructor() { - this.logger = winston.createLogger({ - transports: [ - new winston.transports.Console(), - new winston.transports.File({ - filename: 'logs.txt' - }) - ] - }); - } +export class AppController { + @dependency + logger: Logger; - info(msg: string) { - this.logger.info(msg); - } + @Get('/foo') + getFoo(ctx: Context) { + this.logger.info('Hello world'); + // equivalent to this.logger.info('Hello world', { requestId: ctx.request.id }); - warn(msg: string) { - this.logger.warn(msg); + setTimeout(() => { + this.logger.info('Hello world'); + // equivalent to this.logger.info('Hello world', { requestId: ctx.request.id }); + }, 1000) + return new HttpResponseOK(); } +} +``` - error(msg: string) { - this.logger.error(msg); - } +In the same way, the authentification hooks `@JWTRequired`, `@JWTOptional` and `@UseSessions` will add the `userId` (if any) to the logger context. -} +This mecanism helps filter logs of a specific request or specific user in a logging tool. +If needed, you call also add manually custom parameters to the logger context with this fonction: +```typescript +logger.addLogContext('myKey', 'myValue'); ``` -*LogUserId hook* -```typescript -import { Hook } from '@foal/core'; -// LoggerService import. +## Transports -export function LogUserId() { - return Hook((ctx, services) => { - const logger = services.get(LoggerService); - logger.info(`UserId is: ${ctx.user.id}`); - }); -} +All logs are printed using the `console.log` function. -``` +If you also wish to consume the logs in another way (for example, to send them to a third-party error-tracking or logging tool), you can add one or more transports to the logger: -*ProductController* ```typescript -import { Get } from '@foal/core'; -// LogUserId import. +logger.addTransport((level: 'debug'|'warn'|'info'|'error', log: string) => { + // Do something +}) +``` -export class ProductController { +## Logging Hook (deprecated) - @Get('/') - @LogUserId() - readProducts() { - ... - } +> This hook is deprecated and will be removed in a next release. Use the `Logger` service in a custom hook instead. -} +FoalTS provides a convenient hook for logging debug messages: `Log(message: string, options: LogOptions = {})`. +```typescript +interface LogOptions { + body?: boolean; + params?: boolean; + headers?: string[]|boolean; + query?: boolean; +} ``` -*AuthController* +*Example:* ```typescript -import { dependency, Post } from '@foal/core'; -// LoggerService import. - -export class AuthController { - @dependency - logger: LoggerService; +import { Get, HttpResponseOK, Log } from '@foal/core'; - @Post('/signup') - signup() { - ... - this.logger.info('Someone signed up!'); +@Log('AppController', { + body: true, + headers: [ 'X-CSRF-Token' ], + params: true, + query: true +}) +export class AppController { + @Get() + index() { + return new HttpResponseOK(); } - } - -``` \ No newline at end of file +``` diff --git a/docs/i18n/fr/docusaurus-plugin-content-docs/current/architecture/controllers.md b/docs/i18n/fr/docusaurus-plugin-content-docs/current/architecture/controllers.md index 5030eb6d06..e5e8dfa971 100644 --- a/docs/i18n/fr/docusaurus-plugin-content-docs/current/architecture/controllers.md +++ b/docs/i18n/fr/docusaurus-plugin-content-docs/current/architecture/controllers.md @@ -250,6 +250,11 @@ class AppController { > In order to use signed cookies, you must provide a secret with the configuration key `settings.cookieParser.secret`. +#### Read the request ID + +On each request, a request ID is generated randomly. It can be read through `ctx.request.id`. + +If the `X-Request-ID` header exists, then the header value is used as the request identifier. #### The Controller Method Arguments diff --git a/docs/src/pages/who-is-using-foal.jsx b/docs/src/pages/who-is-using-foal.jsx index c0f4a5b536..cce16bd27e 100644 --- a/docs/src/pages/who-is-using-foal.jsx +++ b/docs/src/pages/who-is-using-foal.jsx @@ -14,7 +14,7 @@ const posts = [ }, ]; -export default function Newsletter() { +export default function WhoIsUsingFoal() { return ( diff --git a/docs/static/blog/twitter-banners/version-4.1-release-notes.png b/docs/static/blog/twitter-banners/version-4.1-release-notes.png new file mode 100644 index 0000000000..c742d8947b Binary files /dev/null and b/docs/static/blog/twitter-banners/version-4.1-release-notes.png differ diff --git a/packages/acceptance-tests/package-lock.json b/packages/acceptance-tests/package-lock.json index edd5dcd273..6988816512 100644 --- a/packages/acceptance-tests/package-lock.json +++ b/packages/acceptance-tests/package-lock.json @@ -31,7 +31,7 @@ }, "devDependencies": { "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "@types/react": "18.2.21", "@types/react-dom": "18.2.7", "@types/supertest": "2.0.12", @@ -387,9 +387,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==" }, "node_modules/@types/prop-types": { "version": "15.7.5", @@ -4577,9 +4577,9 @@ "dev": true }, "@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==" }, "@types/prop-types": { "version": "15.7.5", diff --git a/packages/acceptance-tests/package.json b/packages/acceptance-tests/package.json index 9e9e2a657d..8144344cda 100644 --- a/packages/acceptance-tests/package.json +++ b/packages/acceptance-tests/package.json @@ -48,7 +48,7 @@ }, "devDependencies": { "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "@types/react": "18.2.21", "@types/react-dom": "18.2.7", "@types/supertest": "2.0.12", @@ -56,4 +56,4 @@ "ts-node": "~10.9.1", "typescript": "~4.9.5" } -} +} \ No newline at end of file diff --git a/packages/acceptance-tests/src/additional/authentication/jwt.jwks.spec.ts b/packages/acceptance-tests/src/additional/authentication/jwt.jwks.spec.ts index 76a5009cc3..0198edecbf 100644 --- a/packages/acceptance-tests/src/additional/authentication/jwt.jwks.spec.ts +++ b/packages/acceptance-tests/src/additional/authentication/jwt.jwks.spec.ts @@ -68,17 +68,13 @@ describe('[Authentication|JWT|JWKS] Users can be authenticated with a JWKS retre server = (await createApp(AppController)).listen(3000); - try { - const response = await superagent - .get('http://localhost:3000/api/users/me') - .set('Authorization', 'Bearer ' + sign({}, privateKey, { algorithm: 'RS256', header: { kid: 'aaa' } })); - deepStrictEqual(response.body, { - name: 'Alix' - }); - } catch (error: any) { - console.log(error); - throw error; - } + const response = await superagent + .get('http://localhost:3000/api/users/me') + .set('Authorization', 'Bearer ' + sign({}, privateKey, { algorithm: 'RS256', header: { kid: 'aaa' } })); + + deepStrictEqual(response.body, { + name: 'Alix' + }); }); }); diff --git a/packages/aws-s3/package-lock.json b/packages/aws-s3/package-lock.json index f0d5913838..4db9d6b482 100644 --- a/packages/aws-s3/package-lock.json +++ b/packages/aws-s3/package-lock.json @@ -14,7 +14,7 @@ }, "devDependencies": { "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "copy": "~0.3.2", "mocha": "~10.2.0", "rimraf": "~5.0.1", @@ -1535,9 +1535,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "node_modules/acorn": { @@ -5018,9 +5018,9 @@ "dev": true }, "@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "acorn": { diff --git a/packages/aws-s3/package.json b/packages/aws-s3/package.json index 0810431a7e..bd7b015b38 100644 --- a/packages/aws-s3/package.json +++ b/packages/aws-s3/package.json @@ -50,11 +50,11 @@ }, "devDependencies": { "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "copy": "~0.3.2", "mocha": "~10.2.0", "rimraf": "~5.0.1", "ts-node": "~10.9.1", "typescript": "~4.9.5" } -} +} \ No newline at end of file diff --git a/packages/cli/package-lock.json b/packages/cli/package-lock.json index 15b7c9808b..843dad19eb 100644 --- a/packages/cli/package-lock.json +++ b/packages/cli/package-lock.json @@ -21,7 +21,7 @@ }, "devDependencies": { "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "copyfiles": "~2.4.1", "mocha": "~10.2.0", "rimraf": "~5.0.1", @@ -209,9 +209,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "node_modules/acorn": { @@ -1936,9 +1936,9 @@ "dev": true }, "@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "acorn": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 46317d5533..7a907792e1 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -66,11 +66,11 @@ }, "devDependencies": { "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "copyfiles": "~2.4.1", "mocha": "~10.2.0", "rimraf": "~5.0.1", "ts-node": "~10.9.1", "typescript": "~4.9.5" } -} +} \ No newline at end of file diff --git a/packages/cli/src/generate/specs/app/config/default.json b/packages/cli/src/generate/specs/app/config/default.json index 23b9f6e127..89e6cd04c8 100644 --- a/packages/cli/src/generate/specs/app/config/default.json +++ b/packages/cli/src/generate/specs/app/config/default.json @@ -1,7 +1,7 @@ { "port": "env(PORT)", "settings": { - "loggerFormat": "tiny", + "loggerFormat": "foal", "session": { "store": "@foal/typeorm" } diff --git a/packages/cli/src/generate/specs/app/config/default.mongodb.json b/packages/cli/src/generate/specs/app/config/default.mongodb.json index cbe36e70aa..1e975a3cd8 100644 --- a/packages/cli/src/generate/specs/app/config/default.mongodb.json +++ b/packages/cli/src/generate/specs/app/config/default.mongodb.json @@ -1,10 +1,10 @@ { "port": "env(PORT)", "settings": { - "loggerFormat": "tiny" + "loggerFormat": "foal" }, "database": { "type": "mongodb", "url": "mongodb://localhost:27017/test-foo-bar" } -} +} \ No newline at end of file diff --git a/packages/cli/src/generate/specs/app/config/default.mongodb.yml b/packages/cli/src/generate/specs/app/config/default.mongodb.yml index ae86ee05bc..33bd271229 100644 --- a/packages/cli/src/generate/specs/app/config/default.mongodb.yml +++ b/packages/cli/src/generate/specs/app/config/default.mongodb.yml @@ -1,7 +1,7 @@ port: env(PORT) settings: - loggerFormat: tiny + loggerFormat: foal database: type: mongodb diff --git a/packages/cli/src/generate/specs/app/config/default.yml b/packages/cli/src/generate/specs/app/config/default.yml index 664a2b41f7..b8d452a4a6 100644 --- a/packages/cli/src/generate/specs/app/config/default.yml +++ b/packages/cli/src/generate/specs/app/config/default.yml @@ -1,7 +1,7 @@ port: env(PORT) settings: - loggerFormat: tiny + loggerFormat: foal session: store: '@foal/typeorm' diff --git a/packages/cli/src/generate/specs/app/config/development.json b/packages/cli/src/generate/specs/app/config/development.json index 11141fbb0f..dbf4a581d8 100644 --- a/packages/cli/src/generate/specs/app/config/development.json +++ b/packages/cli/src/generate/specs/app/config/development.json @@ -1,6 +1,8 @@ { "settings": { "debug": true, - "loggerFormat": "dev" + "logger": { + "format": "dev" + } } -} +} \ No newline at end of file diff --git a/packages/cli/src/generate/specs/app/config/development.yml b/packages/cli/src/generate/specs/app/config/development.yml index b7f97731c5..259f5fb0fa 100644 --- a/packages/cli/src/generate/specs/app/config/development.yml +++ b/packages/cli/src/generate/specs/app/config/development.yml @@ -1,3 +1,4 @@ settings: debug: true - loggerFormat: dev \ No newline at end of file + logger: + format: dev \ No newline at end of file diff --git a/packages/cli/src/generate/specs/app/config/e2e.json b/packages/cli/src/generate/specs/app/config/e2e.json index ed22ed8551..3aa0c9d436 100644 --- a/packages/cli/src/generate/specs/app/config/e2e.json +++ b/packages/cli/src/generate/specs/app/config/e2e.json @@ -7,4 +7,4 @@ "dropSchema": true, "synchronize": true } -} +} \ No newline at end of file diff --git a/packages/cli/src/generate/specs/app/package.json b/packages/cli/src/generate/specs/app/package.json index 8be1b7d30d..6de2dfece4 100644 --- a/packages/cli/src/generate/specs/app/package.json +++ b/packages/cli/src/generate/specs/app/package.json @@ -33,7 +33,7 @@ "devDependencies": { "@foal/cli": "^4.0.0", "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "concurrently": "~8.2.1", "mocha": "~10.2.0", "supertest": "~6.3.3", diff --git a/packages/cli/src/generate/specs/app/package.mongodb.json b/packages/cli/src/generate/specs/app/package.mongodb.json index 1eb5a87c29..56396ffb66 100644 --- a/packages/cli/src/generate/specs/app/package.mongodb.json +++ b/packages/cli/src/generate/specs/app/package.mongodb.json @@ -29,7 +29,7 @@ "devDependencies": { "@foal/cli": "^4.0.0", "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "concurrently": "~8.2.1", "mocha": "~10.2.0", "supertest": "~6.3.3", diff --git a/packages/cli/src/generate/specs/app/package.mongodb.yaml.json b/packages/cli/src/generate/specs/app/package.mongodb.yaml.json index d865bf872a..05b62efa16 100644 --- a/packages/cli/src/generate/specs/app/package.mongodb.yaml.json +++ b/packages/cli/src/generate/specs/app/package.mongodb.yaml.json @@ -30,7 +30,7 @@ "devDependencies": { "@foal/cli": "^4.0.0", "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "concurrently": "~8.2.1", "mocha": "~10.2.0", "supertest": "~6.3.3", diff --git a/packages/cli/src/generate/specs/app/package.yaml.json b/packages/cli/src/generate/specs/app/package.yaml.json index a8b15c7523..a82e4f1488 100644 --- a/packages/cli/src/generate/specs/app/package.yaml.json +++ b/packages/cli/src/generate/specs/app/package.yaml.json @@ -34,7 +34,7 @@ "devDependencies": { "@foal/cli": "^4.0.0", "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "concurrently": "~8.2.1", "mocha": "~10.2.0", "supertest": "~6.3.3", diff --git a/packages/cli/src/generate/templates/app/config/default.json b/packages/cli/src/generate/templates/app/config/default.json index 23b9f6e127..89e6cd04c8 100644 --- a/packages/cli/src/generate/templates/app/config/default.json +++ b/packages/cli/src/generate/templates/app/config/default.json @@ -1,7 +1,7 @@ { "port": "env(PORT)", "settings": { - "loggerFormat": "tiny", + "loggerFormat": "foal", "session": { "store": "@foal/typeorm" } diff --git a/packages/cli/src/generate/templates/app/config/default.mongodb.json b/packages/cli/src/generate/templates/app/config/default.mongodb.json index 1f1a4f7734..f0e91cf5e5 100644 --- a/packages/cli/src/generate/templates/app/config/default.mongodb.json +++ b/packages/cli/src/generate/templates/app/config/default.mongodb.json @@ -1,10 +1,10 @@ { "port": "env(PORT)", "settings": { - "loggerFormat": "tiny" + "loggerFormat": "foal" }, "database": { "type": "mongodb", "url": "mongodb://localhost:27017//* kebabName */" } -} +} \ No newline at end of file diff --git a/packages/cli/src/generate/templates/app/config/default.mongodb.yml b/packages/cli/src/generate/templates/app/config/default.mongodb.yml index 88d0e5b673..2afcb89e21 100644 --- a/packages/cli/src/generate/templates/app/config/default.mongodb.yml +++ b/packages/cli/src/generate/templates/app/config/default.mongodb.yml @@ -1,7 +1,7 @@ port: env(PORT) settings: - loggerFormat: tiny + loggerFormat: foal database: type: mongodb diff --git a/packages/cli/src/generate/templates/app/config/default.yml b/packages/cli/src/generate/templates/app/config/default.yml index 664a2b41f7..b8d452a4a6 100644 --- a/packages/cli/src/generate/templates/app/config/default.yml +++ b/packages/cli/src/generate/templates/app/config/default.yml @@ -1,7 +1,7 @@ port: env(PORT) settings: - loggerFormat: tiny + loggerFormat: foal session: store: '@foal/typeorm' diff --git a/packages/cli/src/generate/templates/app/config/development.json b/packages/cli/src/generate/templates/app/config/development.json index 11141fbb0f..dbf4a581d8 100644 --- a/packages/cli/src/generate/templates/app/config/development.json +++ b/packages/cli/src/generate/templates/app/config/development.json @@ -1,6 +1,8 @@ { "settings": { "debug": true, - "loggerFormat": "dev" + "logger": { + "format": "dev" + } } -} +} \ No newline at end of file diff --git a/packages/cli/src/generate/templates/app/config/development.yml b/packages/cli/src/generate/templates/app/config/development.yml index b7f97731c5..259f5fb0fa 100644 --- a/packages/cli/src/generate/templates/app/config/development.yml +++ b/packages/cli/src/generate/templates/app/config/development.yml @@ -1,3 +1,4 @@ settings: debug: true - loggerFormat: dev \ No newline at end of file + logger: + format: dev \ No newline at end of file diff --git a/packages/cli/src/generate/templates/app/config/e2e.json b/packages/cli/src/generate/templates/app/config/e2e.json index ed22ed8551..3aa0c9d436 100644 --- a/packages/cli/src/generate/templates/app/config/e2e.json +++ b/packages/cli/src/generate/templates/app/config/e2e.json @@ -7,4 +7,4 @@ "dropSchema": true, "synchronize": true } -} +} \ No newline at end of file diff --git a/packages/cli/src/generate/templates/app/package.json b/packages/cli/src/generate/templates/app/package.json index 6129f3d0fd..7b4fea82d9 100644 --- a/packages/cli/src/generate/templates/app/package.json +++ b/packages/cli/src/generate/templates/app/package.json @@ -33,7 +33,7 @@ "devDependencies": { "@foal/cli": "^4.0.0", "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "concurrently": "~8.2.1", "mocha": "~10.2.0", "supertest": "~6.3.3", diff --git a/packages/cli/src/generate/templates/app/package.mongodb.json b/packages/cli/src/generate/templates/app/package.mongodb.json index ab49726db1..c27bf56cc2 100644 --- a/packages/cli/src/generate/templates/app/package.mongodb.json +++ b/packages/cli/src/generate/templates/app/package.mongodb.json @@ -29,7 +29,7 @@ "devDependencies": { "@foal/cli": "^4.0.0", "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "concurrently": "~8.2.1", "mocha": "~10.2.0", "supertest": "~6.3.3", diff --git a/packages/core/package-lock.json b/packages/core/package-lock.json index 81eb506e35..5e9360ed64 100644 --- a/packages/core/package-lock.json +++ b/packages/core/package-lock.json @@ -18,7 +18,7 @@ }, "devDependencies": { "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "@types/supertest": "2.0.12", "ajv-errors": "~3.0.0", "ejs": "~3.1.9", @@ -230,9 +230,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "node_modules/@types/superagent": { @@ -2770,9 +2770,9 @@ "dev": true }, "@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "@types/superagent": { diff --git a/packages/core/package.json b/packages/core/package.json index 468a8561e5..1ce7355c4a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -69,7 +69,7 @@ "devDependencies": { "@foal/internal-test": "^4.0.0", "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "@types/supertest": "2.0.12", "ajv-errors": "~3.0.0", "ejs": "~3.1.9", @@ -82,4 +82,4 @@ "typescript": "~4.9.5", "yamljs": "~0.3.0" } -} +} \ No newline at end of file diff --git a/packages/core/src/common/index.ts b/packages/core/src/common/index.ts index 0bb0e9d37b..61e6d65bdb 100644 --- a/packages/core/src/common/index.ts +++ b/packages/core/src/common/index.ts @@ -1,6 +1,7 @@ export * from './auth'; export * from './encoding'; export * from './file'; +export * from './logging'; export * from './templates'; export * from './tokens'; export * from './utils'; diff --git a/packages/core/src/common/logging/index.ts b/packages/core/src/common/logging/index.ts new file mode 100644 index 0000000000..fc055d66fc --- /dev/null +++ b/packages/core/src/common/logging/index.ts @@ -0,0 +1,2 @@ +export { Logger } from './logger'; +export { httpRequestMessagePrefix } from './logger.utils'; \ No newline at end of file diff --git a/packages/core/src/common/logging/logger.spec.ts b/packages/core/src/common/logging/logger.spec.ts new file mode 100644 index 0000000000..645551eeaf --- /dev/null +++ b/packages/core/src/common/logging/logger.spec.ts @@ -0,0 +1,243 @@ +// std +import { notStrictEqual, strictEqual } from 'assert'; +import { mock } from 'node:test'; + +// FoalTS +import { Logger } from './logger'; +import { Config } from '../../core'; + +describe('Logger', () => { + + afterEach(() => { + mock.reset(); + Config.remove('settings.logger.format'); + Config.remove('settings.logger.logLevel'); + }) + + describe('has a "log" method that', () => { + + context('given the log context has been initialized', () => { + it('should log the message with the context.', () => { + const consoleMock = mock.method(console, 'log', () => {}).mock; + + const logger = new Logger(); + logger.initLogContext(() => { + logger.addLogContext('foo', 'bar'); + logger.log('error', 'Hello world', {}); + }); + logger.initLogContext(() => { + logger.addLogContext('foo2', 'bar2'); + logger.log('error', 'Hello world 2', {}); + }); + + strictEqual(consoleMock.callCount(), 2); + + const loggedMessage = consoleMock.calls[0].arguments[0]; + + strictEqual(loggedMessage.includes('[ERROR]'), true); + strictEqual(loggedMessage.includes('foo: "bar"'), true); + notStrictEqual(loggedMessage.includes('foo2: "bar2"'), true); + + const loggedMessage2 = consoleMock.calls[1].arguments[0]; + + strictEqual(loggedMessage2.includes('[ERROR]'), true); + strictEqual(loggedMessage2.includes('foo2: "bar2"'), true); + notStrictEqual(loggedMessage2.includes('foo: "bar"'), true); + }); + + it('should let given params override the context.', () => { + const consoleMock = mock.method(console, 'log', () => {}).mock; + + const logger = new Logger(); + logger.initLogContext(() => { + logger.addLogContext('foo', 'bar'); + logger.log('error', 'Hello world', { foo: 'bar2' }); + }); + + strictEqual(consoleMock.callCount(), 1); + + const loggedMessage = consoleMock.calls[0].arguments[0]; + + strictEqual(loggedMessage.includes('[ERROR]'), true); + strictEqual(loggedMessage.includes('foo: "bar2"'), true); + }); + }); + + context('given the log context has NOT been initialized', () => { + it('should log a warning message when adding log context.', () => { + const consoleMock = mock.method(console, 'log', () => {}).mock; + + const logger = new Logger(); + logger.addLogContext('foo', 'bar'); + + strictEqual(consoleMock.callCount(), 1); + + const loggedMessage = consoleMock.calls[0].arguments[0]; + + strictEqual(loggedMessage.includes('[WARN]'), true); + strictEqual( + loggedMessage.includes('Impossible to add log context information. The logger context has not been initialized.'), + true + ); + }); + }); + + context('given the configuration "settings.logger.format" is NOT defined', () => { + it('should log the message to a raw text.', () => { + const consoleMock = mock.method(console, 'log', () => {}).mock; + + const logger = new Logger(); + logger.log('error', 'Hello world', { foobar: 'bar' }); + + strictEqual(consoleMock.callCount(), 1); + + const loggedMessage = consoleMock.calls[0].arguments[0]; + + strictEqual(loggedMessage.includes('[ERROR]'), true); + strictEqual(loggedMessage.includes('foobar: "bar"'), true); + }) + }); + + context('given the configuration "settings.logger.format" is "json"', () => { + it('should log the message to a JSON.', () => { + Config.set('settings.logger.format', 'json'); + + const consoleMock = mock.method(console, 'log', () => {}).mock; + + const logger = new Logger(); + logger.log('error', 'Hello world', { foobar: 'bar' }); + + strictEqual(consoleMock.callCount(), 1); + + const loggedMessage = consoleMock.calls[0].arguments[0]; + const json = JSON.parse(loggedMessage); + + strictEqual(json.level, 'error'); + strictEqual(json.foobar, 'bar'); + }); + }); + + context('given the configuration "settings.logger.format" is "none"', () => { + it('should log nothing.', () => { + Config.set('settings.logger.format', 'none'); + + const consoleMock = mock.method(console, 'log', () => {}).mock; + + const logger = new Logger(); + logger.log('error', 'Hello world', {}); + + strictEqual(consoleMock.callCount(), 0); + }); + }); + + context('given the configuration "settings.logger.logLevel" is NOT defined', () => { + it('should log messages based on an "INFO" log level', () => { + const consoleMock = mock.method(console, 'log', () => {}).mock; + + const logger = new Logger(); + logger.log('info', 'Hello world', {}); + strictEqual(consoleMock.callCount(), 1); + + consoleMock.resetCalls(); + + logger.log('debug', 'Hello world', {}); + strictEqual(consoleMock.callCount(), 0); + }); + }); + + context('given the configuration "settings.logger.logLevel" is "warn"', () => { + it('should log messages based on a "WARN" log level', () => { + Config.set('settings.logger.logLevel', 'warn'); + + const consoleMock = mock.method(console, 'log', () => {}).mock; + + const logger = new Logger(); + logger.log('warn', 'Hello world', {}); + strictEqual(consoleMock.callCount(), 1); + + consoleMock.resetCalls(); + + logger.log('info', 'Hello world', {}); + strictEqual(consoleMock.callCount(), 0); + }); + }); + + context('given transports have been registered', () => { + it('should send them the logs and their levels.', () => { + const transport1 = mock.fn((level, log) => {}); + const transport2 = mock.fn((level, log) => {}); + + const logger = new Logger(); + logger.addTransport(transport1); + logger.addTransport(transport2); + logger.log('error', 'Hello world', {}); + + strictEqual(transport1.mock.callCount(), 1); + strictEqual(transport1.mock.calls[0].arguments[0], 'error'); + strictEqual(transport1.mock.calls[0].arguments[1].includes('Hello world'), true); + + strictEqual(transport2.mock.callCount(), 1); + strictEqual(transport2.mock.calls[0].arguments[0], 'error'); + strictEqual(transport2.mock.calls[0].arguments[1].includes('Hello world'), true); + }); + }) + }); + + it('has a debug(...args) method which is an alias for log("debug", ...args)', () => { + Config.set('settings.logger.logLevel', 'debug'); + + const logger = new Logger(); + + const consoleMock = mock.method(console, 'log', () => {}).mock; + + logger.debug('Hello world', {}); + + strictEqual(consoleMock.callCount(), 1); + strictEqual( + consoleMock.calls[0].arguments[0].includes('[DEBUG]'), + true, + ); + }); + + it('has an info(...args) method which is an alias for log("info", ...args)', () => { + const logger = new Logger(); + + const consoleMock = mock.method(console, 'log', () => {}).mock; + + logger.info('Hello world', {}); + + strictEqual(consoleMock.callCount(), 1); + strictEqual( + consoleMock.calls[0].arguments[0].includes('[INFO]'), + true, + ); + }); + + it('has a warn(...args) method which is an alias for log("warn", ...args)', () => { + const logger = new Logger(); + + const consoleMock = mock.method(console, 'log', () => {}).mock; + + logger.warn('Hello world', {}); + + strictEqual(consoleMock.callCount(), 1); + strictEqual( + consoleMock.calls[0].arguments[0].includes('[WARN]'), + true, + ); + }); + + it('has an error(...args) method which is an alias for log("error", ...args)', () => { + const logger = new Logger(); + + const consoleMock = mock.method(console, 'log', () => {}).mock; + + logger.error('Hello world', {}); + + strictEqual(consoleMock.callCount(), 1); + strictEqual( + consoleMock.calls[0].arguments[0].includes('[ERROR]'), + true, + ); + }); +}); diff --git a/packages/core/src/common/logging/logger.ts b/packages/core/src/common/logging/logger.ts new file mode 100644 index 0000000000..0ee9c253ce --- /dev/null +++ b/packages/core/src/common/logging/logger.ts @@ -0,0 +1,79 @@ +// std +import { AsyncLocalStorage } from 'node:async_hooks'; + +// FoalTS +import { Config } from '../../core'; +import { Level, formatMessage, shouldLog } from './logger.utils'; + +export class Logger { + private asyncLocalStorage = new AsyncLocalStorage>(); + private transports: ((level: Level, log: string) => void)[] = [ + (level, log) => console.log(log), + ]; + + addTransport(transport: (level: Level, log: string) => void): void { + this.transports.push(transport); + } + + initLogContext(callback: () => void): void { + this.asyncLocalStorage.run({}, callback); + } + + addLogContext(name: string, value: any): void { + const store = this.asyncLocalStorage.getStore(); + if (!store) { + this.log('warn', 'Impossible to add log context information. The logger context has not been initialized.'); + return; + } + store[name] = value; + } + + log( + level: Level, + message: string, + params: { [name: string]: any } = {} + ): void { + const format = Config.get('settings.logger.format', 'string', 'raw'); + if (format === 'none') { + return; + } + + const configLogLevel = Config.get('settings.logger.logLevel', 'string', 'info'); + if (!shouldLog(level, configLogLevel)) { + return; + }; + + const now = new Date(); + const contextParams = this.asyncLocalStorage.getStore(); + const formattedMessage = formatMessage( + level, + message, + { + ...contextParams, + ...params, + }, + format, + now, + ); + + for (const transport of this.transports) { + transport(level, formattedMessage); + } + } + + debug(message: string, params: { error?: Error, [name: string]: any } = {}): void { + this.log('debug', message, params); + } + + info(message: string, params: { error?: Error, [name: string]: any } = {}): void { + this.log('info', message, params); + } + + warn(message: string, params: { error?: Error, [name: string]: any } = {}): void { + this.log('warn', message, params); + } + + error(message: string, params: { error?: Error, [name: string]: any } = {}): void { + this.log('error', message, params); + } +} \ No newline at end of file diff --git a/packages/core/src/common/logging/logger.utils.spec.ts b/packages/core/src/common/logging/logger.utils.spec.ts new file mode 100644 index 0000000000..ff5e6bc722 --- /dev/null +++ b/packages/core/src/common/logging/logger.utils.spec.ts @@ -0,0 +1,283 @@ +import { deepStrictEqual, strictEqual, throws } from 'assert'; +import { formatMessage, shouldLog } from './logger.utils'; + +const testStack = `Error: aaa + at createTestParams (/somewhere/logger.spec.ts:6:11) + at Context. (/somewhere/logger.spec.ts:129:13)`; + +function createTestParams() { + const error = new Error('aaa'); + error.stack = testStack; + return { + myBoolean: false, + myNull: null, + myUndefined: undefined, + myNumber: 0, + myString: 'xxx', + mySymbol: Symbol('yyy'), + myObject: { foo: 'bar' }, + error + }; +} + +describe('formatMessage', () => { + const isoNow = '2023-02-03T01:12:03.000Z'; + const now = new Date(isoNow); + const localeTimeNow = now.toLocaleTimeString(); + + context('given format is "raw"', () => { + it('should return a raw text with a full timestamp, detailed params but with no colors.', () => { + const message = 'Hello world'; + const params = createTestParams(); + + const actual = formatMessage('debug', message, params, 'raw', now); + const expected = `[${isoNow}] [DEBUG] Hello world` + + `\n myBoolean: false` + + `\n myNull: null` + + `\n myNumber: 0` + + `\n myString: "xxx"` + + `\n myObject: {` + + `\n "foo": "bar"` + + `\n }` + + `\n error: {` + + `\n name: "Error"` + + `\n message: "aaa"` + + `\n stack: Error: aaa` + + `\n at createTestParams (/somewhere/logger.spec.ts:6:11)` + + `\n at Context. (/somewhere/logger.spec.ts:129:13)` + + `\n }`; + + strictEqual(actual, expected); + }); + }); + + context('given format is "dev"', () => { + context('given level is "debug"', () => { + it('should return a text with gray and pink colors, a short timestamp but with no detailed params (expect for the "error" param).', () => { + const message = 'Hello world'; + const params = createTestParams(); + + const actual = formatMessage('debug', message, params, 'dev', now) + const expected = `\u001b[90m[${localeTimeNow}]\u001b[39m \u001b[35mDEBUG\u001b[39m Hello world` + + `\n error: {` + + `\n name: "Error"` + + `\n message: "aaa"` + + `\n stack: Error: aaa` + + `\n at createTestParams (/somewhere/logger.spec.ts:6:11)` + + `\n at Context. (/somewhere/logger.spec.ts:129:13)` + + `\n }`; + + strictEqual(actual, expected); + }); + }); + + context('given level is "info"', () => { + it('should return a text with gray and cyan colors, a short timestamp but with no detailed params (expect for the "error" param).', () => { + const message = 'Hello world'; + const params = createTestParams(); + + const actual = formatMessage('info', message, params, 'dev', now) + const expected = `\u001b[90m[${localeTimeNow}]\u001b[39m \u001b[36mINFO\u001b[39m Hello world` + + `\n error: {` + + `\n name: "Error"` + + `\n message: "aaa"` + + `\n stack: Error: aaa` + + `\n at createTestParams (/somewhere/logger.spec.ts:6:11)` + + `\n at Context. (/somewhere/logger.spec.ts:129:13)` + + `\n }`; + + strictEqual(actual, expected); + }); + }); + + context('given level is "warn"', () => { + it('should return a text with gray and yellow colors, a short timestamp but with no detailed params (expect for the "error" param).', () => { + const message = 'Hello world'; + const params = createTestParams(); + + const actual = formatMessage('warn', message, params, 'dev', now) + const expected = `\u001b[90m[${localeTimeNow}]\u001b[39m \u001b[33mWARN\u001b[39m Hello world` + + `\n error: {` + + `\n name: "Error"` + + `\n message: "aaa"` + + `\n stack: Error: aaa` + + `\n at createTestParams (/somewhere/logger.spec.ts:6:11)` + + `\n at Context. (/somewhere/logger.spec.ts:129:13)` + + `\n }`; + + strictEqual(actual, expected); + }); + }); + + context('given level is "error"', () => { + it('should return a text with gray and red colors, a short timestamp but with no detailed params (expect for the "error" param).', () => { + const message = 'Hello world'; + const params = createTestParams(); + + const actual = formatMessage('error', message, params, 'dev', now) + const expected = `\u001b[90m[${localeTimeNow}]\u001b[39m \u001b[31mERROR\u001b[39m Hello world` + + `\n error: {` + + `\n name: "Error"` + + `\n message: "aaa"` + + `\n stack: Error: aaa` + + `\n at createTestParams (/somewhere/logger.spec.ts:6:11)` + + `\n at Context. (/somewhere/logger.spec.ts:129:13)` + + `\n }`; + + strictEqual(actual, expected); + }); + }); + + context('given the message is a HTTP log and is prefixed by "HTTP request -"', () => { + it('should return a text with a well-formatted message (1xx status).', () => { + const message = 'HTTP request - GET /foo/bar'; + const params = { + statusCode: 100, + responseTime: 123, + }; + + const actual = formatMessage('info', message, params, 'dev', now); + const expected = `\u001b[90m[${localeTimeNow}]\u001b[39m \u001b[36mINFO\u001b[39m GET /foo/bar 100 - 123 ms` + + strictEqual(actual, expected); + }); + + it('should return a text with a well-formatted message (2xx status).', () => { + const message = 'HTTP request - GET /foo/bar'; + const params = { + statusCode: 200, + responseTime: 123, + }; + + const actual = formatMessage('info', message, params, 'dev', now); + const expected = `\u001b[90m[${localeTimeNow}]\u001b[39m \u001b[36mINFO\u001b[39m GET /foo/bar \u001b[32m200\u001b[39m - 123 ms` + + strictEqual(actual, expected); + }); + + it('should return a text with a well-formatted message (3xx status).', () => { + const message = 'HTTP request - GET /foo/bar'; + const params = { + statusCode: 300, + responseTime: 123, + }; + + const actual = formatMessage('info', message, params, 'dev', now); + const expected = `\u001b[90m[${localeTimeNow}]\u001b[39m \u001b[36mINFO\u001b[39m GET /foo/bar \u001b[36m300\u001b[39m - 123 ms` + + strictEqual(actual, expected); + }); + + it('should return a text with a well-formatted message (4xx status).', () => { + const message = 'HTTP request - GET /foo/bar'; + const params = { + statusCode: 400, + responseTime: 123, + }; + + const actual = formatMessage('info', message, params, 'dev', now); + const expected = `\u001b[90m[${localeTimeNow}]\u001b[39m \u001b[36mINFO\u001b[39m GET /foo/bar \u001b[33m400\u001b[39m - 123 ms` + + strictEqual(actual, expected); + }); + + it('should return a text with a well-formatted message (5xx status).', () => { + const message = 'HTTP request - GET /foo/bar'; + const params = { + statusCode: 500, + responseTime: 123, + }; + + const actual = formatMessage('info', message, params, 'dev', now); + const expected = `\u001b[90m[${localeTimeNow}]\u001b[39m \u001b[36mINFO\u001b[39m GET /foo/bar \u001b[31m500\u001b[39m - 123 ms` + + strictEqual(actual, expected); + }); + }); + }); + + context('given format is "json"', () => { + it('should return a JSON string.', () => { + const message = 'Hello world'; + const params = createTestParams(); + + const actual = JSON.parse(formatMessage('debug', message, params, 'json', now) || ''); + const expected = { + message, + timestamp: isoNow, + level: 'debug', + myBoolean: false, + myNull: null, + myNumber: 0, + myString: 'xxx', + myObject: { foo: 'bar' }, + error: { + name: 'Error', + message: 'aaa', + stack: testStack, + } + }; + + deepStrictEqual(actual, expected); + }); + }); + + context('given format is invalid', () => { + it('should throw an error.', () => { + const message = 'Hello world'; + const params = createTestParams(); + + throws( + () => formatMessage('debug', message, params, 'invalid', now), + (error: any) => error.message === 'Invalid logging format: "invalid"' + ); + }); + }); +}); + +describe('shouldLog', () => { + context('given the configLogLevel is "debug"', () => { + it('should return true for all levels.', () => { + strictEqual(shouldLog('debug', 'debug'), true); + strictEqual(shouldLog('info', 'debug'), true); + strictEqual(shouldLog('warn', 'debug'), true); + strictEqual(shouldLog('error', 'debug'), true); + }); + }); + + context('given the configLogLevel is "info"', () => { + it('should return false for "debug" and true for "info", "warn" and "error".', () => { + strictEqual(shouldLog('debug', 'info'), false); + strictEqual(shouldLog('info', 'info'), true); + strictEqual(shouldLog('warn', 'info'), true); + strictEqual(shouldLog('error', 'info'), true); + }); + }); + + context('given the configLogLevel is "warn"', () => { + it('should return false for "debug" and "info" and true for "warn" and "error".', () => { + strictEqual(shouldLog('debug', 'warn'), false); + strictEqual(shouldLog('info', 'warn'), false); + strictEqual(shouldLog('warn', 'warn'), true); + strictEqual(shouldLog('error', 'warn'), true); + }); + }); + + context('given the configLogLevel is "error"', () => { + it('should return false for "debug", "info" and "warn" and true for "error".', () => { + strictEqual(shouldLog('debug', 'error'), false); + strictEqual(shouldLog('info', 'error'), false); + strictEqual(shouldLog('warn', 'error'), false); + strictEqual(shouldLog('error', 'error'), true); + }); + }); + + context('given the configLogLevel is invalid', () => { + it('should throw an error.', () => { + throws( + () => shouldLog('debug', 'invalid'), + (error: any) => error.message === 'Invalid log level: "invalid"' + ); + }); + }); +}); \ No newline at end of file diff --git a/packages/core/src/common/logging/logger.utils.ts b/packages/core/src/common/logging/logger.utils.ts new file mode 100644 index 0000000000..31446c3e53 --- /dev/null +++ b/packages/core/src/common/logging/logger.utils.ts @@ -0,0 +1,143 @@ +export type Level = 'debug'|'info'|'warn'|'error'; +export const httpRequestMessagePrefix = 'HTTP request - '; + +function formatParamsToText(params: { error?: Error, [name: string]: any }): string { + const tabLength = 2; + const offsetLength = 4; + + const tab = ' '.repeat(tabLength); + const offset = ' '.repeat(offsetLength); + + const lines: string[] = []; + + for (const key in params) { + const value = params[key]; + if (value instanceof Error) { + lines.push( + `${key}: {`, + `${tab}name: "${value.name}"`, + `${tab}message: "${value.message}"`, + `${tab}stack: ${value.stack?.split('\n').join(`\n${tab}${tab}`)}`, + `}` + ); + continue; + } + + const formattedValue = JSON.stringify(value, null, tabLength); + + if (formattedValue !== undefined) { + lines.push(...`${key}: ${formattedValue}`.split('\n')); + } + } + + return lines + .map(line => `\n${offset}${line}`) + .join(''); +} + +function formatMessageToRawText( + level: Level, + message: string, + params: { error?: Error, [name: string]: any }, + now: Date +): string { + const timestamp = `[${now.toISOString()}]`; + const logLevel = `[${level.toUpperCase()}]`; + + return `${timestamp} ${logLevel} ${message}` + formatParamsToText(params); +} + +function getColoredStatusCode(statusCode: number): string { + if (statusCode >= 500) { + return `\u001b[31m${statusCode}\u001b[39m`; + } + if (statusCode >= 400) { + return `\u001b[33m${statusCode}\u001b[39m`; + } + if (statusCode >= 300) { + return `\u001b[36m${statusCode}\u001b[39m`; + } + if (statusCode >= 200) { + return `\u001b[32m${statusCode}\u001b[39m`; + } + return statusCode.toString(); +} + +function formatMessageToDevText( + level: Level, + message: string, + params: { error?: Error, [name: string]: any }, + now: Date +): string { + const levelColorCodes: Record = { + debug: 35, + info: 36, + warn: 33, + error: 31, + }; + const timestamp = `\u001b[90m[${now.toLocaleTimeString()}]\u001b[39m`; + const logLevel = `\u001b[${levelColorCodes[level]}m${level.toUpperCase()}\u001b[39m`; + + if (message.startsWith(httpRequestMessagePrefix)) { + message = message.slice(httpRequestMessagePrefix.length); + message += ` ${getColoredStatusCode(params.statusCode)} - ${params.responseTime} ms`; + } + + return `${timestamp} ${logLevel} ${message}` + formatParamsToText({ error: params.error }); +} + +function formatMessageToJson( + level: Level, + message: string, + params: { error?: Error, [name: string]: any }, + now: Date +): string { + const json = { + timestamp: now.toISOString(), + level, + message, + ...params, + }; + + if (json.error instanceof Error) { + json.error = { + name: json.error.name, + message: json.error.message, + stack: json.error.stack, + }; + } + + return JSON.stringify(json); +} + +export function formatMessage( + level: Level, + message: string, + params: { error?: Error, [name: string]: any }, + format: string, + now: Date, +): string { + switch (format) { + case 'raw': + return formatMessageToRawText(level, message, params, now); + case 'dev': + return formatMessageToDevText(level, message, params, now); + case 'json': + return formatMessageToJson(level, message, params, now); + default: + throw new Error(`Invalid logging format: "${format}"`); + } +} + +export function shouldLog(level: Level, configLogLevel: string): boolean { + const levels: string[] = ['debug', 'info', 'warn', 'error']; + + const levelIndex = levels.indexOf(level); + const configLogLevelIndex = levels.indexOf(configLogLevel); + + if (configLogLevelIndex === -1) { + throw new Error(`Invalid log level: "${configLogLevel}"`); + } + + return levelIndex >= configLogLevelIndex; +} \ No newline at end of file diff --git a/packages/core/src/common/utils/log.hook.spec.ts b/packages/core/src/common/utils/log.hook.spec.ts index 1715f39907..5bdcdbd22c 100644 --- a/packages/core/src/common/utils/log.hook.spec.ts +++ b/packages/core/src/common/utils/log.hook.spec.ts @@ -4,6 +4,8 @@ import { strictEqual } from 'assert'; // FoalTS import { Context, getHookFunction, ServiceManager } from '../../core'; import { Log } from './log.hook'; +import { mock } from 'node:test'; +import { Logger } from '../logging'; describe('Log', () => { @@ -15,6 +17,28 @@ describe('Log', () => { logFn = (...args) => msgs.push(args); }); + afterEach(() => { + mock.reset(); + }) + + it('should log a deprecation message.', () => { + const hook = getHookFunction(Log('foo', { logFn })); + + const ctx = new Context({}); + const services = new ServiceManager(); + + const logger = services.get(Logger); + const loggerMock = mock.method(logger, 'warn').mock; + + hook(ctx, services); + + strictEqual(loggerMock.callCount(), 1); + strictEqual( + loggerMock.calls[0].arguments[0], + 'Using the @Log hook is deprecated. Use the Logger service in a custom hook instead.' + ); + }); + it('should log the message.', () => { const hook = getHookFunction(Log('foo', { logFn })); diff --git a/packages/core/src/common/utils/log.hook.ts b/packages/core/src/common/utils/log.hook.ts index 067d349ccf..871d2c3cc5 100644 --- a/packages/core/src/common/utils/log.hook.ts +++ b/packages/core/src/common/utils/log.hook.ts @@ -1,4 +1,5 @@ import { Context, Hook, HookDecorator } from '../../core'; +import { Logger } from '../logging'; /** * Options of the `Log` hook. @@ -25,13 +26,17 @@ export interface LogOptions { * Hook factory logging a message with optional information on the HTTP request. * * @export + * @deprecated Use the Logger service in a custom hook instead. * @param {string} message - The message to print on each request. * @param {LogOptions} [options={}] - Options to specify which information on the HTTP request should be printed. * @returns {HookDecorator} The hook. */ export function Log(message: string, options: LogOptions = {}): HookDecorator { const logFn = options.logFn || console.log; - return Hook((ctx: Context) => { + return Hook((ctx: Context, services) => { + const logger = services.get(Logger); + logger.warn('Using the @Log hook is deprecated. Use the Logger service in a custom hook instead.'); + logFn(message); if (options.body) { logFn('Body: ', ctx.request.body); diff --git a/packages/core/src/core/http/context.ts b/packages/core/src/core/http/context.ts index 622bdb6903..f0db4e3c1f 100644 --- a/packages/core/src/core/http/context.ts +++ b/packages/core/src/core/http/context.ts @@ -58,6 +58,9 @@ interface Request extends IncomingMessage { // This line has been added based on @types/express in order not to make the url possibly undefined. url: string; + // Extra attribute added for Foal + id: string; + // This line has been added based on @types/express but it is not present in Express official documentation. accepts(): string[]; accepts(types: string|string[]): string|false; diff --git a/packages/core/src/core/routes/convert-error-to-response.spec.ts b/packages/core/src/core/routes/convert-error-to-response.spec.ts index fe3ba303c3..b6a2f9899b 100644 --- a/packages/core/src/core/routes/convert-error-to-response.spec.ts +++ b/packages/core/src/core/routes/convert-error-to-response.spec.ts @@ -22,12 +22,20 @@ describe('convertErrorToResponse', () => { context('given the configuration settings.logErrors is true or not defined', () => { it('should log the error stack.', async () => { - let message = ''; - const log = (msg: string) => message = msg; + let actualMessage: string|undefined; + let actualError: Error|undefined; - await convertErrorToResponse(error, ctx, new AppController(), log); + const logger = { + error(message: string, { error }: { error: Error }): void { + actualMessage = message; + actualError = error; + } + } + + await convertErrorToResponse(error, ctx, new AppController(), logger); - strictEqual(message, error.stack); + strictEqual(actualMessage, error.message); + strictEqual(actualError, error); }); }); @@ -38,13 +46,21 @@ describe('convertErrorToResponse', () => { afterEach(() => Config.remove('settings.logErrors')); - it('should not log the error stack.', async () => { - let message = ''; - const log = (msg: string) => message = msg; + it('should not log the error.', async () => { + let actualMessage: string|undefined; + let actualError: Error|undefined; + + const logger = { + error(message: string, { error }: { error: Error }): void { + actualMessage = message; + actualError = error; + } + } - await convertErrorToResponse(error, ctx, new AppController(), log); + await convertErrorToResponse(error, ctx, new AppController(), logger); - strictEqual(message, ''); + strictEqual(actualMessage, undefined); + strictEqual(actualError, undefined); }); }); @@ -52,7 +68,7 @@ describe('convertErrorToResponse', () => { context('given appController.handleError is not defined', () => { it('should return an HttpResponseInternalServerError object created by renderError.', async () => { - const response = await convertErrorToResponse(error, ctx, new AppController(), () => {}); + const response = await convertErrorToResponse(error, ctx, new AppController(), { error: () => {} }); if (!isHttpResponseInternalServerError(response)) { throw new Error('An HttpResponseInternalServerError should have been returned.'); @@ -82,7 +98,7 @@ describe('convertErrorToResponse', () => { } it('should return the response returned by handleError.', async () => { - const response = await convertErrorToResponse(error, ctx, new AppController(), () => {}); + const response = await convertErrorToResponse(error, ctx, new AppController(), { error: () => {} }); if (!isHttpResponseInternalServerError(response)) { throw new Error('An HttpResponseInternalServerError should have been returned.'); @@ -102,7 +118,7 @@ describe('convertErrorToResponse', () => { } it('should return an HttpResponseInternalServerError object created by renderError.', async () => { - const response = await convertErrorToResponse(error, ctx, new AppController(), () => {}); + const response = await convertErrorToResponse(error, ctx, new AppController(), { error: () => {} }); if (!isHttpResponseInternalServerError(response)) { throw new Error('An HttpResponseInternalServerError should have been returned.'); @@ -123,7 +139,7 @@ describe('convertErrorToResponse', () => { } it('should return an HttpResponseInternalServerError object created by renderError.', async () => { - const response = await convertErrorToResponse(error, ctx, new AppController(), () => {}); + const response = await convertErrorToResponse(error, ctx, new AppController(), { error: () => {} }); if (!isHttpResponseInternalServerError(response)) { throw new Error('An HttpResponseInternalServerError should have been returned.'); diff --git a/packages/core/src/core/routes/convert-error-to-response.ts b/packages/core/src/core/routes/convert-error-to-response.ts index 04311fd7d2..27ee838057 100644 --- a/packages/core/src/core/routes/convert-error-to-response.ts +++ b/packages/core/src/core/routes/convert-error-to-response.ts @@ -4,10 +4,15 @@ import { Config } from '../config'; import { Context, HttpResponse } from '../http'; export async function convertErrorToResponse( - error: Error, ctx: Context, appController: IAppController, log = console.error + error: Error, + ctx: Context, + appController: IAppController, + logger: { + error: (message: string, params: { error: Error }) => void + } ): Promise { if (Config.get('settings.logErrors', 'boolean', true)) { - log(error.stack); + logger.error(error.message, { error }); } if (appController.handleError) { diff --git a/packages/core/src/core/routes/get-response.ts b/packages/core/src/core/routes/get-response.ts index cb4da5b6eb..dcbb35aa10 100644 --- a/packages/core/src/core/routes/get-response.ts +++ b/packages/core/src/core/routes/get-response.ts @@ -1,3 +1,4 @@ +import { Logger } from '../../common'; import { IAppController } from '../app.controller.interface'; import { HookPostFunction } from '../hooks'; import { Context, HttpResponse, isHttpResponse } from '../http'; @@ -8,6 +9,8 @@ import { Route } from './route.interface'; export async function getResponse( route: Route, ctx: Context, services: ServiceManager, appController: IAppController ): Promise { + const logger = services.get(Logger); + let response: undefined | HttpResponse; const hookPostFunctions: HookPostFunction[] = []; @@ -17,7 +20,7 @@ export async function getResponse( try { result = await hook(ctx, services); } catch (error: any) { - result = await convertErrorToResponse(error, ctx, appController); + result = await convertErrorToResponse(error, ctx, appController, logger); } if (isHttpResponse(result)) { response = result; @@ -31,20 +34,20 @@ export async function getResponse( try { response = await route.controller[route.propertyKey](ctx, ctx.request.params, ctx.request.body); } catch (error: any) { - response = await convertErrorToResponse(error, ctx, appController); + response = await convertErrorToResponse(error, ctx, appController, logger); } } if (!isHttpResponse(response)) { const error = new Error(`The controller method "${route.propertyKey}" should return an HttpResponse.`); - response = await convertErrorToResponse(error, ctx, appController); + response = await convertErrorToResponse(error, ctx, appController, logger); } for (const postFn of hookPostFunctions) { try { await postFn(response); } catch (error: any) { - response = await convertErrorToResponse(error, ctx, appController); + response = await convertErrorToResponse(error, ctx, appController, logger); } } diff --git a/packages/core/src/express/create-app.spec.ts b/packages/core/src/express/create-app.spec.ts index ebb789c248..dad7648616 100644 --- a/packages/core/src/express/create-app.spec.ts +++ b/packages/core/src/express/create-app.spec.ts @@ -1,5 +1,5 @@ // std -import { strictEqual } from 'assert'; +import { deepStrictEqual, strictEqual } from 'assert'; import { Buffer } from 'buffer'; // 3p @@ -16,6 +16,7 @@ import { dependency, Get, Head, + Hook, HttpResponseOK, OpenApi, Options, @@ -25,6 +26,8 @@ import { ServiceManager } from '../core'; import { createApp, OPENAPI_SERVICE_ID } from './create-app'; +import { mock } from 'node:test'; +import { Logger } from '../common'; describe('createApp', () => { @@ -48,10 +51,13 @@ describe('createApp', () => { }); afterEach(() => { + mock.reset(); Config.remove('settings.staticPathPrefix'); Config.remove('settings.debug'); Config.remove('settings.bodyParser.limit'); Config.remove('settings.cookieParser.secret'); + Config.remove('settings.loggerFormat'); + Config.remove('settings.logger.format'); }); const cookieSecret = 'strong-secret'; @@ -487,9 +493,18 @@ describe('createApp', () => { .type('application/json') .send('{ "foo": "bar", }') .expect(400) - .expect({ - body: '{ \"foo\": \"bar\", }', - message: 'Unexpected token } in JSON at position 16' + .then(response => { + try { + deepStrictEqual(response.body, { + body: '{ \"foo\": \"bar\", }', + message: 'Unexpected token } in JSON at position 16' + }); + } catch (error) { + deepStrictEqual(response.body, { + body: '{ \"foo\": \"bar\", }', + message: 'Expected double-quoted property name in JSON at position 16' + }) + } }); }); @@ -663,4 +678,238 @@ describe('createApp', () => { } }); + context('given the configuration "settings.loggerFormat" is set to "foal"', () => { + it('should log the request with a detailed message and detail parameters.', async () => { + Config.set('settings.loggerFormat', 'foal'); + + class AppController { + @Get('/a') + getA(ctx: Context) { + return new HttpResponseOK('a'); + } + } + + const serviceManager = new ServiceManager(); + + const logger = serviceManager.get(Logger); + const loggerMock = mock.method(logger, 'info', () => {}).mock; + + const app = await createApp(AppController, { + serviceManager + }); + + await request(app) + .get('/a?apiKey=a_secret_api_key') + .expect(200); + + strictEqual(loggerMock.callCount(), 1); + + const message = loggerMock.calls[0].arguments[0]; + const params = loggerMock.calls[0].arguments[1]; + + strictEqual(message, 'HTTP request - GET /a'); + strictEqual(typeof params?.responseTime, 'number') + + delete params?.responseTime; + + deepStrictEqual(params, { + method: 'GET', + url: '/a', + statusCode: 200, + contentLength: '1', + }); + }); + + it('should use the options.getHttpLogParams if provided', async () => { + Config.set('settings.loggerFormat', 'foal'); + + class AppController { + @Get('/a') + getA(ctx: Context) { + return new HttpResponseOK('a'); + } + } + + const serviceManager = new ServiceManager(); + + const logger = serviceManager.get(Logger); + const loggerMock = mock.method(logger, 'info', () => {}).mock; + + const app = await createApp(AppController, { + serviceManager, + getHttpLogParams: (tokens: any, req: any, res: any) => ({ + method: tokens.method(req, res), + url: tokens.url(req, res).split('?')[0], + myCustomHeader: req.get('my-custom-header') + }), + }); + + await request(app) + .get('/a') + .set('my-custom-header', 'my-custom-value') + .expect(200); + + strictEqual(loggerMock.callCount(), 1); + + const message = loggerMock.calls[0].arguments[0]; + const params = loggerMock.calls[0].arguments[1]; + + strictEqual(message, 'HTTP request - GET /a'); + + deepStrictEqual(params, { + method: 'GET', + url: '/a', + myCustomHeader: 'my-custom-value', + }); + }); + }); + + context('given the configuration "settings.loggerFormat" is set to a value different from "none" or "foal"', () => { + it('should log a warning message.', async () => { + Config.set('settings.loggerFormat', 'dev'); + + class AppController { + @Get('/a') + getA(ctx: Context) { + return new HttpResponseOK('a'); + } + } + + const serviceManager = new ServiceManager(); + + const logger = serviceManager.get(Logger); + const loggerMock = mock.method(logger, 'warn', () => {}).mock; + + await createApp(AppController, { + serviceManager + }); + + strictEqual(loggerMock.callCount(), 1); + strictEqual(loggerMock.calls[0].arguments[0], '[CONFIG] Using another format than "foal" for "settings.loggerFormat" is deprecated.'); + }); + }); + + it('should allow to add log context information.', async () => { + Config.set('settings.logger.format', 'json'); + + class AppController { + @dependency + logger: Logger; + + @Get('/') + @Hook((ctx, services) => { + const logger = services.get(Logger); + logger.addLogContext('foo', 'bar'); + }) + getA(ctx: Context) { + this.logger.info('Hello world'); + return new HttpResponseOK(); + } + } + + const serviceManager = new ServiceManager(); + + const consoleMock = mock.method(console, 'log', () => {}).mock; + + const app = await createApp(AppController, { + serviceManager + }); + + await request(app) + .get('/') + .expect(200); + + const messages = consoleMock.calls.map(call => JSON.parse(call.arguments[0])); + + strictEqual(messages.some(message => message.foo === 'bar'), true); + }); + + context('given a "X-Request-ID" header is present in the request', () => { + it('should add the request ID to the request object.', async () => { + const requestId = 'a_request_id'; + + class AppController { + @Get('/') + get(ctx: Context) { + return new HttpResponseOK({ + requestId: ctx.request.id + }); + } + } + + const serviceManager = new ServiceManager(); + const app = await createApp(AppController, { + serviceManager + }); + + await request(app) + .get('/') + .set('X-Request-ID', requestId) + .expect(200) + .expect({ + requestId, + }); + }); + }); + + context('given a "X-Request-ID" header is NOT present in the request', () => { + it('should add a request ID to the request object.', async () => { + class AppController { + @Get('/') + get(ctx: Context) { + return new HttpResponseOK({ + requestId: ctx.request.id + }); + } + } + + const serviceManager = new ServiceManager(); + const app = await createApp(AppController, { + serviceManager + }); + + await request(app) + .get('/') + .expect(200) + .expect(response => { + if (!response.body.requestId) { + throw new Error('The request ID should exist.'); + } + }); + }); + }); + + it('should add the request ID to the log context.', async () => { + class AppController { + @Get('/') + get(ctx: Context) { + return new HttpResponseOK({ + requestId: ctx.request.id + }); + } + } + + const serviceManager = new ServiceManager(); + const logger = serviceManager.get(Logger); + const loggerMock = mock.method(logger, 'addLogContext', () => {}).mock; + + const app = await createApp(AppController, { + serviceManager + }); + + let requestId: string|undefined; + await request(app) + .get('/') + .expect(200) + .then(response => { + requestId = response.body.requestId; + }) + + strictEqual(loggerMock.callCount(), 1); + + const [key, value] = loggerMock.calls[0].arguments; + + strictEqual(key, 'requestId'); + strictEqual(value, requestId); + }); }); diff --git a/packages/core/src/express/create-app.ts b/packages/core/src/express/create-app.ts index 5a3c9f6e95..d63765a6ee 100644 --- a/packages/core/src/express/create-app.ts +++ b/packages/core/src/express/create-app.ts @@ -1,7 +1,10 @@ +// std +import { randomUUID } from 'node:crypto'; + // 3p import * as cookieParser from 'cookie-parser'; import * as express from 'express'; -import * as logger from 'morgan'; +import * as morgan from 'morgan'; // FoalTS import { @@ -15,6 +18,7 @@ import { ServiceManager, } from '../core'; import { sendResponse } from './send-response'; +import { httpRequestMessagePrefix, Logger } from '../common'; export const OPENAPI_SERVICE_ID = 'OPENAPI_SERVICE_ID_a5NWKbBNBxVVZ'; @@ -24,6 +28,7 @@ type ErrorMiddleware = (err: any, req: any, res: any, next: (err?: any) => any) export interface CreateAppOptions { expressInstance?: any; serviceManager?: ServiceManager; + getHttpLogParams?: (tokens: any, req: any, res: any) => Record; preMiddlewares?: (Middleware|ErrorMiddleware)[]; afterPreMiddlewares?: (Middleware|ErrorMiddleware)[]; postMiddlewares?: (Middleware|ErrorMiddleware)[]; @@ -49,6 +54,16 @@ function protectionHeaders(req: any, res: any, next: (err?: any) => any) { next(); } +export function getHttpLogParamsDefault(tokens: any, req: any, res: any): Record { + return { + method: tokens.method(req, res), + url: tokens.url(req, res).split('?')[0], + statusCode: parseInt(tokens.status(req, res), 10), + contentLength: tokens.res(req, res, 'content-length'), + responseTime: parseFloat(tokens['response-time'](req, res)), + }; +} + /** * Create an Express application from the root controller. * @@ -79,14 +94,50 @@ export async function createApp( app.use(middleware); } + // Create the service and controller manager. + const services = options.serviceManager || new ServiceManager(); + app.foal = { services }; + + // Retrieve the logger. + const logger = services.get(Logger); + + // Allow to add log context. + app.use((req: any, res: any, next: (err?: any) => any) => { + logger.initLogContext(next); + }); + + // Generate a unique ID for each request. + app.use((req: any, res: any, next: (err?: any) => any) => { + const requestId = req.get('x-request-id') || randomUUID(); + + req.id = requestId; + logger.addLogContext('requestId', requestId); + + next(); + }); + // Log requests. const loggerFormat = Config.get( 'settings.loggerFormat', 'string', '[:date] ":method :url HTTP/:http-version" :status - :response-time ms' ); - if (loggerFormat !== 'none') { - app.use(logger(loggerFormat)); + if (loggerFormat === 'foal') { + const getHttpLogParams = options.getHttpLogParams || getHttpLogParamsDefault; + app.use(morgan( + (tokens: any, req: any, res: any) => JSON.stringify(getHttpLogParams(tokens, req, res)), + { + stream: { + write: (message: string) => { + const data = JSON.parse(message); + logger.info(`${httpRequestMessagePrefix}${data.method} ${data.url}`, data); + }, + }, + } + )) + } else if (loggerFormat !== 'none') { + logger.warn('[CONFIG] Using another format than "foal" for "settings.loggerFormat" is deprecated.'); + app.use(morgan(loggerFormat)); } app.use(protectionHeaders); @@ -112,10 +163,6 @@ export async function createApp( app.use(middleware); } - // Create the service and controller manager. - const services = options.serviceManager || new ServiceManager(); - app.foal = { services }; - // Inject the OpenAPI service with an ID string to avoid duplicated singletons // across several npm packages. services.set(OPENAPI_SERVICE_ID, services.get(OpenApi)); @@ -131,7 +178,7 @@ export async function createApp( const ctx = new Context(req, route.controller.constructor.name, route.propertyKey); // TODO: better test this line. const response = await getResponse(route, ctx, services, appController); - sendResponse(response, res); + sendResponse(response, res, logger); } catch (error: any) { // This try/catch will never be called: the `getResponse` function catches any errors // thrown or rejected in the application and converts it into a response. diff --git a/packages/core/src/express/index.ts b/packages/core/src/express/index.ts index d481ac92dc..2acd84f439 100644 --- a/packages/core/src/express/index.ts +++ b/packages/core/src/express/index.ts @@ -1 +1 @@ -export { createApp, OPENAPI_SERVICE_ID } from './create-app'; +export { createApp, OPENAPI_SERVICE_ID, getHttpLogParamsDefault } from './create-app'; diff --git a/packages/core/src/express/send-response.spec.ts b/packages/core/src/express/send-response.spec.ts index 0ba321c7cb..06cee9f07e 100644 --- a/packages/core/src/express/send-response.spec.ts +++ b/packages/core/src/express/send-response.spec.ts @@ -16,7 +16,7 @@ import { sendResponse } from './send-response'; function execSendResponse(response: HttpResponse): request.Test { const app = express() - .use((req: any, res: any) => sendResponse(response, res)); + .use((req: any, res: any) => sendResponse(response, res, { error: () => {} })); return request(app).get('/'); } diff --git a/packages/core/src/express/send-response.ts b/packages/core/src/express/send-response.ts index 7678bcff9f..7585c7b526 100644 --- a/packages/core/src/express/send-response.ts +++ b/packages/core/src/express/send-response.ts @@ -12,7 +12,13 @@ import { HttpResponse, isHttpResponseMovedPermanently, isHttpResponseRedirect } * @param {any} res - Express response used in middlewares. * @returns {void} */ -export function sendResponse(response: HttpResponse, res: any): void { +export function sendResponse( + response: HttpResponse, + res: any, + logger: { + error: (message: string, params: { error: Error }) => void + } +): void { res.status(response.statusCode); res.set(response.getHeaders()); const cookies = response.getCookies(); @@ -35,8 +41,10 @@ export function sendResponse(response: HttpResponse, res: any): void { } if (response.stream === true) { - pipeline(response.body, res, (err: any) => { - if (err) { console.log(err); } + pipeline(response.body, res, (error: any) => { + if (error) { + logger.error(error.message, { error }); + } }); return; } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 63d808c3c1..acd0200cce 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -36,6 +36,7 @@ export { hashPassword, passwordHashNeedsToBeRefreshed, isInFile, + Logger, render, renderToString, renderError, @@ -217,6 +218,7 @@ export { export { OPENAPI_SERVICE_ID, createApp, + getHttpLogParamsDefault, } from './express'; export { Session, diff --git a/packages/core/src/sessions/http/use-sessions.hook.spec.ts b/packages/core/src/sessions/http/use-sessions.hook.spec.ts index 6f011b5c11..e31ebafb0f 100644 --- a/packages/core/src/sessions/http/use-sessions.hook.spec.ts +++ b/packages/core/src/sessions/http/use-sessions.hook.spec.ts @@ -1,5 +1,6 @@ // std import { deepStrictEqual, doesNotReject, rejects, strictEqual } from 'assert'; +import { mock } from 'node:test'; // FoalTS import { @@ -36,6 +37,7 @@ import { SessionStore, } from '../core'; import { UseSessions } from './use-sessions.hook'; +import { Logger } from '../../common'; describe('UseSessions', () => { @@ -513,7 +515,7 @@ describe('UseSessions', () => { it('should throw an error if the session state has no CSRF token.', async () => { ctx = createContextWithPostMethod({}, { [SESSION_DEFAULT_COOKIE_NAME]: anonymousSessionID }); return rejects( - () => hook(ctx, services), + async () => { await hook(ctx, services) }, { message: 'Unexpected error: the session content does not have a "csrfToken" field. ' + 'Are you sure you created the session with "createSession"?' @@ -605,12 +607,34 @@ describe('UseSessions', () => { strictEqual(ctx.user, null); }); + it('and add null as user ID to the log context.', async () => { + const logger = services.get(Logger); + const loggerMock = mock.method(logger, 'addLogContext', () => {}).mock; + + await hook(ctx, services); + + strictEqual(loggerMock.callCount(), 1); + + deepStrictEqual(loggerMock.calls[0].arguments, ['userId', null]); + }); + }); context('given the session has a user ID', () => { beforeEach(() => ctx = createContext({ Authorization: `Bearer ${authenticatedSessionID}`})); + it('and add null as user ID to the log context.', async () => { + const logger = services.get(Logger); + const loggerMock = mock.method(logger, 'addLogContext', () => {}).mock; + + await hook(ctx, services); + + strictEqual(loggerMock.callCount(), 1); + + deepStrictEqual(loggerMock.calls[0].arguments, ['userId', userId]); + }); + context('given options.user is not defined', () => { it('with the null value.', async () => { diff --git a/packages/core/src/sessions/http/use-sessions.hook.ts b/packages/core/src/sessions/http/use-sessions.hook.ts index a9e2225d68..9d69af00f2 100644 --- a/packages/core/src/sessions/http/use-sessions.hook.ts +++ b/packages/core/src/sessions/http/use-sessions.hook.ts @@ -22,6 +22,7 @@ import { checkUserIdType } from './check-user-id-type'; import { getSessionIDFromRequest, RequestValidationError } from './get-session-id-from-request'; import { createSession, readSession, SessionStore } from '../core'; import { getCsrfTokenFromRequest, removeSessionCookie, setSessionCookie, shouldVerifyCsrfToken } from './utils'; +import { Logger } from '../../common'; export type UseSessionOptions = { store?: Class; @@ -142,6 +143,9 @@ export function UseSessions(options: UseSessionOptions = {}): HookDecorator { /* Set ctx.user */ + const logger = services.get(Logger); + logger.addLogContext('userId', session.userId); + if (session.userId !== null && options.user) { const userId = checkUserIdType(session.userId, options.userIdType); ctx.user = await options.user(userId as never, services); diff --git a/packages/examples/config/default.yml b/packages/examples/config/default.yml index 304432adae..2b7a2a1c0f 100644 --- a/packages/examples/config/default.yml +++ b/packages/examples/config/default.yml @@ -3,8 +3,7 @@ port: 3001 settings: openapi: useHooks: true - debug: true - loggerFormat: dev + loggerFormat: foal session: secret: 'my secret' staticPath: 'public/' diff --git a/packages/examples/config/development.yml b/packages/examples/config/development.yml new file mode 100644 index 0000000000..259f5fb0fa --- /dev/null +++ b/packages/examples/config/development.yml @@ -0,0 +1,4 @@ +settings: + debug: true + logger: + format: dev \ No newline at end of file diff --git a/packages/examples/package-lock.json b/packages/examples/package-lock.json index ca160dc4f8..0a1b7e1741 100644 --- a/packages/examples/package-lock.json +++ b/packages/examples/package-lock.json @@ -17,7 +17,7 @@ }, "devDependencies": { "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "concurrently": "~8.2.1", "copy": "~0.3.2", "mocha": "~10.2.0", @@ -124,9 +124,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "node_modules/abbrev": { @@ -3661,9 +3661,9 @@ "dev": true }, "@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "abbrev": { diff --git a/packages/examples/package.json b/packages/examples/package.json index e4e329f285..93f999bd32 100644 --- a/packages/examples/package.json +++ b/packages/examples/package.json @@ -61,11 +61,11 @@ "devDependencies": { "@foal/cli": "^4.0.0", "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "concurrently": "~8.2.1", "copy": "~0.3.2", "mocha": "~10.2.0", "supervisor": "~0.12.0", "typescript": "~4.9.5" } -} +} \ No newline at end of file diff --git a/packages/examples/src/app/controllers/profile.controller.ts b/packages/examples/src/app/controllers/profile.controller.ts index e8e9a34034..7a49bb1881 100644 --- a/packages/examples/src/app/controllers/profile.controller.ts +++ b/packages/examples/src/app/controllers/profile.controller.ts @@ -1,5 +1,5 @@ import { - ApiInfo, ApiServer, Context, dependency, Get, Hook, HttpResponseNotFound, HttpResponseRedirect, Post, render, UserRequired + ApiInfo, ApiServer, Context, dependency, Get, Hook, HttpResponseNotFound, HttpResponseRedirect, Logger, Post, render, UserRequired } from '@foal/core'; import { Disk, ParseAndValidateFiles } from '@foal/storage'; @@ -16,6 +16,9 @@ export class ProfileController { @dependency disk: Disk; + @dependency + logger: Logger; + @Post('/image') @Hook(async ctx => { ctx.user = await User.findOneBy({ email: 'john@foalts.org' }); }) @UserRequired() @@ -28,7 +31,7 @@ export class ProfileController { try { await this.disk.delete(user.profile); } catch (error: any) { - console.log(error.message); + this.logger.error(error.message, { error }); } } diff --git a/packages/graphiql/package-lock.json b/packages/graphiql/package-lock.json index ee6d68704c..b918379760 100644 --- a/packages/graphiql/package-lock.json +++ b/packages/graphiql/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "devDependencies": { "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "graphiql": "~3.0.5", "mocha": "~10.2.0", "react": "~18.1.0", @@ -1129,9 +1129,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "node_modules/@types/tern": { @@ -3645,9 +3645,9 @@ "dev": true }, "@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "@types/tern": { diff --git a/packages/graphiql/package.json b/packages/graphiql/package.json index 245df5c6b4..ce81265a5c 100644 --- a/packages/graphiql/package.json +++ b/packages/graphiql/package.json @@ -44,7 +44,7 @@ }, "devDependencies": { "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "graphiql": "~3.0.5", "mocha": "~10.2.0", "react": "~18.1.0", @@ -53,4 +53,4 @@ "ts-node": "~10.9.1", "typescript": "~4.9.5" } -} +} \ No newline at end of file diff --git a/packages/graphql/package-lock.json b/packages/graphql/package-lock.json index 5c847240df..b8a0a76585 100644 --- a/packages/graphql/package-lock.json +++ b/packages/graphql/package-lock.json @@ -14,7 +14,7 @@ }, "devDependencies": { "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "graphql": "~16.8.0", "graphql-request": "~6.1.0", "mocha": "~10.2.0", @@ -208,9 +208,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "node_modules/@types/semver": { @@ -1863,9 +1863,9 @@ "dev": true }, "@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "@types/semver": { diff --git a/packages/graphql/package.json b/packages/graphql/package.json index 530072f37d..94d87234e6 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -46,7 +46,7 @@ }, "devDependencies": { "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "graphql": "~16.8.0", "graphql-request": "~6.1.0", "mocha": "~10.2.0", @@ -58,4 +58,4 @@ "peerDependencies": { "graphql": "^16.8.0" } -} +} \ No newline at end of file diff --git a/packages/graphql/src/acceptance-test.spec.ts b/packages/graphql/src/acceptance-test.spec.ts index 3c5bf8f643..dcc809af5e 100644 --- a/packages/graphql/src/acceptance-test.spec.ts +++ b/packages/graphql/src/acceptance-test.spec.ts @@ -244,7 +244,6 @@ describe('[Acceptance test] GraphQLController', () => { data += chunk; }); resp.on('end', () => { - console.log(data); resolve(JSON.parse(data)); }); }).on('error', err => { diff --git a/packages/jwks-rsa/package-lock.json b/packages/jwks-rsa/package-lock.json index 9baed060d6..4d43aecffd 100644 --- a/packages/jwks-rsa/package-lock.json +++ b/packages/jwks-rsa/package-lock.json @@ -13,7 +13,7 @@ }, "devDependencies": { "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "mocha": "~10.2.0", "rimraf": "~5.0.1", "ts-node": "~10.9.1", @@ -289,9 +289,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==" }, "node_modules/@types/qs": { "version": "6.9.7", @@ -2731,9 +2731,9 @@ "dev": true }, "@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==" }, "@types/qs": { "version": "6.9.7", diff --git a/packages/jwks-rsa/package.json b/packages/jwks-rsa/package.json index 7a26d471c2..51f361c77a 100644 --- a/packages/jwks-rsa/package.json +++ b/packages/jwks-rsa/package.json @@ -53,10 +53,10 @@ "@foal/core": "^4.0.0", "@foal/jwt": "^4.0.0", "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "mocha": "~10.2.0", "rimraf": "~5.0.1", "ts-node": "~10.9.1", "typescript": "~4.9.5" } -} +} \ No newline at end of file diff --git a/packages/jwt/package-lock.json b/packages/jwt/package-lock.json index 9a1cb95ce6..d5a3326259 100644 --- a/packages/jwt/package-lock.json +++ b/packages/jwt/package-lock.json @@ -13,7 +13,7 @@ }, "devDependencies": { "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "mocha": "~10.2.0", "rimraf": "~5.0.1", "ts-node": "~10.9.1", @@ -200,9 +200,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "node_modules/acorn": { @@ -1728,9 +1728,9 @@ "dev": true }, "@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "acorn": { diff --git a/packages/jwt/package.json b/packages/jwt/package.json index 57f34b52a7..bd1b2b2439 100644 --- a/packages/jwt/package.json +++ b/packages/jwt/package.json @@ -47,10 +47,10 @@ }, "devDependencies": { "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "mocha": "~10.2.0", "rimraf": "~5.0.1", "ts-node": "~10.9.1", "typescript": "~4.9.5" } -} +} \ No newline at end of file diff --git a/packages/jwt/src/http/jwt.hook.spec.ts b/packages/jwt/src/http/jwt.hook.spec.ts index 54162d8e28..cdfa648f78 100644 --- a/packages/jwt/src/http/jwt.hook.spec.ts +++ b/packages/jwt/src/http/jwt.hook.spec.ts @@ -1,5 +1,6 @@ // std import { deepStrictEqual, notStrictEqual, rejects, strictEqual } from 'assert'; +import { mock } from 'node:test'; // 3p import { @@ -20,6 +21,7 @@ import { isHttpResponseBadRequest, isHttpResponseForbidden, isHttpResponseUnauthorized, + Logger, ServiceManager } from '@foal/core'; import { sign } from 'jsonwebtoken'; @@ -631,6 +633,27 @@ export function testSuite(JWT: typeof JWTOptional|typeof JWTRequired, required: describe('should set Context.user', () => { + it('and shoud add the user ID to the log context.', async () => { + const jwt = sign({}, secret, { subject: '123' }); + ctx = createContext({ Authorization: `Bearer ${jwt}` }); + + hook = getHookFunction(JWT({ + user: async () => null, + userIdType: 'number' + })); + + const logger = services.get(Logger); + const loggerMock = mock.method(logger, 'addLogContext', () => {}).mock; + + await hook(ctx, services); + + strictEqual(loggerMock.callCount(), 1); + deepStrictEqual( + loggerMock.calls[0].arguments, + ['userId', 123], + ); + }) + context('given options.user is not defined', () => { it('with the decoded payload (header & secret).', async () => { diff --git a/packages/jwt/src/http/jwt.hook.ts b/packages/jwt/src/http/jwt.hook.ts index 76716985e4..44d18db238 100644 --- a/packages/jwt/src/http/jwt.hook.ts +++ b/packages/jwt/src/http/jwt.hook.ts @@ -12,6 +12,7 @@ import { HttpResponseForbidden, HttpResponseUnauthorized, IApiSecurityScheme, + Logger, ServiceManager } from '@foal/core'; import { decode, verify } from 'jsonwebtoken'; @@ -198,6 +199,10 @@ export function JWT(required: boolean, options: JWTOptions, verifyOptions: Verif } const userId = checkAndConvertUserIdType(payload.sub, options.userIdType); + + const logger = services.get(Logger); + logger.addLogContext('userId', userId); + const user = await options.user(userId as never, services); if (!user) { return new InvalidTokenResponse('The token subject does not match any user.'); diff --git a/packages/password/package-lock.json b/packages/password/package-lock.json index f37e65176e..71cbee1338 100644 --- a/packages/password/package-lock.json +++ b/packages/password/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "devDependencies": { "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "copy": "~0.3.2", "mocha": "~10.2.0", "rimraf": "~5.0.1", @@ -148,9 +148,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "node_modules/acorn": { @@ -2485,9 +2485,9 @@ "dev": true }, "@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "acorn": { diff --git a/packages/password/package.json b/packages/password/package.json index 718364f7c5..07a19b9418 100644 --- a/packages/password/package.json +++ b/packages/password/package.json @@ -41,11 +41,11 @@ ], "devDependencies": { "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "copy": "~0.3.2", "mocha": "~10.2.0", "rimraf": "~5.0.1", "ts-node": "~10.9.1", "typescript": "~4.9.5" } -} +} \ No newline at end of file diff --git a/packages/redis/package-lock.json b/packages/redis/package-lock.json index d815b63251..dc4dc27759 100644 --- a/packages/redis/package-lock.json +++ b/packages/redis/package-lock.json @@ -13,7 +13,7 @@ }, "devDependencies": { "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "mocha": "~10.2.0", "rimraf": "~5.0.1", "ts-node": "~10.9.1", @@ -253,9 +253,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "node_modules/acorn": { @@ -1739,9 +1739,9 @@ "dev": true }, "@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "acorn": { diff --git a/packages/redis/package.json b/packages/redis/package.json index ccf80c046d..8d3d6d3a2e 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -46,10 +46,10 @@ }, "devDependencies": { "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "mocha": "~10.2.0", "rimraf": "~5.0.1", "ts-node": "~10.9.1", "typescript": "~4.9.5" } -} +} \ No newline at end of file diff --git a/packages/social/package-lock.json b/packages/social/package-lock.json index f2b1f59306..3b277df1ca 100644 --- a/packages/social/package-lock.json +++ b/packages/social/package-lock.json @@ -13,7 +13,7 @@ }, "devDependencies": { "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "copy": "~0.3.2", "jsonwebtoken": "~9.0.2", "mocha": "~10.2.0", @@ -202,9 +202,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "node_modules/acorn": { @@ -2613,9 +2613,9 @@ "dev": true }, "@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "acorn": { diff --git a/packages/social/package.json b/packages/social/package.json index 233bca229c..831aaea9d2 100644 --- a/packages/social/package.json +++ b/packages/social/package.json @@ -57,7 +57,7 @@ }, "devDependencies": { "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "copy": "~0.3.2", "jsonwebtoken": "~9.0.2", "mocha": "~10.2.0", @@ -65,4 +65,4 @@ "ts-node": "~10.9.1", "typescript": "~4.9.5" } -} +} \ No newline at end of file diff --git a/packages/socket.io/package-lock.json b/packages/socket.io/package-lock.json index 418c03ca3e..a1ee7c0f8d 100644 --- a/packages/socket.io/package-lock.json +++ b/packages/socket.io/package-lock.json @@ -15,7 +15,7 @@ "devDependencies": { "@socket.io/redis-adapter": "~8.2.1", "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "mocha": "~10.2.0", "redis": "~4.6.8", "rimraf": "~5.0.1", @@ -298,9 +298,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==" }, "node_modules/accepts": { "version": "1.3.8", @@ -2045,9 +2045,9 @@ "dev": true }, "@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==" }, "accepts": { "version": "1.3.8", diff --git a/packages/socket.io/package.json b/packages/socket.io/package.json index 338be9fcd4..6b0b54b1ca 100644 --- a/packages/socket.io/package.json +++ b/packages/socket.io/package.json @@ -46,7 +46,7 @@ "devDependencies": { "@socket.io/redis-adapter": "~8.2.1", "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "mocha": "~10.2.0", "redis": "~4.6.8", "rimraf": "~5.0.1", @@ -59,4 +59,4 @@ "reflect-metadata": "~0.1.13", "socket.io": "~4.7.2" } -} +} \ No newline at end of file diff --git a/packages/socket.io/src/errors/convert-error-to-websocket-response.spec.ts b/packages/socket.io/src/errors/convert-error-to-websocket-response.spec.ts index f3cfb36283..b3ed99804f 100644 --- a/packages/socket.io/src/errors/convert-error-to-websocket-response.spec.ts +++ b/packages/socket.io/src/errors/convert-error-to-websocket-response.spec.ts @@ -23,12 +23,20 @@ describe('convertErrorToWebsocketResponse', () => { context('given the configuration settings.logErrors is true or not defined', () => { it('should log the error stack.', async () => { - let message = ''; - const log = (msg: string) => message = msg; + let actualMessage: string|undefined; + let actualError: Error|undefined; - await convertErrorToWebsocketResponse(error, ctx, new SocketIOController(), log); + const logger = { + error(message: string, { error }: { error: Error }): void { + actualMessage = message; + actualError = error; + } + } + + await convertErrorToWebsocketResponse(error, ctx, new SocketIOController(), logger); - strictEqual(message, error.stack); + strictEqual(actualMessage, error.message); + strictEqual(actualError, error); }); }); @@ -40,12 +48,19 @@ describe('convertErrorToWebsocketResponse', () => { afterEach(() => Config.remove('settings.logErrors')); it('should not log the error stack.', async () => { - let message = ''; - const log = (msg: string) => message = msg; + let actualMessage: string|undefined; + let actualError: Error|undefined; - await convertErrorToWebsocketResponse(error, ctx, new SocketIOController(), log); + const logger = { + error(message: string, { error }: { error: Error }): void { + actualMessage = message; + actualError = error; + } + } + await convertErrorToWebsocketResponse(error, ctx, new SocketIOController(), logger); - strictEqual(message, ''); + strictEqual(actualMessage, undefined); + strictEqual(actualError, undefined); }); }); @@ -53,7 +68,7 @@ describe('convertErrorToWebsocketResponse', () => { context('given socketioController.handleError is not defined', () => { it('should return an WebsocketErrorResponse object created by renderWebsocketError.', async () => { - const response = await convertErrorToWebsocketResponse(error, ctx, new SocketIOController(), () => {}); + const response = await convertErrorToWebsocketResponse(error, ctx, new SocketIOController(), { error: () => {} }); if (!(response instanceof WebsocketErrorResponse)) { throw new Error('An WebsocketErrorResponse should have been returned.'); @@ -83,7 +98,7 @@ describe('convertErrorToWebsocketResponse', () => { } it('should return the response returned by handleError.', async () => { - const response = await convertErrorToWebsocketResponse(error, ctx, new SocketIOController(), () => {}); + const response = await convertErrorToWebsocketResponse(error, ctx, new SocketIOController(), { error: () => {} }); if (!(response instanceof WebsocketErrorResponse)) { throw new Error('An WebsocketErrorResponse should have been returned.'); @@ -103,7 +118,7 @@ describe('convertErrorToWebsocketResponse', () => { } it('should return an WebsocketErrorResponse object created by renderWebsocketError.', async () => { - const response = await convertErrorToWebsocketResponse(error, ctx, new SocketIOController(), () => {}); + const response = await convertErrorToWebsocketResponse(error, ctx, new SocketIOController(), { error: () => {} }); if (!(response instanceof WebsocketErrorResponse)) { throw new Error('An WebsocketErrorResponse should have been returned.'); @@ -124,7 +139,7 @@ describe('convertErrorToWebsocketResponse', () => { } it('should return an WebsocketErrorResponse object created by renderWebsocketError.', async () => { - const response = await convertErrorToWebsocketResponse(error, ctx, new SocketIOController(), () => {}); + const response = await convertErrorToWebsocketResponse(error, ctx, new SocketIOController(), { error: () => {} }); if (!(response instanceof WebsocketErrorResponse)) { throw new Error('An WebsocketErrorResponse should have been returned.'); diff --git a/packages/socket.io/src/errors/convert-error-to-websocket-response.ts b/packages/socket.io/src/errors/convert-error-to-websocket-response.ts index d15c568ad1..06f5e07242 100644 --- a/packages/socket.io/src/errors/convert-error-to-websocket-response.ts +++ b/packages/socket.io/src/errors/convert-error-to-websocket-response.ts @@ -6,10 +6,15 @@ import { ISocketIOController, WebsocketContext, WebsocketErrorResponse, Websocke import { renderWebsocketError } from './render-websocket-error'; export async function convertErrorToWebsocketResponse( - error: Error, ctx: WebsocketContext, socketIOController: ISocketIOController, log = console.error + error: Error, + ctx: WebsocketContext, + socketIOController: ISocketIOController, + logger: { + error: (message: string, params: { error: Error }) => void + } ): Promise { if (Config.get('settings.logErrors', 'boolean', true)) { - log(error.stack); + logger.error(error.message, { error }); } if (socketIOController.handleError) { diff --git a/packages/socket.io/src/routes/get-websocket-response.ts b/packages/socket.io/src/routes/get-websocket-response.ts index 644bf17c03..21b66f9fb0 100644 --- a/packages/socket.io/src/routes/get-websocket-response.ts +++ b/packages/socket.io/src/routes/get-websocket-response.ts @@ -1,5 +1,5 @@ // 3p -import { ServiceManager } from '@foal/core'; +import { Logger, ServiceManager } from '@foal/core'; // FoalTS import { @@ -15,6 +15,8 @@ import { convertErrorToWebsocketResponse } from '../errors'; export async function getWebsocketResponse( route: WebsocketRoute, ctx: WebsocketContext, services: ServiceManager, socketIOController: ISocketIOController ): Promise { + const logger = services.get(Logger); + let response: undefined | WebsocketResponse | WebsocketErrorResponse; const hookPostFunctions: WebsocketHookPostFunction[] = []; @@ -24,7 +26,7 @@ export async function getWebsocketResponse( try { result = await hook(ctx, services); } catch (error: any) { - result = await convertErrorToWebsocketResponse(error, ctx, socketIOController); + result = await convertErrorToWebsocketResponse(error, ctx, socketIOController, logger); } if ((result instanceof WebsocketResponse) || (result instanceof WebsocketErrorResponse)) { response = result; @@ -38,20 +40,20 @@ export async function getWebsocketResponse( try { response = await route.controller[route.propertyKey](ctx, ctx.payload); } catch (error: any) { - response = await convertErrorToWebsocketResponse(error, ctx, socketIOController); + response = await convertErrorToWebsocketResponse(error, ctx, socketIOController, logger); } } if (!((response instanceof WebsocketResponse) || (response instanceof WebsocketErrorResponse))) { const error = new Error(`The controller method "${route.propertyKey}" should return a WebsocketResponse or a WebsocketErrorResponse.`); - response = await convertErrorToWebsocketResponse(error, ctx, socketIOController); + response = await convertErrorToWebsocketResponse(error, ctx, socketIOController, logger); } for (const postFn of hookPostFunctions) { try { await postFn(response); } catch (error: any) { - response = await convertErrorToWebsocketResponse(error, ctx, socketIOController); + response = await convertErrorToWebsocketResponse(error, ctx, socketIOController, logger); } } diff --git a/packages/storage/package-lock.json b/packages/storage/package-lock.json index 772859b06a..b69013860a 100644 --- a/packages/storage/package-lock.json +++ b/packages/storage/package-lock.json @@ -14,7 +14,7 @@ }, "devDependencies": { "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "@types/supertest": "2.0.12", "copy": "~0.3.2", "mocha": "~10.2.0", @@ -210,9 +210,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "node_modules/@types/superagent": { @@ -2821,9 +2821,9 @@ "dev": true }, "@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "@types/superagent": { diff --git a/packages/storage/package.json b/packages/storage/package.json index f3ad808c3b..82e520e6b1 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -51,7 +51,7 @@ "devDependencies": { "@foal/internal-test": "^4.0.0", "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "@types/supertest": "2.0.12", "copy": "~0.3.2", "mocha": "~10.2.0", @@ -60,4 +60,4 @@ "ts-node": "~10.9.1", "typescript": "~4.9.5" } -} +} \ No newline at end of file diff --git a/packages/swagger/package-lock.json b/packages/swagger/package-lock.json index 5a31312314..63994020d5 100644 --- a/packages/swagger/package-lock.json +++ b/packages/swagger/package-lock.json @@ -13,7 +13,7 @@ }, "devDependencies": { "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "copy": "~0.3.2", "mocha": "~10.2.0", "rimraf": "~5.0.1", @@ -201,9 +201,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "node_modules/acorn": { @@ -2446,9 +2446,9 @@ "dev": true }, "@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "acorn": { diff --git a/packages/swagger/package.json b/packages/swagger/package.json index d507d23fb6..c8c2dc3c65 100644 --- a/packages/swagger/package.json +++ b/packages/swagger/package.json @@ -48,11 +48,11 @@ }, "devDependencies": { "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "copy": "~0.3.2", "mocha": "~10.2.0", "rimraf": "~5.0.1", "ts-node": "~10.9.1", "typescript": "~4.9.5" } -} +} \ No newline at end of file diff --git a/packages/typestack/package-lock.json b/packages/typestack/package-lock.json index 5ff62dfff6..52214ef965 100644 --- a/packages/typestack/package-lock.json +++ b/packages/typestack/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "devDependencies": { "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "@types/validator": "13.11.1", "class-transformer": "~0.5.1", "class-validator": "~0.14.0", @@ -205,9 +205,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "node_modules/@types/validator": { @@ -2483,9 +2483,9 @@ "dev": true }, "@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "@types/validator": { diff --git a/packages/typestack/package.json b/packages/typestack/package.json index 5aff6b98b3..ff13d7c232 100644 --- a/packages/typestack/package.json +++ b/packages/typestack/package.json @@ -52,7 +52,7 @@ }, "devDependencies": { "@types/mocha": "10.0.1", - "@types/node": "18.11.9", + "@types/node": "18.18.6", "@types/validator": "13.11.1", "class-transformer": "~0.5.1", "class-validator": "~0.14.0", @@ -62,4 +62,4 @@ "ts-node": "~10.9.1", "typescript": "~4.9.5" } -} +} \ No newline at end of file