@tsed/platform-log-request 8.0.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=PlatformLogRequestSettings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PlatformLogRequestSettings.js","sourceRoot":"","sources":["../../../src/domain/PlatformLogRequestSettings.ts"],"names":[],"mappings":""}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @file Automatically generated by @tsed/barrels.
3
+ */
4
+ export * from "./domain/PlatformLogRequestSettings.js";
5
+ export * from "./services/PlatformLogRequestFactory.js";
6
+ export * from "./utils/defaultAlterLog.js";
7
+ export * from "./utils/defaultLogResponse.js";
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,cAAc,wCAAwC,CAAC;AACvD,cAAc,yCAAyC,CAAC;AACxD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,+BAA+B,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { Configuration, registerProvider } from "@tsed/di";
2
+ import { defaultAlterLog } from "../utils/defaultAlterLog.js";
3
+ import { defaultLogResponse } from "../utils/defaultLogResponse.js";
4
+ function factory(configuration) {
5
+ const { logRequest = true, alterLog = defaultAlterLog, onLogResponse = defaultLogResponse } = configuration.get("logger");
6
+ return logRequest
7
+ ? {
8
+ alterLog,
9
+ onLogResponse
10
+ }
11
+ : null;
12
+ }
13
+ export const PlatformLogRequestFactory = Symbol.for("PLATFORM:LOGGER:REQUEST");
14
+ registerProvider({
15
+ provide: PlatformLogRequestFactory,
16
+ deps: [Configuration],
17
+ useFactory: factory,
18
+ hooks: {
19
+ $onRequest(instance, $ctx) {
20
+ if (instance) {
21
+ $ctx.logger.alterLog((obj, level) => instance.alterLog(level, obj, $ctx));
22
+ $ctx.logStarted = true;
23
+ }
24
+ },
25
+ $onResponse(instance, $ctx) {
26
+ if (instance && $ctx.logStarted) {
27
+ instance.onLogResponse($ctx);
28
+ }
29
+ }
30
+ }
31
+ });
32
+ //# sourceMappingURL=PlatformLogRequestFactory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PlatformLogRequestFactory.js","sourceRoot":"","sources":["../../../src/services/PlatformLogRequestFactory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,aAAa,EAAE,gBAAgB,EAAC,MAAM,UAAU,CAAC;AAE3E,OAAO,EAAC,eAAe,EAAC,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAC,kBAAkB,EAAC,MAAM,gCAAgC,CAAC;AAElE,SAAS,OAAO,CAAC,aAA4B;IAC3C,MAAM,EACJ,UAAU,GAAG,IAAI,EACjB,QAAQ,GAAG,eAAe,EAC1B,aAAa,GAAG,kBAAkB,EACnC,GAAG,aAAa,CAAC,GAAG,CAA2B,QAAQ,CAAC,CAAC;IAE1D,OAAO,UAAU;QACf,CAAC,CAAC;YACE,QAAQ;YACR,aAAa;SACd;QACH,CAAC,CAAC,IAAI,CAAC;AACX,CAAC;AAED,MAAM,CAAC,MAAM,yBAAyB,GAAG,MAAM,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;AAG/E,gBAAgB,CAAC;IACf,OAAO,EAAE,yBAAyB;IAClC,IAAI,EAAE,CAAC,aAAa,CAAC;IACrB,UAAU,EAAE,OAAO;IACnB,KAAK,EAAE;QACL,UAAU,CAAC,QAAoC,EAAE,IAAiB;YAChE,IAAI,QAAQ,EAAE,CAAC;gBACb,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,GAAQ,EAAE,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;gBAC/E,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;YACzB,CAAC;QACH,CAAC;QACD,WAAW,CAAC,QAAoC,EAAE,IAAiB;YACjE,IAAI,QAAQ,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;gBAChC,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;YAC/B,CAAC;QACH,CAAC;KACF;CACF,CAAC,CAAC"}
@@ -0,0 +1,19 @@
1
+ export function defaultAlterLog(level, obj, ctx) {
2
+ const minimalLog = {
3
+ method: ctx.request.method,
4
+ url: ctx.request.url,
5
+ route: ctx.request.route || ctx.request.url,
6
+ ...obj
7
+ };
8
+ if (level === "info") {
9
+ return minimalLog;
10
+ }
11
+ return {
12
+ ...minimalLog,
13
+ headers: ctx.request.headers,
14
+ body: ctx.request.body,
15
+ query: ctx.request.query,
16
+ params: ctx.request.params
17
+ };
18
+ }
19
+ //# sourceMappingURL=defaultAlterLog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defaultAlterLog.js","sourceRoot":"","sources":["../../../src/utils/defaultAlterLog.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,eAAe,CAAC,KAAa,EAAE,GAA4B,EAAE,GAAgB;IAC3F,MAAM,UAAU,GAAG;QACjB,MAAM,EAAE,GAAG,CAAC,OAAO,CAAC,MAAM;QAC1B,GAAG,EAAE,GAAG,CAAC,OAAO,CAAC,GAAG;QACpB,KAAK,EAAE,GAAG,CAAC,OAAO,CAAC,KAAK,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG;QAC3C,GAAG,GAAG;KACP,CAAC;IAEF,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;QACrB,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,OAAO;QACL,GAAG,UAAU;QACb,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,OAAO;QAC5B,IAAI,EAAE,GAAG,CAAC,OAAO,CAAC,IAAI;QACtB,KAAK,EAAE,GAAG,CAAC,OAAO,CAAC,KAAK;QACxB,MAAM,EAAE,GAAG,CAAC,OAAO,CAAC,MAAM;KAC3B,CAAC;AACJ,CAAC"}
@@ -0,0 +1,29 @@
1
+ import { cleanObject } from "@tsed/core";
2
+ export function defaultLogResponse($ctx) {
3
+ if ($ctx.response.statusCode >= 400) {
4
+ const error = $ctx.error;
5
+ $ctx.logger.error({
6
+ event: "request.end",
7
+ status: $ctx.response.statusCode,
8
+ status_code: String($ctx.response.statusCode),
9
+ state: "KO",
10
+ ...cleanObject({
11
+ error_name: error?.name || error?.code,
12
+ error_message: error?.message,
13
+ error_errors: error?.errors,
14
+ error_stack: error?.stack,
15
+ error_body: error?.body,
16
+ error_headers: error?.headers
17
+ })
18
+ });
19
+ }
20
+ else {
21
+ $ctx.logger.info({
22
+ event: "request.end",
23
+ status: $ctx.response.statusCode,
24
+ status_code: String($ctx.response.statusCode),
25
+ state: "OK"
26
+ });
27
+ }
28
+ }
29
+ //# sourceMappingURL=defaultLogResponse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defaultLogResponse.js","sourceRoot":"","sources":["../../../src/utils/defaultLogResponse.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,WAAW,EAAC,MAAM,YAAY,CAAC;AAGvC,MAAM,UAAU,kBAAkB,CAAC,IAAiB;IAClD,IAAI,IAAI,CAAC,QAAQ,CAAC,UAAU,IAAI,GAAG,EAAE,CAAC;QACpC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAwB,CAAC;QAE5C,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;YAChB,KAAK,EAAE,aAAa;YACpB,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,UAAU;YAChC,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC;YAC7C,KAAK,EAAE,IAAI;YACX,GAAG,WAAW,CAAC;gBACb,UAAU,EAAE,KAAK,EAAE,IAAI,IAAI,KAAK,EAAE,IAAI;gBACtC,aAAa,EAAE,KAAK,EAAE,OAAO;gBAC7B,YAAY,EAAE,KAAK,EAAE,MAAM;gBAC3B,WAAW,EAAE,KAAK,EAAE,KAAK;gBACzB,UAAU,EAAE,KAAK,EAAE,IAAI;gBACvB,aAAa,EAAE,KAAK,EAAE,OAAO;aAC9B,CAAC;SACH,CAAC,CAAC;IACL,CAAC;SAAM,CAAC;QACN,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;YACf,KAAK,EAAE,aAAa;YACpB,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,UAAU;YAChC,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC;YAC7C,KAAK,EAAE,IAAI;SACZ,CAAC,CAAC;IACL,CAAC;AACH,CAAC"}
@@ -0,0 +1,22 @@
1
+ import type { BaseContext } from "@tsed/di";
2
+ export type AlterLogCallback = (level: "debug" | "info" | "warn" | "error" | "all", obj: Record<string, unknown>, ctx: BaseContext) => Record<string, unknown>;
3
+ export interface PlatformLogRequestSettings {
4
+ /**
5
+ * Log all incoming request. By default, is true and print the configured `logger.requestFields`.
6
+ */
7
+ logRequest?: boolean;
8
+ /**
9
+ * A function to alter the log object before it's logged.
10
+ */
11
+ alterLog?: AlterLogCallback;
12
+ /**
13
+ * A function to log the server response.
14
+ */
15
+ onLogResponse?: ($ctx: BaseContext) => void;
16
+ }
17
+ declare global {
18
+ namespace TsED {
19
+ interface LoggerConfiguration extends Partial<PlatformLogRequestSettings> {
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @file Automatically generated by @tsed/barrels.
3
+ */
4
+ export * from "./domain/PlatformLogRequestSettings.js";
5
+ export * from "./services/PlatformLogRequestFactory.js";
6
+ export * from "./utils/defaultAlterLog.js";
7
+ export * from "./utils/defaultLogResponse.js";
@@ -0,0 +1,9 @@
1
+ import { type BaseContext, Configuration } from "@tsed/di";
2
+ import { defaultAlterLog } from "../utils/defaultAlterLog.js";
3
+ declare function factory(configuration: Configuration): {
4
+ alterLog: import("../index.js").AlterLogCallback | typeof defaultAlterLog;
5
+ onLogResponse: ($ctx: BaseContext) => void;
6
+ } | null;
7
+ export declare const PlatformLogRequestFactory: unique symbol;
8
+ export type PlatformLogRequestFactory = ReturnType<typeof factory>;
9
+ export {};
@@ -0,0 +1,14 @@
1
+ import type { BaseContext } from "@tsed/di";
2
+ export declare function defaultAlterLog(level: string, obj: Record<string, unknown>, ctx: BaseContext): {
3
+ method: any;
4
+ url: any;
5
+ route: any;
6
+ } | {
7
+ headers: any;
8
+ body: any;
9
+ query: any;
10
+ params: any;
11
+ method: any;
12
+ url: any;
13
+ route: any;
14
+ };
@@ -0,0 +1,2 @@
1
+ import type { BaseContext } from "@tsed/di";
2
+ export declare function defaultLogResponse($ctx: BaseContext): void;
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@tsed/platform-log-request",
3
+ "description": "Log request module for Ts.ED Framework (Express, Koa, Serverless)",
4
+ "type": "module",
5
+ "version": "8.0.0-alpha.10",
6
+ "source": "./src/index.ts",
7
+ "main": "./lib/esm/index.js",
8
+ "module": "./lib/esm/index.js",
9
+ "typings": "./lib/types/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./lib/types/index.d.ts",
13
+ "import": "./lib/esm/index.js",
14
+ "default": "./lib/cjs/index.js"
15
+ }
16
+ },
17
+ "scripts": {
18
+ "build": "yarn barrels && yarn build:ts",
19
+ "barrels": "barrels",
20
+ "test": "vitest run",
21
+ "build:ts": "tsc --build tsconfig.json",
22
+ "test:ci": "vitest run --coverage.thresholds.autoUpdate=true"
23
+ },
24
+ "dependencies": {
25
+ "tslib": "2.7.0"
26
+ },
27
+ "devDependencies": {
28
+ "@tsed/barrels": "8.0.0-alpha.10",
29
+ "@tsed/di": "8.0.0-alpha.10",
30
+ "@tsed/platform-middlewares": "8.0.0-alpha.10",
31
+ "@tsed/platform-params": "8.0.0-alpha.10",
32
+ "@tsed/typescript": "8.0.0-alpha.10",
33
+ "eslint": "9.12.0",
34
+ "typescript": "5.4.5",
35
+ "vitest": "2.1.2"
36
+ },
37
+ "peerDependencies": {
38
+ "@tsed/di": "8.0.0-alpha.10",
39
+ "@tsed/platform-middlewares": "8.0.0-alpha.10",
40
+ "@tsed/platform-params": "8.0.0-alpha.10"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "@tsed/di": {
44
+ "optional": false
45
+ },
46
+ "@tsed/platform-middlewares": {
47
+ "optional": false
48
+ },
49
+ "@tsed/platform-params": {
50
+ "optional": false
51
+ }
52
+ },
53
+ "repository": "https://github.com/tsedio/tsed",
54
+ "bugs": {
55
+ "url": "https://github.com/tsedio/tsed/issues"
56
+ },
57
+ "homepage": "https://github.com/tsedio/tsed/tree/production/packages/platform/platform-log-request",
58
+ "author": "Romain Lenzotti",
59
+ "license": "MIT",
60
+ "publishConfig": {
61
+ "tag": "alpha"
62
+ }
63
+ }
package/readme.md ADDED
@@ -0,0 +1,184 @@
1
+ <p style="text-align: center" align="center">
2
+ <a href="https://tsed.io" target="_blank"><img src="https://tsed.io/tsed-og.png" width="200" alt="Ts.ED logo"/></a>
3
+ </p>
4
+
5
+ <div align="center">
6
+ <h1>@tsed/platform-log-request</h1>
7
+
8
+ [![Build & Release](https://github.com/tsedio/tsed/workflows/Build%20&%20Release/badge.svg)](https://github.com/tsedio/tsed/actions?query=workflow%3A%22Build+%26+Release%22)
9
+ [![PR Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/tsedio/tsed/blob/master/CONTRIBUTING.md)
10
+ [![npm version](https://badge.fury.io/js/%40tsed%2Fcommon.svg)](https://badge.fury.io/js/%40tsed%2Fcommon)
11
+ [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
12
+ [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier)
13
+ [![github](https://img.shields.io/static/v1?label=Github%20sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/romakita)
14
+ [![opencollective](https://img.shields.io/static/v1?label=OpenCollective%20sponsor&message=%E2%9D%A4&logo=OpenCollective&color=%23fe8e86)](https://opencollective.com/tsed)
15
+
16
+ </div>
17
+
18
+ <div align="center">
19
+ <a href="https://tsed.io/">Website</a>
20
+ <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
21
+ <a href="https://tsed.io/getting-started/">Getting started</a>
22
+ <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
23
+ <a href="https://api.tsed.io/rest/slack/tsedio/tsed">Slack</a>
24
+ <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
25
+ <a href="https://twitter.com/TsED_io">Twitter</a>
26
+ </div>
27
+
28
+ <hr />
29
+
30
+ A package of Ts.ED framework. See website: https://tsed.io/
31
+
32
+ # Installation
33
+
34
+ ```bash
35
+ npm install --save @tsed/platform-log-request
36
+ ```
37
+
38
+ ## Configuration
39
+
40
+ ```ts
41
+ import {Configuration} from "@tsed/di";
42
+ import "@tsed/platform-log-request";
43
+ import {levels} from "@tsed/logger";
44
+
45
+ @Configuration({
46
+ /**
47
+ * Log all incoming request. By default, is true and print the configured `logger.requestFields`.
48
+ * @optional
49
+ */
50
+ // logRequest: true,
51
+ /**
52
+ * A function to alter the log object before it's logged.
53
+ * @optional
54
+ */
55
+ // alterLog: (level, data, $ctx) => {
56
+ // /// see example above
57
+ // },
58
+ /**
59
+ * A function to alter the log object before it's logged.
60
+ * @optional
61
+ */
62
+ // onLogEnd? : ($ctx: BaseContext) => void;
63
+ })
64
+ class Server {}
65
+ ```
66
+
67
+ ## Alter log request
68
+
69
+ By default, Ts.ED provide a default log object. You can alter this object by providing a function to the `alterLog` property.
70
+
71
+ Here his the default implementation:
72
+
73
+ ```typescript
74
+ export function defaultAlterLog(level: string, obj: Record<string, unknown>, ctx: BaseContext) {
75
+ const minimalLog = {
76
+ method: ctx.request.method,
77
+ url: ctx.request.url,
78
+ route: ctx.request.route || ctx.request.url,
79
+ ...obj
80
+ };
81
+
82
+ if (level === "info") {
83
+ return minimalLog;
84
+ }
85
+
86
+ return {
87
+ ...minimalLog,
88
+ headers: ctx.request.headers,
89
+ body: ctx.request.body,
90
+ query: ctx.request.query,
91
+ params: ctx.request.params
92
+ };
93
+ }
94
+ ```
95
+
96
+ Just give your own implementation to the `alterLog` property.
97
+
98
+ ```typescript
99
+ import {Configuration} from "@tsed/di";
100
+ import "@tsed/platform-log-request";
101
+ import {customAlterLog} from "./utils/customAlterLog.js";
102
+
103
+ @Configuration({
104
+ alterLog: customAlterLog
105
+ })
106
+ class Server {}
107
+ ```
108
+
109
+ ## Customize log response
110
+
111
+ By default, Ts.ED provide a default log response implementation.
112
+
113
+ Here his the default implementation:
114
+
115
+ ```typescript
116
+ export function defaultLogResponse($ctx: BaseContext) {
117
+ if ($ctx.response.statusCode >= 400) {
118
+ const error = $ctx.error as any | undefined;
119
+
120
+ $ctx.logger.error({
121
+ event: "request.end",
122
+ status: $ctx.response.statusCode,
123
+ status_code: String($ctx.response.statusCode),
124
+ state: "KO",
125
+ ...cleanObject({
126
+ error_name: error?.name || error?.code,
127
+ error_message: error?.message,
128
+ error_errors: error?.errors,
129
+ error_stack: error?.stack,
130
+ error_body: error?.body,
131
+ error_headers: error?.headers
132
+ })
133
+ });
134
+ } else {
135
+ $ctx.logger.info({
136
+ event: "request.end",
137
+ status: $ctx.response.statusCode,
138
+ status_code: String($ctx.response.statusCode),
139
+ state: "OK"
140
+ });
141
+ }
142
+ }
143
+ ```
144
+
145
+ Just give your own implementation to the `onLogResponse` property.
146
+
147
+ ```typescript
148
+ import {Configuration} from "@tsed/di";
149
+ import "@tsed/platform-log-request";
150
+ import {customOnLogResponse} from "./utils/customOnLogResponse.js";
151
+
152
+ @Configuration({
153
+ onLogResponse: customOnLogResponse
154
+ })
155
+ class Server {}
156
+ ```
157
+
158
+ ## Contributors
159
+
160
+ Please read [contributing guidelines here](https://tsed.io/contributing.html).
161
+
162
+ <a href="https://github.com/tsedio/tsed/graphs/contributors"><img src="https://opencollective.com/tsed/contributors.svg?width=890" /></a>
163
+
164
+ ## Backers
165
+
166
+ Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/tsed#backer)]
167
+
168
+ <a href="https://opencollective.com/tsed#backers" target="_blank"><img src="https://opencollective.com/tsed/tiers/backer.svg?width=890"></a>
169
+
170
+ ## Sponsors
171
+
172
+ Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/tsed#sponsor)]
173
+
174
+ ## License
175
+
176
+ The MIT License (MIT)
177
+
178
+ Copyright (c) 2016 - 2022 Romain Lenzotti
179
+
180
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
181
+
182
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
183
+
184
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,21 @@
1
+ // @ts-ignore
2
+ import {presets} from "@tsed/vitest/presets";
3
+ import {defineConfig} from "vitest/config";
4
+
5
+ export default defineConfig(
6
+ {
7
+ ...presets,
8
+ test: {
9
+ ...presets.test,
10
+ coverage: {
11
+ ...presets.test.coverage,
12
+ thresholds: {
13
+ statements: 100,
14
+ branches: 100,
15
+ functions: 100,
16
+ lines: 100
17
+ }
18
+ }
19
+ }
20
+ }
21
+ );