From 3fcbe3699d03ae74510b0b41414860c3d4ef1092 Mon Sep 17 00:00:00 2001 From: matt Date: Thu, 26 Dec 2019 16:22:56 -0800 Subject: [PATCH 1/3] feat: adds several convenience decorators This branch adds the following openapi convenience decorators - @deprecated - @tags - @response This branch adds the following features: - creates a `MethodMultiDecoratorFactory`, allowing multiple uses of the same decorator per method. - adds support for the schema keywords 'allOf', 'anyOf', 'oneOf', and 'not' in resolving schemas with the 'x-ts-type' extension - adds a `get-dist-file` to build as a utility to make it easier to debug a unit test file directly in vscode. --- .vscode/launch.json | 16 ++ .../repository-cloudant/package-lock.json | 6 + .../repository-mongodb/package-lock.json | 6 + acceptance/repository-mysql/package-lock.json | 6 + .../repository-postgresql/package-lock.json | 6 + benchmark/package-lock.json | 6 + docs/package-lock.json | 6 + examples/context/package-lock.json | 6 + .../express-composition/package-lock.json | 6 + examples/greeter-extension/package-lock.json | 6 + examples/greeting-app/package-lock.json | 6 + examples/hello-world/package-lock.json | 6 + examples/log-extension/package-lock.json | 6 + examples/metrics-prometheus/package-lock.json | 6 + examples/rpc-server/package-lock.json | 6 + examples/soap-calculator/package-lock.json | 6 + examples/todo-list/package-lock.json | 6 + examples/todo/package-lock.json | 6 + examples/todo/package.json | 2 +- .../authentication-passport/package-lock.json | 6 + extensions/health/package-lock.json | 6 + extensions/metrics/package-lock.json | 6 + packages/authentication/package-lock.json | 6 + packages/authorization/package-lock.json | 6 + packages/boot/package-lock.json | 6 + packages/build/bin/get-dist-file.js | 75 ++++++ packages/context/package-lock.json | 6 + packages/core/package-lock.json | 6 + packages/eslint-config/package-lock.json | 14 ++ packages/eslint-config/package.json | 3 + packages/http-caching-proxy/package-lock.json | 6 + packages/http-server/package-lock.json | 6 + packages/metadata/package-lock.json | 6 + .../__tests__/unit/decorator-factory.unit.ts | 162 +++++++++++++ packages/metadata/src/decorator-factory.ts | 90 +++++++ packages/model-api-builder/package-lock.json | 6 + .../openapi-spec-builder/package-lock.json | 6 + packages/openapi-v3/package-lock.json | 5 + packages/openapi-v3/package.json | 7 +- .../decorators/deprecated.decorator.unit.ts | 95 ++++++++ .../decorators/response.decorator.unit.ts | 216 +++++++++++++++++ .../unit/decorators/tags.decorator.unit.ts | 86 +++++++ .../unit/decorators/x-ts-type.unit.ts | 226 ++++++++++++++++++ .../src/build-responses-from-metadata.ts | 150 ++++++++++++ packages/openapi-v3/src/controller-spec.ts | 173 ++++++++++++-- .../src/decorators/deprecated.decorator.ts | 51 ++++ packages/openapi-v3/src/decorators/index.ts | 3 + .../src/decorators/response.decorator.ts | 50 ++++ .../src/decorators/tags.decorator.ts | 50 ++++ packages/openapi-v3/src/keys.ts | 46 +++- packages/openapi-v3/src/types.ts | 22 +- .../repository-json-schema/package-lock.json | 6 + packages/repository-tests/package-lock.json | 6 + packages/repository/package-lock.json | 6 + packages/rest-crud/package-lock.json | 6 + packages/rest-explorer/package-lock.json | 6 + packages/security/package-lock.json | 6 + packages/service-proxy/package-lock.json | 6 + packages/testlab/package-lock.json | 6 + packages/tsdocs/package-lock.json | 6 + sandbox/example/package-lock.json | 14 ++ 61 files changed, 1764 insertions(+), 26 deletions(-) create mode 100644 packages/build/bin/get-dist-file.js create mode 100644 packages/eslint-config/package-lock.json create mode 100644 packages/openapi-v3/src/__tests__/unit/decorators/deprecated.decorator.unit.ts create mode 100644 packages/openapi-v3/src/__tests__/unit/decorators/response.decorator.unit.ts create mode 100644 packages/openapi-v3/src/__tests__/unit/decorators/tags.decorator.unit.ts create mode 100644 packages/openapi-v3/src/__tests__/unit/decorators/x-ts-type.unit.ts create mode 100644 packages/openapi-v3/src/build-responses-from-metadata.ts create mode 100644 packages/openapi-v3/src/decorators/deprecated.decorator.ts create mode 100644 packages/openapi-v3/src/decorators/response.decorator.ts create mode 100644 packages/openapi-v3/src/decorators/tags.decorator.ts create mode 100644 sandbox/example/package-lock.json diff --git a/.vscode/launch.json b/.vscode/launch.json index c6adda413542..7715a6bc9e35 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -18,6 +18,22 @@ "0" ] }, + { + "type": "node", + "request": "launch", + "name": "Debug Current Test File", + "program": "${workspaceRoot}/packages/build/node_modules/.bin/_mocha", + "cwd": "${workspaceRoot}", + "autoAttachChildProcesses": true, + "args": [ + "--config", + "${workspaceRoot}/packages/build/config/.mocharc.json", + "-t", + "0", + "$(node ${workspaceRoot}/packages/build/bin/get-dist-file ${file})" + ], + "disableOptimisticBPs": true + }, { "type": "node", "request": "attach", diff --git a/acceptance/repository-cloudant/package-lock.json b/acceptance/repository-cloudant/package-lock.json index 09e7d240cabf..788ed8ffa221 100644 --- a/acceptance/repository-cloudant/package-lock.json +++ b/acceptance/repository-cloudant/package-lock.json @@ -529,6 +529,12 @@ "sshpk": "^1.7.0" } }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", diff --git a/acceptance/repository-mongodb/package-lock.json b/acceptance/repository-mongodb/package-lock.json index a292b607c5ad..49dc57ee2c65 100644 --- a/acceptance/repository-mongodb/package-lock.json +++ b/acceptance/repository-mongodb/package-lock.json @@ -193,6 +193,12 @@ "cldrjs": "^0.5.0" } }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", diff --git a/acceptance/repository-mysql/package-lock.json b/acceptance/repository-mysql/package-lock.json index b4baf4df1d7e..12241b9842bc 100644 --- a/acceptance/repository-mysql/package-lock.json +++ b/acceptance/repository-mysql/package-lock.json @@ -196,6 +196,12 @@ "cldrjs": "^0.5.0" } }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", diff --git a/acceptance/repository-postgresql/package-lock.json b/acceptance/repository-postgresql/package-lock.json index 0ffb40b276d5..74222b1fcda2 100644 --- a/acceptance/repository-postgresql/package-lock.json +++ b/acceptance/repository-postgresql/package-lock.json @@ -193,6 +193,12 @@ "cldrjs": "^0.5.0" } }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", diff --git a/benchmark/package-lock.json b/benchmark/package-lock.json index 742f8e8d1b0f..415f913639bd 100644 --- a/benchmark/package-lock.json +++ b/benchmark/package-lock.json @@ -623,6 +623,12 @@ "sshpk": "^1.7.0" } }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "hyperid": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hyperid/-/hyperid-2.0.2.tgz", diff --git a/docs/package-lock.json b/docs/package-lock.json index f86534c59762..7609f73d06c7 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -19,6 +19,12 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", diff --git a/examples/context/package-lock.json b/examples/context/package-lock.json index 90fd7e7cadd6..254c41b2c10c 100644 --- a/examples/context/package-lock.json +++ b/examples/context/package-lock.json @@ -557,6 +557,12 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/examples/express-composition/package-lock.json b/examples/express-composition/package-lock.json index 497f6ea80976..8fb59b393092 100644 --- a/examples/express-composition/package-lock.json +++ b/examples/express-composition/package-lock.json @@ -812,6 +812,12 @@ "toidentifier": "1.0.0" } }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/examples/greeter-extension/package-lock.json b/examples/greeter-extension/package-lock.json index 6e5912da9df6..079dd0595410 100644 --- a/examples/greeter-extension/package-lock.json +++ b/examples/greeter-extension/package-lock.json @@ -664,6 +664,12 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/examples/greeting-app/package-lock.json b/examples/greeting-app/package-lock.json index 3af902fae653..0a3ae3dccd08 100644 --- a/examples/greeting-app/package-lock.json +++ b/examples/greeting-app/package-lock.json @@ -664,6 +664,12 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/examples/hello-world/package-lock.json b/examples/hello-world/package-lock.json index a191b433a29d..424c4b257351 100644 --- a/examples/hello-world/package-lock.json +++ b/examples/hello-world/package-lock.json @@ -557,6 +557,12 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/examples/log-extension/package-lock.json b/examples/log-extension/package-lock.json index 5bceedeb7d1e..d4d37ee7c99f 100644 --- a/examples/log-extension/package-lock.json +++ b/examples/log-extension/package-lock.json @@ -664,6 +664,12 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/examples/metrics-prometheus/package-lock.json b/examples/metrics-prometheus/package-lock.json index 1f856644061d..8c6596192f0b 100644 --- a/examples/metrics-prometheus/package-lock.json +++ b/examples/metrics-prometheus/package-lock.json @@ -557,6 +557,12 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/examples/rpc-server/package-lock.json b/examples/rpc-server/package-lock.json index 8ef03b4959ec..9310b3cf43ef 100644 --- a/examples/rpc-server/package-lock.json +++ b/examples/rpc-server/package-lock.json @@ -812,6 +812,12 @@ "toidentifier": "1.0.0" } }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/examples/soap-calculator/package-lock.json b/examples/soap-calculator/package-lock.json index 60f5a276c981..934b0016f6be 100644 --- a/examples/soap-calculator/package-lock.json +++ b/examples/soap-calculator/package-lock.json @@ -962,6 +962,12 @@ "sshpk": "^1.7.0" } }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "httpntlm": { "version": "1.7.6", "resolved": "https://registry.npmjs.org/httpntlm/-/httpntlm-1.7.6.tgz", diff --git a/examples/todo-list/package-lock.json b/examples/todo-list/package-lock.json index 9a338c80c65f..3b8e0c10e242 100644 --- a/examples/todo-list/package-lock.json +++ b/examples/todo-list/package-lock.json @@ -758,6 +758,12 @@ "sshpk": "^1.7.0" } }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/examples/todo/package-lock.json b/examples/todo/package-lock.json index e9f42a9a4b1f..0ad0de7f2cc8 100644 --- a/examples/todo/package-lock.json +++ b/examples/todo/package-lock.json @@ -758,6 +758,12 @@ "sshpk": "^1.7.0" } }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/examples/todo/package.json b/examples/todo/package.json index 883bbc9a90f7..fc1824be021d 100644 --- a/examples/todo/package.json +++ b/examples/todo/package.json @@ -1,6 +1,6 @@ { "name": "@loopback/example-todo", - "version": "1.9.2", + "version": "1.9.4", "description": "Tutorial example on how to build an application with LoopBack 4.", "main": "index.js", "engines": { diff --git a/extensions/authentication-passport/package-lock.json b/extensions/authentication-passport/package-lock.json index e4b492a2e591..3c8e1a8f4600 100644 --- a/extensions/authentication-passport/package-lock.json +++ b/extensions/authentication-passport/package-lock.json @@ -91,6 +91,12 @@ "@types/mime": "*" } }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "passport": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz", diff --git a/extensions/health/package-lock.json b/extensions/health/package-lock.json index 5c5628b1fda0..c11f1d0a373d 100644 --- a/extensions/health/package-lock.json +++ b/extensions/health/package-lock.json @@ -15,6 +15,12 @@ "integrity": "sha512-SSB4O9/0NVv5mbQ5/MabnAyFfcpVFRVIJj1TZkG21HHgwXQGjosiQB3SBWC9pMCMUTNpWL9gUe//9mFFPQAdKw==", "dev": true }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "p-event": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.1.0.tgz", diff --git a/extensions/metrics/package-lock.json b/extensions/metrics/package-lock.json index 3649faee5c48..9154402ec852 100644 --- a/extensions/metrics/package-lock.json +++ b/extensions/metrics/package-lock.json @@ -267,6 +267,12 @@ "toidentifier": "1.0.0" } }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/packages/authentication/package-lock.json b/packages/authentication/package-lock.json index e311dc2e77b6..40752e13ec11 100644 --- a/packages/authentication/package-lock.json +++ b/packages/authentication/package-lock.json @@ -106,6 +106,12 @@ "safe-buffer": "^5.0.1" } }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "jsonwebtoken": { "version": "8.5.1", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", diff --git a/packages/authorization/package-lock.json b/packages/authorization/package-lock.json index 89034d0e0c5c..fa78edc95df2 100644 --- a/packages/authorization/package-lock.json +++ b/packages/authorization/package-lock.json @@ -44,6 +44,12 @@ "jsep": "^0.3.0" } }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", diff --git a/packages/boot/package-lock.json b/packages/boot/package-lock.json index e3c94be1eb79..78cec8acc3e3 100644 --- a/packages/boot/package-lock.json +++ b/packages/boot/package-lock.json @@ -87,6 +87,12 @@ "path-is-absolute": "^1.0.0" } }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", diff --git a/packages/build/bin/get-dist-file.js b/packages/build/bin/get-dist-file.js new file mode 100644 index 000000000000..418c4a408fff --- /dev/null +++ b/packages/build/bin/get-dist-file.js @@ -0,0 +1,75 @@ +#!/usr/bin/env node +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/build +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +/* +======== +This is used in the launch.json to enable you to debug a test file written in +typescript. This function attempts to convert the passed typescript file to +the best-gust output javascript file. + +It walks up the filesystem from the current file, stops at package.json, and +looks in `dist` + +Ideally, we could use the typescript compiler and tsconfig.json to get the +explicit output file instead of trying to guess it. + +Ex: +```jsonc +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug Current Test File", + "program": "${workspaceRoot}/packages/build/node_modules/.bin/_mocha", + "cwd": "${workspaceRoot}", + "autoAttachChildProcesses": true, + "args": [ + "--config", + "${workspaceRoot}/packages/build/config/.mocharc.json", + "-t", + "0", + "$(node ${workspaceRoot}/packages/build/bin/get-dist-file ${file})" + ], + "disableOptimisticBPs": true + } + ] +} +``` + +For your personal projects, you can sub directlry from loopback: +``` +"$(node ${workspaceRoot}/node_modules/@loopback/build/bin/get-dist-file ${file})" +``` +You still have to compile the package/project first. +======== +*/ + +'use strict'; +const path = require('path'); +const fs = require('fs'); + +function findDistFile(filename) { + const absolutePath = path.resolve(filename); + let currentDir = path.dirname(absolutePath); + let isPackageRoot = fs.existsSync(path.resolve(currentDir, 'package.json')); + while (!isPackageRoot) { + currentDir = path.join(currentDir, '..'); + isPackageRoot = fs.existsSync(path.resolve(currentDir, 'package.json')); + } + const base = path.resolve(currentDir); + const relative = path.relative(currentDir, absolutePath); + const resultPath = relative + .replace(/^src\//, 'dist/') + .replace(/\.ts$/, '.js'); + return path.resolve(base, resultPath); +} + +module.exports = findDistFile; +if (require.main === module) { + process.stdout.write(findDistFile(process.argv.splice(-1)[0])); +} diff --git a/packages/context/package-lock.json b/packages/context/package-lock.json index fbf91f637b27..280ff830af9b 100644 --- a/packages/context/package-lock.json +++ b/packages/context/package-lock.json @@ -45,6 +45,12 @@ "ms": "^2.1.1" } }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/packages/core/package-lock.json b/packages/core/package-lock.json index 03403782bc70..c1838135d6a5 100644 --- a/packages/core/package-lock.json +++ b/packages/core/package-lock.json @@ -24,6 +24,12 @@ "ms": "^2.1.1" } }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/packages/eslint-config/package-lock.json b/packages/eslint-config/package-lock.json new file mode 100644 index 000000000000..fca86ce859a8 --- /dev/null +++ b/packages/eslint-config/package-lock.json @@ -0,0 +1,14 @@ +{ + "name": "@loopback/eslint-config", + "version": "5.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + } + } +} diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 66fb380480c4..782c6627235a 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -24,5 +24,8 @@ }, "publishConfig": { "access": "public" + }, + "devDependencies": { + "@types/http-status": "^1.1.2" } } diff --git a/packages/http-caching-proxy/package-lock.json b/packages/http-caching-proxy/package-lock.json index 7465263c2e56..bf2e226075f5 100644 --- a/packages/http-caching-proxy/package-lock.json +++ b/packages/http-caching-proxy/package-lock.json @@ -411,6 +411,12 @@ "sshpk": "^1.7.0" } }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "iferr": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", diff --git a/packages/http-server/package-lock.json b/packages/http-server/package-lock.json index aa00ebb6450c..ce7878efd7c3 100644 --- a/packages/http-server/package-lock.json +++ b/packages/http-server/package-lock.json @@ -19,6 +19,12 @@ "@types/node": "*" } }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "p-event": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.1.0.tgz", diff --git a/packages/metadata/package-lock.json b/packages/metadata/package-lock.json index f5fd50171df0..7f17493451c1 100644 --- a/packages/metadata/package-lock.json +++ b/packages/metadata/package-lock.json @@ -30,6 +30,12 @@ "ms": "^2.1.1" } }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", diff --git a/packages/metadata/src/__tests__/unit/decorator-factory.unit.ts b/packages/metadata/src/__tests__/unit/decorator-factory.unit.ts index d942761399eb..1d3cc52b8327 100644 --- a/packages/metadata/src/__tests__/unit/decorator-factory.unit.ts +++ b/packages/metadata/src/__tests__/unit/decorator-factory.unit.ts @@ -8,6 +8,7 @@ import { ClassDecoratorFactory, DecoratorFactory, MethodDecoratorFactory, + MethodMultiDecoratorFactory, MethodParameterDecoratorFactory, ParameterDecoratorFactory, PropertyDecoratorFactory, @@ -523,6 +524,167 @@ describe('MethodDecoratorFactory for static methods', () => { }); }); +describe('MethodMultiDecoratorFactory', () => { + function methodMultiDecorator(spec: object | object[]): MethodDecorator { + if (Array.isArray(spec)) { + return MethodMultiDecoratorFactory.createDecorator('test', spec); + } else { + return MethodMultiDecoratorFactory.createDecorator('test', [spec]); + } + } + + class BaseController { + @methodMultiDecorator({x: 1}) + public myMethod() {} + + @methodMultiDecorator({foo: 1}) + @methodMultiDecorator({foo: 2}) + @methodMultiDecorator([{foo: 3}, {foo: 4}]) + public multiMethod() {} + } + + class SubController extends BaseController { + @methodMultiDecorator({y: 2}) + public myMethod() {} + + @methodMultiDecorator({bar: 1}) + @methodMultiDecorator([{bar: 2}, {bar: 3}]) + public multiMethod() {} + } + + describe('single-decorator methods', () => { + it('applies metadata to a method', () => { + const meta = Reflector.getOwnMetadata('test', BaseController.prototype); + expect(meta.myMethod).to.eql([{x: 1}]); + }); + + it('merges with base method metadata', () => { + const meta = Reflector.getOwnMetadata('test', SubController.prototype); + expect(meta.myMethod).to.eql([{x: 1}, {y: 2}]); + }); + + it('does not mutate base method metadata', () => { + const meta = Reflector.getOwnMetadata('test', BaseController.prototype); + expect(meta.myMethod).to.eql([{x: 1}]); + }); + }); + + describe('multi-decorator methods', () => { + it('applies metadata to a method', () => { + const meta = Reflector.getOwnMetadata('test', BaseController.prototype); + expect(meta.multiMethod).to.containDeep([ + {foo: 4}, + {foo: 3}, + {foo: 2}, + {foo: 1}, + ]); + }); + + it('merges with base method metadata', () => { + const meta = Reflector.getOwnMetadata('test', SubController.prototype); + expect(meta.multiMethod).to.containDeep([ + {foo: 4}, + {foo: 3}, + {foo: 2}, + {foo: 1}, + {bar: 3}, + {bar: 2}, + {bar: 1}, + ]); + }); + + it('does not mutate base method metadata', () => { + const meta = Reflector.getOwnMetadata('test', BaseController.prototype); + expect(meta.multiMethod).to.containDeep([ + {foo: 1}, + {foo: 2}, + {foo: 3}, + {foo: 4}, + ]); + }); + }); +}); +describe('MethodMultiDecoratorFactory for static methods', () => { + function methodMultiDecorator(spec: object | object[]): MethodDecorator { + if (Array.isArray(spec)) { + return MethodMultiDecoratorFactory.createDecorator('test', spec); + } else { + return MethodMultiDecoratorFactory.createDecorator('test', [spec]); + } + } + + class BaseController { + @methodMultiDecorator({x: 1}) + static myMethod() {} + + @methodMultiDecorator({foo: 1}) + @methodMultiDecorator({foo: 2}) + @methodMultiDecorator([{foo: 3}, {foo: 4}]) + static multiMethod() {} + } + + class SubController extends BaseController { + @methodMultiDecorator({y: 2}) + static myMethod() {} + + @methodMultiDecorator({bar: 1}) + @methodMultiDecorator([{bar: 2}, {bar: 3}]) + static multiMethod() {} + } + + describe('single-decorator methods', () => { + it('applies metadata to a method', () => { + const meta = Reflector.getOwnMetadata('test', BaseController); + expect(meta.myMethod).to.eql([{x: 1}]); + }); + + it('merges with base method metadata', () => { + const meta = Reflector.getOwnMetadata('test', SubController); + expect(meta.myMethod).to.eql([{x: 1}, {y: 2}]); + }); + + it('does not mutate base method metadata', () => { + const meta = Reflector.getOwnMetadata('test', BaseController); + expect(meta.myMethod).to.eql([{x: 1}]); + }); + }); + + describe('multi-decorator methods', () => { + it('applies metadata to a method', () => { + const meta = Reflector.getOwnMetadata('test', BaseController); + expect(meta.multiMethod).to.containDeep([ + {foo: 4}, + {foo: 3}, + {foo: 2}, + {foo: 1}, + ]); + }); + + it('merges with base method metadata', () => { + const meta = Reflector.getOwnMetadata('test', SubController); + expect(meta.multiMethod).to.containDeep([ + {foo: 4}, + {foo: 3}, + {foo: 2}, + {foo: 1}, + {bar: 3}, + {bar: 2}, + {bar: 1}, + ]); + }); + + it('does not mutate base method metadata', () => { + const meta = Reflector.getOwnMetadata('test', BaseController); + expect(meta.multiMethod).to.containDeep([ + {foo: 1}, + {foo: 2}, + {foo: 3}, + {foo: 4}, + ]); + }); + }); +}); + describe('ParameterDecoratorFactory', () => { /** * Define `@parameterDecorator(spec)` diff --git a/packages/metadata/src/decorator-factory.ts b/packages/metadata/src/decorator-factory.ts index 3c8a10dbbc14..47bec82986a2 100644 --- a/packages/metadata/src/decorator-factory.ts +++ b/packages/metadata/src/decorator-factory.ts @@ -789,3 +789,93 @@ export class MethodParameterDecoratorFactory extends DecoratorFactory< ); } } + +/** + * Factory for an append-array of method-level decorators + * The @response metadata for a method is an array. + * Each item in the array should be a single value, containing + * a response code and a single spec or Model. This should allow: + * ```ts + * @response(200, MyFirstModel) + * @response(403, [NotAuthorizedReasonOne, NotAuthorizedReasonTwo]) + * @response(404, NotFoundOne) + * @response(404, NotFoundTwo) + * @response(409, {schema: {}}) + * public async myMethod() {} + * ``` + * + * In the case that a ResponseObject is passed, it becomes the + * default for description/content, and if possible, further Models are + * incorporated as a `oneOf: []` array. + * + * In the case that a ReferenceObject is passed, it and it alone is used, since + * references can be external and we cannot `oneOf` their content. + * + * The factory creates and updates an array of items T[], and the getter + * provides the values as that array. + */ +export class MethodMultiDecoratorFactory extends MethodDecoratorFactory< + T[] +> { + private getOrInitMetadata( + meta: MetadataMap, + target: Object, + methodName?: string, + ) { + const method = methodName ? methodName : ''; + let methodMeta = meta[method]; + if (methodMeta == null) { + // Initialize the method metadata + methodMeta = []; + meta[method] = methodMeta; + } + return methodMeta; + } + + protected mergeWithInherited( + inheritedMetadata: MetadataMap, + target: Object, + methodName?: string, + ) { + inheritedMetadata = inheritedMetadata || {}; + + inheritedMetadata[methodName!] = this._mergeArray( + inheritedMetadata[methodName!], + this.withTarget(this.spec, target), + ); + + return inheritedMetadata; + } + + protected mergeWithOwn( + ownMetadata: MetadataMap, + target: Object, + methodName?: string, + methodDescriptor?: TypedPropertyDescriptor | number, + ) { + ownMetadata = ownMetadata || {}; + ownMetadata[methodName!] = this._mergeArray( + ownMetadata[methodName!], + this.withTarget(this.spec, target), + ); + return ownMetadata; + } + + private _mergeArray(result: T[], methodMeta: T | T[]) { + if (!result) { + if (Array.isArray(methodMeta)) { + result = methodMeta; + } else { + result = [methodMeta]; + } + } else { + if (Array.isArray(methodMeta)) { + result.push(...methodMeta); + } else { + result.push(methodMeta); + } + } + + return result; + } +} diff --git a/packages/model-api-builder/package-lock.json b/packages/model-api-builder/package-lock.json index 1457df48f8aa..1e47c57189aa 100644 --- a/packages/model-api-builder/package-lock.json +++ b/packages/model-api-builder/package-lock.json @@ -9,6 +9,12 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.12.tgz", "integrity": "sha512-SSB4O9/0NVv5mbQ5/MabnAyFfcpVFRVIJj1TZkG21HHgwXQGjosiQB3SBWC9pMCMUTNpWL9gUe//9mFFPQAdKw==", "dev": true + }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true } } } diff --git a/packages/openapi-spec-builder/package-lock.json b/packages/openapi-spec-builder/package-lock.json index 7ce8ad8f0bc5..766042be444b 100644 --- a/packages/openapi-spec-builder/package-lock.json +++ b/packages/openapi-spec-builder/package-lock.json @@ -10,6 +10,12 @@ "integrity": "sha512-SSB4O9/0NVv5mbQ5/MabnAyFfcpVFRVIJj1TZkG21HHgwXQGjosiQB3SBWC9pMCMUTNpWL9gUe//9mFFPQAdKw==", "dev": true }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "openapi3-ts": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-1.3.0.tgz", diff --git a/packages/openapi-v3/package-lock.json b/packages/openapi-v3/package-lock.json index f41855c464c9..173b9776b31d 100644 --- a/packages/openapi-v3/package-lock.json +++ b/packages/openapi-v3/package-lock.json @@ -97,6 +97,11 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==" + }, "is-arguments": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", diff --git a/packages/openapi-v3/package.json b/packages/openapi-v3/package.json index 6dcfdcf8a368..5d1a79eb8854 100644 --- a/packages/openapi-v3/package.json +++ b/packages/openapi-v3/package.json @@ -6,12 +6,13 @@ "node": ">=8.9" }, "dependencies": { - "@loopback/repository-json-schema": "^1.11.3", "@loopback/core": "^1.12.0", + "@loopback/repository-json-schema": "^1.11.3", "debug": "^4.1.1", + "http-status": "^1.4.2", + "json-merge-patch": "^0.2.3", "lodash": "^4.17.15", - "openapi3-ts": "^1.3.0", - "json-merge-patch": "^0.2.3" + "openapi3-ts": "^1.3.0" }, "devDependencies": { "@loopback/build": "^3.0.0", diff --git a/packages/openapi-v3/src/__tests__/unit/decorators/deprecated.decorator.unit.ts b/packages/openapi-v3/src/__tests__/unit/decorators/deprecated.decorator.unit.ts new file mode 100644 index 000000000000..65984a6fbe2c --- /dev/null +++ b/packages/openapi-v3/src/__tests__/unit/decorators/deprecated.decorator.unit.ts @@ -0,0 +1,95 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/openapi-v3 +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {anOpenApiSpec} from '@loopback/openapi-spec-builder'; +import {expect} from '@loopback/testlab'; +import {getControllerSpec} from '../../..'; +import {api, deprecated, get} from '../../../decorators'; + +describe('deprecation decorator', () => { + it('Returns a spec with all the items decorated from the class level', () => { + const expectedSpec = anOpenApiSpec() + .withOperationReturningString('get', '/greet', 'greet') + .withOperationReturningString('get', '/echo', 'echo') + .build(); + + @api(expectedSpec) + @deprecated() + class MyController { + greet() { + return 'Hello world!'; + } + echo() { + return 'Hello world!'; + } + } + + const actualSpec = getControllerSpec(MyController); + expect(actualSpec.paths['/greet'].get.deprecated).to.eql(true); + expect(actualSpec.paths['/echo'].get.deprecated).to.eql(true); + }); + + it('Returns a spec where only one method is deprecated', () => { + class MyController { + @get('/greet') + greet() { + return 'Hello world!'; + } + + @get('/echo') + @deprecated() + echo() { + return 'Hello world!'; + } + } + + const actualSpec = getControllerSpec(MyController); + expect(actualSpec.paths['/greet'].get.deprecated).to.be.undefined(); + expect(actualSpec.paths['/echo'].get.deprecated).to.eql(true); + }); + + it('Allows a method to override the deprecation of a class', () => { + @deprecated() + class MyController { + @get('/greet') + greet() { + return 'Hello world!'; + } + + @get('/echo') + echo() { + return 'Hello world!'; + } + + @get('/yell') + @deprecated(false) + yell() { + return 'HELLO WORLD!'; + } + } + const actualSpec = getControllerSpec(MyController); + expect(actualSpec.paths['/greet'].get.deprecated).to.eql(true); + expect(actualSpec.paths['/echo'].get.deprecated).to.eql(true); + expect(actualSpec.paths['/yell'].get.deprecated).to.be.undefined(); + }); + + it('Allows a class to not be decorated with @deprecated at all', () => { + class MyController { + @get('/greet') + greet() { + return 'Hello world!'; + } + + @get('/echo') + echo() { + return 'Hello world!'; + } + } + + const actualSpec = getControllerSpec(MyController); + expect(actualSpec.paths['/greet'].get.deprecated).to.be.undefined(); + expect(actualSpec.paths['/echo'].get.deprecated).to.be.undefined(); + }); +}); diff --git a/packages/openapi-v3/src/__tests__/unit/decorators/response.decorator.unit.ts b/packages/openapi-v3/src/__tests__/unit/decorators/response.decorator.unit.ts new file mode 100644 index 000000000000..659d09d1e401 --- /dev/null +++ b/packages/openapi-v3/src/__tests__/unit/decorators/response.decorator.unit.ts @@ -0,0 +1,216 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/openapi-v3 +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Model, model, property} from '@loopback/repository'; +import {expect} from '@loopback/testlab'; +import * as httpStatus from 'http-status'; +import {ResponseObject} from 'openapi3-ts'; +import {getControllerSpec} from '../../..'; +import {get, response} from '../../../decorators'; + +describe('@response decorator', () => { + it('allows a class to not be decorated with @response at all', () => { + class MyController { + @get('/greet') + greet() { + return 'Hello world!'; + } + } + + const actualSpec = getControllerSpec(MyController); + expect(actualSpec.paths['/greet'].get.responses['200'].description).to.eql( + 'Return value of MyController.greet', + ); + }); + + context('with response models', () => { + @model() + class SuccessModel extends Model { + constructor(err: Partial) { + super(err); + } + @property({default: 'ok'}) + message: string; + } + + @model() + class FooError extends Model { + constructor(err: Partial) { + super(err); + } + @property({default: 'foo'}) + foo: string; + } + + @model() + class BarError extends Model { + constructor(err: Partial) { + super(err); + } + @property({default: 'bar'}) + bar: string; + } + + const successSchema: ResponseObject = { + description: httpStatus['200_MESSAGE'], + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/SuccessModel', + }, + }, + }, + }; + + const fooBarSchema: ResponseObject = { + description: httpStatus['404_MESSAGE'], + content: { + 'application/json': { + schema: { + anyOf: [ + {$ref: '#/components/schemas/BarError'}, + {$ref: '#/components/schemas/FooError'}, + ], + }, + }, + }, + }; + + it('supports a single @response decorator', () => { + class MyController { + @get('/greet') + @response(200, SuccessModel) + greet() { + return new SuccessModel({message: 'Hello, world'}); + } + } + + const actualSpec = getControllerSpec(MyController); + expect(actualSpec.paths['/greet'].get.responses[200]).to.eql( + successSchema, + ); + expect( + actualSpec.components?.schemas?.SuccessModel, + ).to.not.be.undefined(); + }); + + it('supports multiple @response decorators on a method', () => { + class MyController { + @get('/greet') + @response(200, SuccessModel) + @response(404, FooError) + @response(404, BarError) + greet() { + throw new FooError({foo: 'bar'}); + } + } + + const actualSpec = getControllerSpec(MyController); + expect(actualSpec.paths['/greet'].get.responses[404]).to.eql( + fooBarSchema, + ); + expect(actualSpec.components?.schemas?.FooError).to.not.be.undefined(); + expect(actualSpec.components?.schemas?.BarError).to.not.be.undefined(); + expect( + actualSpec.components?.schemas?.SuccessModel, + ).to.not.be.undefined(); + }); + it('supports multiple @response decorators with an array of models', () => { + class MyController { + @get('/greet') + @response(200, SuccessModel) + @response(404, [BarError, FooError]) + greet() { + throw new BarError({bar: 'baz'}); + } + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec.paths['/greet'].get.responses[404]).to.eql( + fooBarSchema, + ); + expect(actualSpec.components?.schemas?.FooError).to.not.be.undefined(); + expect(actualSpec.components?.schemas?.BarError).to.not.be.undefined(); + expect( + actualSpec.components?.schemas?.SuccessModel, + ).to.not.be.undefined(); + }); + + context('with complex responses', () => { + const FIRST_SCHEMA = { + type: 'object', + properties: { + x: { + type: 'int', + default: 1, + }, + y: { + type: 'string', + default: '2', + }, + }, + }; + + const SECOND_SCHEMA = { + type: 'string', + format: 'base64', + }; + + class MyController { + @get('/greet', { + responses: { + 200: { + description: 'Unknown', + content: { + 'application/json': {schema: FIRST_SCHEMA}, + }, + }, + }, + }) + @response(200, SECOND_SCHEMA, {contentType: 'application/pdf'}) + @response(200, SuccessModel, {contentType: 'application/jsonc'}) + @response(404, [FooError, BarError], {contentType: 'application/jsonc'}) + greet() { + return new SuccessModel({message: 'Hello, world!'}); + } + } + + const actualSpec = getControllerSpec(MyController); + expect( + actualSpec.paths['/greet'].get.responses[200].content[ + 'application/json' + ], + ).to.not.be.undefined(); + expect( + actualSpec.paths['/greet'].get.responses[200].content[ + 'application/pdf' + ], + ).to.not.be.undefined(); + + expect( + actualSpec.paths['/greet'].get.responses[200].content[ + 'application/jsonc' + ], + ).to.not.be.undefined(); + expect( + actualSpec.paths['/greet'].get.responses[200].content[ + 'application/json' + ].schema, + ).to.eql(FIRST_SCHEMA); + + expect( + actualSpec.paths['/greet'].get.responses[200].content['application/pdf'] + .schema, + ).to.eql(SECOND_SCHEMA); + + expect( + actualSpec.paths['/greet'].get.responses[200].content[ + 'application/jsonc' + ].schema, + ).to.eql({$ref: '#/components/schemas/SuccessModel'}); + }); + }); +}); diff --git a/packages/openapi-v3/src/__tests__/unit/decorators/tags.decorator.unit.ts b/packages/openapi-v3/src/__tests__/unit/decorators/tags.decorator.unit.ts new file mode 100644 index 000000000000..85dc79710b97 --- /dev/null +++ b/packages/openapi-v3/src/__tests__/unit/decorators/tags.decorator.unit.ts @@ -0,0 +1,86 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/openapi-v3 +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect} from '@loopback/testlab'; +import {getControllerSpec} from '../../..'; +import {get, tags} from '../../../decorators'; + +describe('@tags decorator', () => { + it('Allows a class to not be decorated with @tags at all', () => { + class MyController { + @get('/greet') + greet() { + return 'Hello world!'; + } + + @get('/echo') + echo() { + return 'Hello world!'; + } + } + + const actualSpec = getControllerSpec(MyController); + expect(actualSpec.paths['/greet'].get.tags).to.be.undefined(); + }); + + it('Allows a class to decorate methods with @tags', () => { + @tags(['Foo', 'Bar']) + class MyController { + @get('/greet') + greet() { + return 'Hello world!'; + } + + @get('/echo') + echo() { + return 'Hello world!'; + } + } + + const actualSpec = getControllerSpec(MyController); + expect(actualSpec.paths['/greet'].get.tags).to.eql(['Foo', 'Bar']); + expect(actualSpec.paths['/echo'].get.tags).to.eql(['Foo', 'Bar']); + }); + + it('Allows method @tags to override controller @tags', () => { + @tags(['Foo', 'Bar']) + class MyController { + @get('/greet') + greet() { + return 'Hello world!'; + } + + @get('/echo') + @tags(['Baz']) + echo() { + return 'Hello world!'; + } + } + + const actualSpec = getControllerSpec(MyController); + expect(actualSpec.paths['/greet'].get.tags).to.eql(['Foo', 'Bar']); + expect(actualSpec.paths['/echo'].get.tags).to.eql(['Baz']); + }); + + it('Allows @tags with options to append', () => { + @tags(['Foo']) + class MyController { + @get('/greet') + greet() { + return 'Hello world!'; + } + + @get('/echo') + @tags(['Bar'], {append: true}) + echo() { + return 'Hello world!'; + } + } + + const actualSpec = getControllerSpec(MyController); + expect(actualSpec.paths['/greet'].get.tags).to.eql(['Foo']); + expect(actualSpec.paths['/echo'].get.tags).to.eql(['Foo', 'Bar']); + }); +}); diff --git a/packages/openapi-v3/src/__tests__/unit/decorators/x-ts-type.unit.ts b/packages/openapi-v3/src/__tests__/unit/decorators/x-ts-type.unit.ts new file mode 100644 index 000000000000..4d978a10940d --- /dev/null +++ b/packages/openapi-v3/src/__tests__/unit/decorators/x-ts-type.unit.ts @@ -0,0 +1,226 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/openapi-v3 +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT +import {Model, model, property} from '@loopback/repository'; +import {expect} from '@loopback/testlab'; +import {RequestBodyObject, ResponseObject} from 'openapi3-ts'; +import {getControllerSpec} from '../../..'; +import {get, post, requestBody} from '../../../decorators'; + +describe('x-ts-type is converted in the right places', () => { + // setup the models for use + @model() + class TestRequest extends Model { + @property({default: 1}) + value: number; + } + @model() + class SuccessModel extends Model { + constructor(err: Partial) { + super(err); + } + @property({default: 'ok'}) + message: string; + } + + @model() + class FooError extends Model { + constructor(err: Partial) { + super(err); + } + @property({default: 'foo'}) + foo: string; + } + + @model() + class BarError extends Model { + constructor(err: Partial) { + super(err); + } + @property({default: 'bar'}) + bar: string; + } + + const testRequestSchema: RequestBodyObject = { + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/TestRequest', + }, + }, + }, + }; + const successSchema: ResponseObject = { + description: 'Success', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/SuccessModel', + }, + }, + }, + }; + + const notSchema: ResponseObject = { + description: 'Failure', + content: { + 'application/json': { + schema: { + not: {$ref: '#/components/schemas/BarError'}, + }, + }, + }, + }; + const fooBarSchema = (k: 'anyOf' | 'allOf' | 'oneOf'): ResponseObject => ({ + description: 'Failure', + content: { + 'application/json': { + schema: { + [k]: [ + {$ref: '#/components/schemas/FooError'}, + {$ref: '#/components/schemas/BarError'}, + ], + }, + }, + }, + }); + + it('Allows a simple request schema', () => { + class MyController { + @post('/greet') + greet(@requestBody() body: TestRequest) { + return 'Hello world!'; + } + } + const actualSpec = getControllerSpec(MyController); + expect(actualSpec.paths['/greet'].post.requestBody).to.eql( + testRequestSchema, + ); + }); + + it('Allows for a response schema using the spec', () => { + class MyController { + @get('/greet', { + responses: { + 200: { + description: 'Success', + content: { + 'application/json': { + schema: { + 'x-ts-type': SuccessModel, + }, + }, + }, + }, + }, + }) + greet() { + return new SuccessModel({message: 'hello, world'}); + } + } + const actualSpec = getControllerSpec(MyController); + expect(actualSpec.paths['/greet'].get.responses[200]).to.eql(successSchema); + expect(actualSpec.components?.schemas?.SuccessModel).to.not.be.undefined(); + }); + + it('Allows `anyOf` responses', () => { + class MyController { + @get('/greet', { + responses: { + 404: { + description: 'Failure', + content: { + 'application/json': { + schema: { + anyOf: [{'x-ts-type': FooError}, {'x-ts-type': BarError}], + }, + }, + }, + }, + }, + }) + greet() { + throw new FooError({foo: 'foo'}); + } + } + const actualSpec = getControllerSpec(MyController); + expect(actualSpec.paths['/greet'].get.responses[404]).to.eql( + fooBarSchema('anyOf'), + ); + }); + it('Allows `allOf` responses', () => { + class MyController { + @get('/greet', { + responses: { + 404: { + description: 'Failure', + content: { + 'application/json': { + schema: { + allOf: [{'x-ts-type': FooError}, {'x-ts-type': BarError}], + }, + }, + }, + }, + }, + }) + greet() { + throw new FooError({foo: 'foo'}); + } + } + const actualSpec = getControllerSpec(MyController); + expect(actualSpec.paths['/greet'].get.responses[404]).to.eql( + fooBarSchema('allOf'), + ); + }); + + it('Allows `oneOf` responses', () => { + class MyController { + @get('/greet', { + responses: { + 404: { + description: 'Failure', + content: { + 'application/json': { + schema: { + oneOf: [{'x-ts-type': FooError}, {'x-ts-type': BarError}], + }, + }, + }, + }, + }, + }) + greet() { + throw new FooError({foo: 'foo'}); + } + } + const actualSpec = getControllerSpec(MyController); + expect(actualSpec.paths['/greet'].get.responses[404]).to.eql( + fooBarSchema('oneOf'), + ); + }); + it('Allows `not` responses', () => { + class MyController { + @get('/greet', { + responses: { + 404: { + description: 'Failure', + content: { + 'application/json': { + schema: { + not: {'x-ts-type': BarError}, + }, + }, + }, + }, + }, + }) + greet() { + throw new FooError({foo: 'foo'}); + } + } + const actualSpec = getControllerSpec(MyController); + expect(actualSpec.paths['/greet'].get.responses[404]).to.eql(notSchema); + }); +}); diff --git a/packages/openapi-v3/src/build-responses-from-metadata.ts b/packages/openapi-v3/src/build-responses-from-metadata.ts new file mode 100644 index 000000000000..30a532d36e36 --- /dev/null +++ b/packages/openapi-v3/src/build-responses-from-metadata.ts @@ -0,0 +1,150 @@ +import {Model} from '@loopback/repository'; +import _ from 'lodash'; +import { + ContentObject, + OperationObject, + ResponseDecoratorMetadata, + ResponseModelOrSpec, + ResponseObject, +} from './types'; + +declare type ContentMap = Map; +declare type ResponseMap = Map< + number, + {description: string; content: ContentMap} +>; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function isModel(c: any): c is T { + return c && c.prototype instanceof Model; +} + +/** + * Reducer which builds the operation responses + */ +function reduceSpecContent( + specContents: ContentObject, + [contentType, modelOrSpecs]: [string, ResponseModelOrSpec[]], +): ContentObject { + if (Array.isArray(modelOrSpecs) && modelOrSpecs.length > 1) { + specContents[contentType] = { + schema: { + anyOf: modelOrSpecs.map(m => { + if (isModel(m)) { + return {'x-ts-type': m}; + } else { + return m; + } + }), + }, + }; + } else { + const modelOrSpec = Array.isArray(modelOrSpecs) + ? modelOrSpecs[0] + : modelOrSpecs; + if (isModel(modelOrSpec)) { + specContents[contentType] = { + schema: {'x-ts-type': modelOrSpec}, + }; + } else { + specContents[contentType] = { + schema: modelOrSpec, + }; + } + } + return specContents; +} + +/** + * Reducer which builds the content sections of the operation responses + */ +function reduceSpecResponses( + specResponses: ResponseObject, + [responseCode, c]: [number, {description: string; content: ContentMap}], +): ResponseObject { + const responseContent = c.content; + // check if there is an existing block, from something like an inhered @op spec + if (Object.prototype.hasOwnProperty.call(specResponses, responseCode)) { + // we might need to merge + const content = Array.from(responseContent).reduce( + reduceSpecContent, + specResponses[responseCode].content as ContentObject, + ); + + specResponses[responseCode] = { + description: c.description, + content, + }; + } else { + const content = Array.from(responseContent).reduce( + reduceSpecContent, + {} as ContentObject, + ); + + specResponses[responseCode] = { + description: c.description, + content, + }; + } + return specResponses; +} + +/** + * This function takes an array of flat-ish data: + * ``` + * [ + * { responseCode, contentType, description, modelOrSpec }, + * { responseCode, contentType, description, modelOrSpec }, + * ] + * ``` + * and turns it into a multi-map structure that more closely aligns with + * the final json + * ``` + * Map{ [code, Map{[contentType, modelOrSpec], [contentType, modelOrSpec]}]} + * ``` + */ +function buildMapsFromMetadata( + metadata: ResponseDecoratorMetadata, +): ResponseMap { + const builder: ResponseMap = new Map(); + metadata.forEach(r => { + if (builder.has(r.responseCode)) { + const responseRef = builder.get(r.responseCode); + const codeRef = responseRef?.content; + + if (codeRef?.has(r.contentType)) { + // eslint-disable-next-line no-unused-expressions + codeRef.get(r.contentType)?.push(r.responseModelOrSpec); + } else { + // eslint-disable-next-line no-unused-expressions + codeRef?.set(r.contentType, [r.responseModelOrSpec]); + } + } else { + const codeRef = new Map(); + codeRef.set(r.contentType, [r.responseModelOrSpec]); + builder.set(r.responseCode, { + description: r.description, + content: codeRef, + }); + } + }); + return builder; +} +export function buildResponsesFromMetadata( + metadata: ResponseDecoratorMetadata, + existingOperation?: OperationObject, +): OperationObject { + const builder = buildMapsFromMetadata(metadata); + const base = existingOperation + ? _.cloneDeep(existingOperation.responses) + : {}; + // Now, mega-reduce. + const responses: ResponseObject = Array.from(builder).reduce( + reduceSpecResponses, + base as ResponseObject, + ); + + return { + responses, + }; +} diff --git a/packages/openapi-v3/src/controller-spec.ts b/packages/openapi-v3/src/controller-spec.ts index 41249c151291..9ee30712f154 100644 --- a/packages/openapi-v3/src/controller-spec.ts +++ b/packages/openapi-v3/src/controller-spec.ts @@ -10,6 +10,7 @@ import { JsonSchemaOptions, } from '@loopback/repository-json-schema'; import {includes} from 'lodash'; +import {buildResponsesFromMetadata} from './build-responses-from-metadata'; import {resolveSchema} from './generate-schema'; import {jsonToSchemaObject, SchemaRef} from './json-to-schema'; import {OAI3Keys} from './keys'; @@ -22,9 +23,11 @@ import { PathObject, ReferenceObject, RequestBodyObject, + ResponseDecoratorMetadata, ResponseObject, SchemaObject, SchemasObject, + TagsDecoratorMetadata, } from './types'; const debug = require('debug')('loopback:openapi3:metadata:controller-spec'); @@ -77,6 +80,48 @@ function resolveControllerSpec(constructor: Function): ControllerSpec { spec = {paths: {}}; } + const isClassDeprecated = MetadataInspector.getClassMetadata( + OAI3Keys.DEPRECATED_CLASS_KEY, + constructor, + ); + + const classTags = MetadataInspector.getClassMetadata( + OAI3Keys.TAGS_CLASS_KEY, + constructor, + ); + + if (isClassDeprecated) { + debug(' using class-level @deprecated()'); + } + + if (classTags) { + debug(' using class-level @tags()'); + } + + if (isClassDeprecated || classTags) { + for (const path of Object.keys(spec.paths)) { + for (const method of Object.keys(spec.paths[path])) { + if (isClassDeprecated) { + spec.paths[path][method].deprecated = true; + } + + if (classTags) { + if ( + classTags.append && + spec.paths[path][method].tags && + spec.paths[path][method].tags.length + ) { + spec.paths[path][method].tags = spec.paths[path][ + method + ].tags.concat(classTags.tags); + } else { + spec.paths[path][method].tags = classTags.tags; + } + } + } + } + } + let endpoints = MetadataInspector.getAllMethodMetadata( OAI3Keys.METHODS_KEY, @@ -91,6 +136,24 @@ function resolveControllerSpec(constructor: Function): ControllerSpec { const verb = endpoint.verb!; const path = endpoint.path!; + const isMethodDeprecated = MetadataInspector.getMethodMetadata( + OAI3Keys.DEPRECATED_METHOD_KEY, + constructor.prototype, + op, + ); + + const methodTags = MetadataInspector.getMethodMetadata< + TagsDecoratorMetadata + >(OAI3Keys.TAGS_METHOD_KEY, constructor.prototype, op); + + if (isMethodDeprecated) { + debug(' using method-level deprecation via @deprecated()'); + } + + if (methodTags) { + debug(' using method-level tags via @tags()'); + } + let endpointName = ''; /* istanbul ignore if */ if (debug.enabled) { @@ -106,13 +169,55 @@ function resolveControllerSpec(constructor: Function): ControllerSpec { }; let operationSpec = endpoint.spec; + + const decoratedResponses = MetadataInspector.getMethodMetadata< + ResponseDecoratorMetadata + >(OAI3Keys.RESPONSE_METHOD_KEY, constructor.prototype, op); + if (!operationSpec) { - // The operation was defined via @operation(verb, path) with no spec - operationSpec = { - responses: defaultResponse, - }; + if (decoratedResponses) { + operationSpec = buildResponsesFromMetadata(decoratedResponses); + } else { + // The operation was defined via @operation(verb, path) with no spec + operationSpec = { + responses: defaultResponse, + }; + } endpoint.spec = operationSpec; + } else if (decoratedResponses) { + operationSpec = buildResponsesFromMetadata( + decoratedResponses, + operationSpec, + ); + } + + // Prescedence: method decorator > class decorator > operationSpec > undefined + const deprecationSpec = + isMethodDeprecated ?? + isClassDeprecated ?? + operationSpec.deprecated ?? + false; + + if (deprecationSpec) { + operationSpec.deprecated = true; } + + if (classTags && !operationSpec.tags) { + operationSpec.tags = classTags.tags; + } + + if (methodTags) { + if ( + methodTags.append && + operationSpec.tags && + operationSpec.tags.length + ) { + operationSpec.tags = operationSpec.tags.concat(methodTags.tags); + } else { + operationSpec.tags = methodTags.tags; + } + } + debug(' operation for method %s: %j', op, endpoint); debug(' spec responses for method %s: %o', op, operationSpec.responses); @@ -233,6 +338,8 @@ function resolveControllerSpec(constructor: Function): ControllerSpec { return spec; } +declare type MixKey = 'allOf' | 'anyOf' | 'oneOf'; +const SCHEMA_ARR_KEYS: MixKey[] = ['allOf', 'anyOf', 'oneOf']; /** * Resolve the x-ts-type in the schema object * @param spec - Controller spec @@ -248,24 +355,50 @@ function processSchemaExtensions( assignRelatedSchemas(spec, schema.definitions); delete schema.definitions; - if (isReferenceObject(schema)) return; + /** + * check if we have been provided a `not` + * `not` is valid in many cases- here we're checking for + * `not: { schema: {'x-ts-type': SomeModel }} + */ + if (schema.not) { + processSchemaExtensions(spec, schema.not); + } - const tsType = schema[TS_TYPE_KEY]; - debug(' %s => %o', TS_TYPE_KEY, tsType); - if (tsType) { - schema = resolveSchema(tsType, schema); - if (schema.$ref) generateOpenAPISchema(spec, tsType); + /** + * check for schema.allOf, schema.oneOf, schema.anyOf arrays first. + * You cannot provide BOTH a defnintion AND one of these keywords. + */ + const hasOwn = (prop: string) => + Object.prototype.hasOwnProperty.call(schema, prop); + + if (SCHEMA_ARR_KEYS.some(k => hasOwn(k))) { + SCHEMA_ARR_KEYS.forEach((k: MixKey) => { + if (schema?.[k] && Array.isArray(schema[k])) { + schema[k].forEach((r: (SchemaObject | ReferenceObject)[]) => { + processSchemaExtensions(spec, r); + }); + } + }); + } else { + if (isReferenceObject(schema)) return; - // We don't want a Function type in the final spec. - delete schema[TS_TYPE_KEY]; - return; - } - if (schema.type === 'array') { - processSchemaExtensions(spec, schema.items); - } else if (schema.type === 'object') { - if (schema.properties) { - for (const p in schema.properties) { - processSchemaExtensions(spec, schema.properties[p]); + const tsType = schema[TS_TYPE_KEY]; + debug(' %s => %o', TS_TYPE_KEY, tsType); + if (tsType) { + schema = resolveSchema(tsType, schema); + if (schema.$ref) generateOpenAPISchema(spec, tsType); + + // We don't want a Function type in the final spec. + delete schema[TS_TYPE_KEY]; + return; + } + if (schema.type === 'array') { + processSchemaExtensions(spec, schema.items); + } else if (schema.type === 'object') { + if (schema.properties) { + for (const p in schema.properties) { + processSchemaExtensions(spec, schema.properties[p]); + } } } } diff --git a/packages/openapi-v3/src/decorators/deprecated.decorator.ts b/packages/openapi-v3/src/decorators/deprecated.decorator.ts new file mode 100644 index 000000000000..b790498ad9c9 --- /dev/null +++ b/packages/openapi-v3/src/decorators/deprecated.decorator.ts @@ -0,0 +1,51 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/openapi-v3 +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + ClassDecoratorFactory, + DecoratorFactory, + MethodDecoratorFactory, +} from '@loopback/core'; +import {OAI3Keys} from '../keys'; + +const debug = require('debug')( + 'loopback:openapi3:metadata:controller-spec:deprecated', +); + +export function deprecated(isDeprecated = true) { + return function deprecatedDecoratorForClassOrMethod( + // Class or a prototype + // eslint-disable-next-line @typescript-eslint/no-explicit-any + target: any, + method?: string, + // Use `any` to for `TypedPropertyDescriptor` + // See https://github.com/strongloop/loopback-next/pull/2704 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + methodDescriptor?: TypedPropertyDescriptor, + ) { + debug(target, method, methodDescriptor); + + if (method && methodDescriptor) { + // Method + return MethodDecoratorFactory.createDecorator( + OAI3Keys.DEPRECATED_METHOD_KEY, + isDeprecated, + {decoratorName: '@deprecated'}, + )(target, method, methodDescriptor); + } else if (typeof target === 'function' && !method && !methodDescriptor) { + // Class + return ClassDecoratorFactory.createDecorator( + OAI3Keys.DEPRECATED_CLASS_KEY, + isDeprecated, + {decoratorName: '@deprecated'}, + )(target); + } else { + throw new Error( + '@deprecated cannot be used on a property: ' + + DecoratorFactory.getTargetName(target, method, methodDescriptor), + ); + } + }; +} diff --git a/packages/openapi-v3/src/decorators/index.ts b/packages/openapi-v3/src/decorators/index.ts index 947da4a6f0e4..20003c5db028 100644 --- a/packages/openapi-v3/src/decorators/index.ts +++ b/packages/openapi-v3/src/decorators/index.ts @@ -4,6 +4,9 @@ // License text available at https://opensource.org/licenses/MIT export * from './api.decorator'; +export * from './deprecated.decorator'; export * from './operation.decorator'; export * from './parameter.decorator'; export * from './request-body.decorator'; +export * from './response.decorator'; +export * from './tags.decorator'; diff --git a/packages/openapi-v3/src/decorators/response.decorator.ts b/packages/openapi-v3/src/decorators/response.decorator.ts new file mode 100644 index 000000000000..4c25427352ae --- /dev/null +++ b/packages/openapi-v3/src/decorators/response.decorator.ts @@ -0,0 +1,50 @@ +// Copyright IBM Corp. 2018,2019. All Rights Reserved. +// Node module: @loopback/openapi-v3 +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT +import {MethodMultiDecoratorFactory} from '@loopback/core'; +import * as httpStatus from 'http-status'; +import {ExampleObject} from 'openapi3-ts'; +import {OAI3Keys} from '../keys'; +import {ResponseModelOrSpec} from '../types'; + +export interface ResponseOptions { + contentType?: string; + description?: string; + examples?: ExampleObject; +} +export function response( + responseCode: number, + responseModelOrSpec: ResponseModelOrSpec, + options?: ResponseOptions, +) { + const coercedCode = String(responseCode) as keyof httpStatus.HttpStatus; + const messageKey = `${coercedCode}_MESSAGE` as keyof httpStatus.HttpStatus; + + if (Array.isArray(responseModelOrSpec)) { + return MethodMultiDecoratorFactory.createDecorator( + OAI3Keys.RESPONSE_METHOD_KEY, + responseModelOrSpec.map(m => ({ + responseCode, + responseModelOrSpec: m, + contentType: options?.contentType ?? 'application/json', + description: options?.description ?? (httpStatus[messageKey] as string), + })), + {decoratorName: '@response', allowInheritance: false}, + ); + } else { + return MethodMultiDecoratorFactory.createDecorator( + OAI3Keys.RESPONSE_METHOD_KEY, + [ + { + responseCode, + responseModelOrSpec, + contentType: options?.contentType ?? 'application/json', + description: + options?.description ?? (httpStatus[messageKey] as string), + }, + ], + {decoratorName: '@response', allowInheritance: false}, + ); + } +} diff --git a/packages/openapi-v3/src/decorators/tags.decorator.ts b/packages/openapi-v3/src/decorators/tags.decorator.ts new file mode 100644 index 000000000000..0bf02397f011 --- /dev/null +++ b/packages/openapi-v3/src/decorators/tags.decorator.ts @@ -0,0 +1,50 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/openapi-v3 +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + ClassDecoratorFactory, + DecoratorFactory, + MethodDecoratorFactory, +} from '@loopback/core'; +import {OAI3Keys} from '../keys'; +import {TagsDecoratorMetadata} from '../types'; + +export interface TagDecoratorOptions { + append: boolean; +} + +export function tags(tagNames: string[], options?: TagDecoratorOptions) { + return function tagsDecoratorForClassOrMethod( + // Class or a prototype + // eslint-disable-next-line @typescript-eslint/no-explicit-any + target: any, + method?: string, + // Use `any` to for `TypedPropertyDescriptor` + // See https://github.com/strongloop/loopback-next/pull/2704 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + methodDescriptor?: TypedPropertyDescriptor, + ) { + if (method && methodDescriptor) { + // Method + return MethodDecoratorFactory.createDecorator( + OAI3Keys.TAGS_METHOD_KEY, + {tags: tagNames, append: options?.append ?? false}, + {decoratorName: '@tags'}, + )(target, method, methodDescriptor); + } else if (typeof target === 'function' && !method && !methodDescriptor) { + // Class + return ClassDecoratorFactory.createDecorator( + OAI3Keys.TAGS_CLASS_KEY, + {tags: tagNames, append: options?.append ?? false}, + {decoratorName: '@tags'}, + )(target); + } else { + throw new Error( + '@tags cannot be used on a property: ' + + DecoratorFactory.getTargetName(target, method, methodDescriptor), + ); + } + }; +} diff --git a/packages/openapi-v3/src/keys.ts b/packages/openapi-v3/src/keys.ts index a6d6cb7771ff..fe1e32d73951 100644 --- a/packages/openapi-v3/src/keys.ts +++ b/packages/openapi-v3/src/keys.ts @@ -5,7 +5,11 @@ import {MetadataAccessor} from '@loopback/core'; import {ControllerSpec, RestEndpoint} from './controller-spec'; -import {ParameterObject, RequestBodyObject} from './types'; +import { + ParameterObject, + RequestBodyObject, + ResponseDecoratorMetadata, +} from './types'; export namespace OAI3Keys { /** @@ -16,6 +20,46 @@ export namespace OAI3Keys { MethodDecorator >('openapi-v3:methods'); + /** + * Metadata key used to set or retrieve `@deprecated` metadata on a method. + */ + export const DEPRECATED_METHOD_KEY = MetadataAccessor.create< + boolean, + MethodDecorator + >('openapi-v3:methods:deprecated'); + + /** + * Metadata key used to set or retrieve `@deprecated` metadata on a class + */ + export const DEPRECATED_CLASS_KEY = MetadataAccessor.create< + boolean, + ClassDecorator + >('openapi-v3:class:deprecated'); + + /** + * Metadata key used to set or retrieve `@deprecated` metadata on a method. + */ + export const TAGS_METHOD_KEY = MetadataAccessor.create< + string[], + MethodDecorator + >('openapi-v3:methods:tags'); + + /** + * Metadata key used to set or retrieve `@deprecated` metadata on a class + */ + export const TAGS_CLASS_KEY = MetadataAccessor.create< + string[], + ClassDecorator + >('openapi-v3:class:tags'); + + /** + * Metadata key used to add to or retrieve an endpoint's responses + */ + export const RESPONSE_METHOD_KEY = MetadataAccessor.create< + ResponseDecoratorMetadata, + MethodDecorator + >('openapi-v3:methods:response'); + /** * Metadata key used to set or retrieve `param` decorator metadata */ diff --git a/packages/openapi-v3/src/types.ts b/packages/openapi-v3/src/types.ts index c420f4fbc7ec..0ce1b204c31c 100644 --- a/packages/openapi-v3/src/types.ts +++ b/packages/openapi-v3/src/types.ts @@ -3,7 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {OpenAPIObject} from 'openapi3-ts'; +import {OpenAPIObject, ReferenceObject, SchemaObject} from 'openapi3-ts'; /* * OpenApiSpec - A typescript representation of OpenApi 3.0.0 */ @@ -28,3 +28,23 @@ export function createEmptyApiSpec(): OpenApiSpec { servers: [{url: '/'}], }; } + +export interface TagsDecoratorMetadata { + tags: string[]; + append: boolean; +} + +export declare type ResponseModelOrSpec = + | Function + | Function[] + | SchemaObject + | ReferenceObject; + +export interface ResponseDecoratorMetadataItem { + responseCode: number; + contentType: string; + responseModelOrSpec: ResponseModelOrSpec; + description: string; +} + +export declare type ResponseDecoratorMetadata = ResponseDecoratorMetadataItem[]; diff --git a/packages/repository-json-schema/package-lock.json b/packages/repository-json-schema/package-lock.json index 23adf7e69fd0..8236adbf58aa 100644 --- a/packages/repository-json-schema/package-lock.json +++ b/packages/repository-json-schema/package-lock.json @@ -53,6 +53,12 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", diff --git a/packages/repository-tests/package-lock.json b/packages/repository-tests/package-lock.json index bee3d3c8c738..2223b0059818 100644 --- a/packages/repository-tests/package-lock.json +++ b/packages/repository-tests/package-lock.json @@ -29,6 +29,12 @@ "ms": "^2.1.1" } }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", diff --git a/packages/repository/package-lock.json b/packages/repository/package-lock.json index 2b7aef06bb9e..6ab1b52c037a 100644 --- a/packages/repository/package-lock.json +++ b/packages/repository/package-lock.json @@ -217,6 +217,12 @@ "cldrjs": "^0.5.0" } }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "ieee754": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", diff --git a/packages/rest-crud/package-lock.json b/packages/rest-crud/package-lock.json index 242f371507f3..2d560e76a2fc 100644 --- a/packages/rest-crud/package-lock.json +++ b/packages/rest-crud/package-lock.json @@ -9,6 +9,12 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.12.tgz", "integrity": "sha512-SSB4O9/0NVv5mbQ5/MabnAyFfcpVFRVIJj1TZkG21HHgwXQGjosiQB3SBWC9pMCMUTNpWL9gUe//9mFFPQAdKw==", "dev": true + }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true } } } diff --git a/packages/rest-explorer/package-lock.json b/packages/rest-explorer/package-lock.json index e18923af4175..3dacf84e05ae 100644 --- a/packages/rest-explorer/package-lock.json +++ b/packages/rest-explorer/package-lock.json @@ -273,6 +273,12 @@ "toidentifier": "1.0.0" } }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/packages/security/package-lock.json b/packages/security/package-lock.json index 0ee67e9a37c3..28a538c3a3af 100644 --- a/packages/security/package-lock.json +++ b/packages/security/package-lock.json @@ -24,6 +24,12 @@ "ms": "^2.1.1" } }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/packages/service-proxy/package-lock.json b/packages/service-proxy/package-lock.json index 7db8c2a05d55..fc402a7f51ed 100644 --- a/packages/service-proxy/package-lock.json +++ b/packages/service-proxy/package-lock.json @@ -171,6 +171,12 @@ "cldrjs": "^0.5.0" } }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "inflection": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", diff --git a/packages/testlab/package-lock.json b/packages/testlab/package-lock.json index 54d57c81d474..2e242842eb7b 100644 --- a/packages/testlab/package-lock.json +++ b/packages/testlab/package-lock.json @@ -696,6 +696,12 @@ "toidentifier": "1.0.0" } }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "http2-client": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.3.tgz", diff --git a/packages/tsdocs/package-lock.json b/packages/tsdocs/package-lock.json index 076cb3a66564..d8ae00974547 100644 --- a/packages/tsdocs/package-lock.json +++ b/packages/tsdocs/package-lock.json @@ -162,6 +162,12 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" }, + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + }, "jju": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", diff --git a/sandbox/example/package-lock.json b/sandbox/example/package-lock.json new file mode 100644 index 000000000000..8ecf65e4eae4 --- /dev/null +++ b/sandbox/example/package-lock.json @@ -0,0 +1,14 @@ +{ + "name": "@loopback/sandbox-example", + "version": "1.0.9", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "http-status": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", + "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", + "dev": true + } + } +} From 4e37df1df1efbf0517221dd61dfe7f363c1159dd Mon Sep 17 00:00:00 2001 From: matt Date: Fri, 10 Jan 2020 15:25:02 -0800 Subject: [PATCH 2/3] fix: remove @types/http-status --- examples/context/package-lock.json | 6 ------ examples/express-composition/package-lock.json | 6 ------ examples/greeter-extension/package-lock.json | 6 ------ examples/greeting-app/package-lock.json | 6 ------ examples/hello-world/package-lock.json | 6 ------ examples/log-extension/package-lock.json | 6 ------ examples/metrics-prometheus/package-lock.json | 6 ------ examples/rpc-server/package-lock.json | 6 ------ examples/soap-calculator/package-lock.json | 6 ------ examples/todo-list/package-lock.json | 6 ------ examples/todo/package-lock.json | 8 +------- packages/authentication/package-lock.json | 6 ------ packages/authorization/package-lock.json | 6 ------ packages/boot/package-lock.json | 6 ------ packages/context/package-lock.json | 6 ------ packages/core/package-lock.json | 6 ------ packages/eslint-config/package.json | 3 --- packages/http-caching-proxy/package-lock.json | 6 ------ packages/http-server/package-lock.json | 6 ------ packages/metadata/package-lock.json | 6 ------ packages/model-api-builder/package-lock.json | 6 ------ packages/openapi-spec-builder/package-lock.json | 6 ------ packages/openapi-v3/src/decorators/response.decorator.ts | 2 -- packages/repository-json-schema/package-lock.json | 6 ------ packages/repository-tests/package-lock.json | 6 ------ packages/repository/package-lock.json | 6 ------ packages/rest-crud/package-lock.json | 6 ------ packages/rest-explorer/package-lock.json | 6 ------ packages/security/package-lock.json | 6 ------ packages/service-proxy/package-lock.json | 6 ------ packages/testlab/package-lock.json | 6 ------ packages/tsdocs/package-lock.json | 6 ------ 32 files changed, 1 insertion(+), 186 deletions(-) diff --git a/examples/context/package-lock.json b/examples/context/package-lock.json index 254c41b2c10c..90fd7e7cadd6 100644 --- a/examples/context/package-lock.json +++ b/examples/context/package-lock.json @@ -557,12 +557,6 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/examples/express-composition/package-lock.json b/examples/express-composition/package-lock.json index 8fb59b393092..497f6ea80976 100644 --- a/examples/express-composition/package-lock.json +++ b/examples/express-composition/package-lock.json @@ -812,12 +812,6 @@ "toidentifier": "1.0.0" } }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/examples/greeter-extension/package-lock.json b/examples/greeter-extension/package-lock.json index 079dd0595410..6e5912da9df6 100644 --- a/examples/greeter-extension/package-lock.json +++ b/examples/greeter-extension/package-lock.json @@ -664,12 +664,6 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/examples/greeting-app/package-lock.json b/examples/greeting-app/package-lock.json index 0a3ae3dccd08..3af902fae653 100644 --- a/examples/greeting-app/package-lock.json +++ b/examples/greeting-app/package-lock.json @@ -664,12 +664,6 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/examples/hello-world/package-lock.json b/examples/hello-world/package-lock.json index 424c4b257351..a191b433a29d 100644 --- a/examples/hello-world/package-lock.json +++ b/examples/hello-world/package-lock.json @@ -557,12 +557,6 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/examples/log-extension/package-lock.json b/examples/log-extension/package-lock.json index d4d37ee7c99f..5bceedeb7d1e 100644 --- a/examples/log-extension/package-lock.json +++ b/examples/log-extension/package-lock.json @@ -664,12 +664,6 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/examples/metrics-prometheus/package-lock.json b/examples/metrics-prometheus/package-lock.json index 8c6596192f0b..1f856644061d 100644 --- a/examples/metrics-prometheus/package-lock.json +++ b/examples/metrics-prometheus/package-lock.json @@ -557,12 +557,6 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/examples/rpc-server/package-lock.json b/examples/rpc-server/package-lock.json index 9310b3cf43ef..8ef03b4959ec 100644 --- a/examples/rpc-server/package-lock.json +++ b/examples/rpc-server/package-lock.json @@ -812,12 +812,6 @@ "toidentifier": "1.0.0" } }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/examples/soap-calculator/package-lock.json b/examples/soap-calculator/package-lock.json index 934b0016f6be..60f5a276c981 100644 --- a/examples/soap-calculator/package-lock.json +++ b/examples/soap-calculator/package-lock.json @@ -962,12 +962,6 @@ "sshpk": "^1.7.0" } }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "httpntlm": { "version": "1.7.6", "resolved": "https://registry.npmjs.org/httpntlm/-/httpntlm-1.7.6.tgz", diff --git a/examples/todo-list/package-lock.json b/examples/todo-list/package-lock.json index 3b8e0c10e242..9a338c80c65f 100644 --- a/examples/todo-list/package-lock.json +++ b/examples/todo-list/package-lock.json @@ -758,12 +758,6 @@ "sshpk": "^1.7.0" } }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/examples/todo/package-lock.json b/examples/todo/package-lock.json index 0ad0de7f2cc8..f671244ddfca 100644 --- a/examples/todo/package-lock.json +++ b/examples/todo/package-lock.json @@ -1,6 +1,6 @@ { "name": "@loopback/example-todo", - "version": "1.9.2", + "version": "1.9.4", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -758,12 +758,6 @@ "sshpk": "^1.7.0" } }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/packages/authentication/package-lock.json b/packages/authentication/package-lock.json index 40752e13ec11..e311dc2e77b6 100644 --- a/packages/authentication/package-lock.json +++ b/packages/authentication/package-lock.json @@ -106,12 +106,6 @@ "safe-buffer": "^5.0.1" } }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "jsonwebtoken": { "version": "8.5.1", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", diff --git a/packages/authorization/package-lock.json b/packages/authorization/package-lock.json index fa78edc95df2..89034d0e0c5c 100644 --- a/packages/authorization/package-lock.json +++ b/packages/authorization/package-lock.json @@ -44,12 +44,6 @@ "jsep": "^0.3.0" } }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", diff --git a/packages/boot/package-lock.json b/packages/boot/package-lock.json index 78cec8acc3e3..e3c94be1eb79 100644 --- a/packages/boot/package-lock.json +++ b/packages/boot/package-lock.json @@ -87,12 +87,6 @@ "path-is-absolute": "^1.0.0" } }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", diff --git a/packages/context/package-lock.json b/packages/context/package-lock.json index 280ff830af9b..fbf91f637b27 100644 --- a/packages/context/package-lock.json +++ b/packages/context/package-lock.json @@ -45,12 +45,6 @@ "ms": "^2.1.1" } }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/packages/core/package-lock.json b/packages/core/package-lock.json index c1838135d6a5..03403782bc70 100644 --- a/packages/core/package-lock.json +++ b/packages/core/package-lock.json @@ -24,12 +24,6 @@ "ms": "^2.1.1" } }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 782c6627235a..66fb380480c4 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -24,8 +24,5 @@ }, "publishConfig": { "access": "public" - }, - "devDependencies": { - "@types/http-status": "^1.1.2" } } diff --git a/packages/http-caching-proxy/package-lock.json b/packages/http-caching-proxy/package-lock.json index bf2e226075f5..7465263c2e56 100644 --- a/packages/http-caching-proxy/package-lock.json +++ b/packages/http-caching-proxy/package-lock.json @@ -411,12 +411,6 @@ "sshpk": "^1.7.0" } }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "iferr": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", diff --git a/packages/http-server/package-lock.json b/packages/http-server/package-lock.json index ce7878efd7c3..aa00ebb6450c 100644 --- a/packages/http-server/package-lock.json +++ b/packages/http-server/package-lock.json @@ -19,12 +19,6 @@ "@types/node": "*" } }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "p-event": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.1.0.tgz", diff --git a/packages/metadata/package-lock.json b/packages/metadata/package-lock.json index 7f17493451c1..f5fd50171df0 100644 --- a/packages/metadata/package-lock.json +++ b/packages/metadata/package-lock.json @@ -30,12 +30,6 @@ "ms": "^2.1.1" } }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", diff --git a/packages/model-api-builder/package-lock.json b/packages/model-api-builder/package-lock.json index 1e47c57189aa..1457df48f8aa 100644 --- a/packages/model-api-builder/package-lock.json +++ b/packages/model-api-builder/package-lock.json @@ -9,12 +9,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.12.tgz", "integrity": "sha512-SSB4O9/0NVv5mbQ5/MabnAyFfcpVFRVIJj1TZkG21HHgwXQGjosiQB3SBWC9pMCMUTNpWL9gUe//9mFFPQAdKw==", "dev": true - }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true } } } diff --git a/packages/openapi-spec-builder/package-lock.json b/packages/openapi-spec-builder/package-lock.json index 766042be444b..7ce8ad8f0bc5 100644 --- a/packages/openapi-spec-builder/package-lock.json +++ b/packages/openapi-spec-builder/package-lock.json @@ -10,12 +10,6 @@ "integrity": "sha512-SSB4O9/0NVv5mbQ5/MabnAyFfcpVFRVIJj1TZkG21HHgwXQGjosiQB3SBWC9pMCMUTNpWL9gUe//9mFFPQAdKw==", "dev": true }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "openapi3-ts": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-1.3.0.tgz", diff --git a/packages/openapi-v3/src/decorators/response.decorator.ts b/packages/openapi-v3/src/decorators/response.decorator.ts index 4c25427352ae..c5d906253e23 100644 --- a/packages/openapi-v3/src/decorators/response.decorator.ts +++ b/packages/openapi-v3/src/decorators/response.decorator.ts @@ -4,14 +4,12 @@ // License text available at https://opensource.org/licenses/MIT import {MethodMultiDecoratorFactory} from '@loopback/core'; import * as httpStatus from 'http-status'; -import {ExampleObject} from 'openapi3-ts'; import {OAI3Keys} from '../keys'; import {ResponseModelOrSpec} from '../types'; export interface ResponseOptions { contentType?: string; description?: string; - examples?: ExampleObject; } export function response( responseCode: number, diff --git a/packages/repository-json-schema/package-lock.json b/packages/repository-json-schema/package-lock.json index 8236adbf58aa..23adf7e69fd0 100644 --- a/packages/repository-json-schema/package-lock.json +++ b/packages/repository-json-schema/package-lock.json @@ -53,12 +53,6 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", diff --git a/packages/repository-tests/package-lock.json b/packages/repository-tests/package-lock.json index 2223b0059818..bee3d3c8c738 100644 --- a/packages/repository-tests/package-lock.json +++ b/packages/repository-tests/package-lock.json @@ -29,12 +29,6 @@ "ms": "^2.1.1" } }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", diff --git a/packages/repository/package-lock.json b/packages/repository/package-lock.json index 6ab1b52c037a..2b7aef06bb9e 100644 --- a/packages/repository/package-lock.json +++ b/packages/repository/package-lock.json @@ -217,12 +217,6 @@ "cldrjs": "^0.5.0" } }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "ieee754": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", diff --git a/packages/rest-crud/package-lock.json b/packages/rest-crud/package-lock.json index 2d560e76a2fc..242f371507f3 100644 --- a/packages/rest-crud/package-lock.json +++ b/packages/rest-crud/package-lock.json @@ -9,12 +9,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.12.tgz", "integrity": "sha512-SSB4O9/0NVv5mbQ5/MabnAyFfcpVFRVIJj1TZkG21HHgwXQGjosiQB3SBWC9pMCMUTNpWL9gUe//9mFFPQAdKw==", "dev": true - }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true } } } diff --git a/packages/rest-explorer/package-lock.json b/packages/rest-explorer/package-lock.json index 3dacf84e05ae..e18923af4175 100644 --- a/packages/rest-explorer/package-lock.json +++ b/packages/rest-explorer/package-lock.json @@ -273,12 +273,6 @@ "toidentifier": "1.0.0" } }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/packages/security/package-lock.json b/packages/security/package-lock.json index 28a538c3a3af..0ee67e9a37c3 100644 --- a/packages/security/package-lock.json +++ b/packages/security/package-lock.json @@ -24,12 +24,6 @@ "ms": "^2.1.1" } }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/packages/service-proxy/package-lock.json b/packages/service-proxy/package-lock.json index fc402a7f51ed..7db8c2a05d55 100644 --- a/packages/service-proxy/package-lock.json +++ b/packages/service-proxy/package-lock.json @@ -171,12 +171,6 @@ "cldrjs": "^0.5.0" } }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "inflection": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", diff --git a/packages/testlab/package-lock.json b/packages/testlab/package-lock.json index 2e242842eb7b..54d57c81d474 100644 --- a/packages/testlab/package-lock.json +++ b/packages/testlab/package-lock.json @@ -696,12 +696,6 @@ "toidentifier": "1.0.0" } }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "http2-client": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.3.tgz", diff --git a/packages/tsdocs/package-lock.json b/packages/tsdocs/package-lock.json index d8ae00974547..076cb3a66564 100644 --- a/packages/tsdocs/package-lock.json +++ b/packages/tsdocs/package-lock.json @@ -162,12 +162,6 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "jju": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", From 7bbfd5b142d31fcbe5dbfc5916265967218b4d12 Mon Sep 17 00:00:00 2001 From: matt Date: Mon, 13 Jan 2020 10:45:05 -0800 Subject: [PATCH 3/3] test: adds additional unit tests for coverage --- .../repository-cloudant/package-lock.json | 6 -- .../repository-mongodb/package-lock.json | 6 -- acceptance/repository-mysql/package-lock.json | 6 -- .../repository-postgresql/package-lock.json | 6 -- benchmark/package-lock.json | 6 -- .../authentication-passport/package-lock.json | 6 -- extensions/health/package-lock.json | 6 -- extensions/metrics/package-lock.json | 6 -- package-lock.json | 64 +++++++++++++++++++ package.json | 3 +- .../__tests__/unit/decorator-factory.unit.ts | 58 ++++++++++++----- packages/metadata/src/decorator-factory.ts | 15 ----- .../decorators/deprecated.decorator.unit.ts | 16 +++++ .../unit/decorators/tags.decorator.unit.ts | 16 +++++ 14 files changed, 140 insertions(+), 80 deletions(-) diff --git a/acceptance/repository-cloudant/package-lock.json b/acceptance/repository-cloudant/package-lock.json index 0fef1d4a2fb8..6882141974b4 100644 --- a/acceptance/repository-cloudant/package-lock.json +++ b/acceptance/repository-cloudant/package-lock.json @@ -529,12 +529,6 @@ "sshpk": "^1.7.0" } }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", diff --git a/acceptance/repository-mongodb/package-lock.json b/acceptance/repository-mongodb/package-lock.json index 867d4b4ad47d..4a4be63683e0 100644 --- a/acceptance/repository-mongodb/package-lock.json +++ b/acceptance/repository-mongodb/package-lock.json @@ -193,12 +193,6 @@ "cldrjs": "^0.5.0" } }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", diff --git a/acceptance/repository-mysql/package-lock.json b/acceptance/repository-mysql/package-lock.json index 62a2458b5a60..b47784c44603 100644 --- a/acceptance/repository-mysql/package-lock.json +++ b/acceptance/repository-mysql/package-lock.json @@ -196,12 +196,6 @@ "cldrjs": "^0.5.0" } }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", diff --git a/acceptance/repository-postgresql/package-lock.json b/acceptance/repository-postgresql/package-lock.json index 44e36fa2bac8..66d1f7eaa894 100644 --- a/acceptance/repository-postgresql/package-lock.json +++ b/acceptance/repository-postgresql/package-lock.json @@ -193,12 +193,6 @@ "cldrjs": "^0.5.0" } }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", diff --git a/benchmark/package-lock.json b/benchmark/package-lock.json index f1c6321b9c6b..b603f76fd58d 100644 --- a/benchmark/package-lock.json +++ b/benchmark/package-lock.json @@ -689,12 +689,6 @@ "sshpk": "^1.7.0" } }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "hyperid": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hyperid/-/hyperid-2.0.2.tgz", diff --git a/extensions/authentication-passport/package-lock.json b/extensions/authentication-passport/package-lock.json index abe5f8db21fc..e64a8d875faf 100644 --- a/extensions/authentication-passport/package-lock.json +++ b/extensions/authentication-passport/package-lock.json @@ -91,12 +91,6 @@ "@types/mime": "*" } }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "passport": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz", diff --git a/extensions/health/package-lock.json b/extensions/health/package-lock.json index fb2c3de24b0c..60ec1089f854 100644 --- a/extensions/health/package-lock.json +++ b/extensions/health/package-lock.json @@ -15,12 +15,6 @@ "integrity": "sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg==", "dev": true }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "p-event": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.1.0.tgz", diff --git a/extensions/metrics/package-lock.json b/extensions/metrics/package-lock.json index 42d715b02ded..b5c599204895 100644 --- a/extensions/metrics/package-lock.json +++ b/extensions/metrics/package-lock.json @@ -267,12 +267,6 @@ "toidentifier": "1.0.0" } }, - "http-status": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.4.2.tgz", - "integrity": "sha512-mBnIohUwRw9NyXMEMMv8/GANnzEYUj0Y8d3uL01zDWFkxUjYyZ6rgCaAI2zZ1Wb34Oqtbx/nFZolPRDc8Xlm5A==", - "dev": true - }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/package-lock.json b/package-lock.json index 42721cdc832c..a584b04e77cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3713,6 +3713,12 @@ "flat-cache": "^2.0.1" } }, + "file-type": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-11.1.0.tgz", + "integrity": "sha512-rM0UO7Qm9K7TWTtA6AShI/t7H5BPjDeGVDaNyg9BjHAj3PysKy7+8C8D137R88jnR3rFJZQB/tFgydl5sN5m7g==", + "dev": true + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -5104,6 +5110,12 @@ "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -6038,6 +6050,58 @@ "mimic-fn": "^1.0.0" } }, + "open": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", + "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", + "dev": true, + "requires": { + "is-wsl": "^1.1.0" + } + }, + "open-cli": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/open-cli/-/open-cli-5.0.0.tgz", + "integrity": "sha512-Y2KQDS6NqNtk+PSXzSgwH41vTDMRndwFgVWsfgMhXv7lNe1cImLCe19Vo8oKwMsL7WeNsGmmbX7Ml74Ydj61Cg==", + "dev": true, + "requires": { + "file-type": "^11.0.0", + "get-stdin": "^7.0.0", + "meow": "^5.0.0", + "open": "^6.3.0", + "temp-write": "^4.0.0" + }, + "dependencies": { + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + }, + "make-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", + "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "temp-write": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/temp-write/-/temp-write-4.0.0.tgz", + "integrity": "sha512-HIeWmj77uOOHb0QX7siN3OtwV3CTntquin6TNVg6SHOqCP3hYKmox90eeFOGaY1MqJ9WYDDjkyZrW6qS5AWpbw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "is-stream": "^2.0.0", + "make-dir": "^3.0.0", + "temp-dir": "^1.0.0", + "uuid": "^3.3.2" + } + } + } + }, "opencollective-postinstall": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz", diff --git a/package.json b/package.json index 28e983b7b598..03cb3b6837d9 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "fs-extra": "^8.1.0", "husky": "^3.1.0", "lerna": "^3.20.2", + "open-cli": "^5.0.0", "typescript": "~3.7.4" }, "scripts": { @@ -42,7 +43,7 @@ "tsdocs": "lerna run --scope @loopback/tsdocs build:tsdocs", "coverage:ci": "node packages/build/bin/run-nyc report --reporter=text-lcov | coveralls", "precoverage": "npm test", - "coverage": "open coverage/index.html", + "coverage": "open-cli coverage/index.html", "lint": "npm run prettier:check && npm run eslint && node bin/check-package-locks", "lint:fix": "npm run eslint:fix && npm run prettier:fix", "eslint": "node packages/build/bin/run-eslint --report-unused-disable-directives --cache .", diff --git a/packages/metadata/src/__tests__/unit/decorator-factory.unit.ts b/packages/metadata/src/__tests__/unit/decorator-factory.unit.ts index 1d3cc52b8327..a912d9c7f378 100644 --- a/packages/metadata/src/__tests__/unit/decorator-factory.unit.ts +++ b/packages/metadata/src/__tests__/unit/decorator-factory.unit.ts @@ -525,7 +525,7 @@ describe('MethodDecoratorFactory for static methods', () => { }); describe('MethodMultiDecoratorFactory', () => { - function methodMultiDecorator(spec: object | object[]): MethodDecorator { + function methodMultiArrayDecorator(spec: object | object[]): MethodDecorator { if (Array.isArray(spec)) { return MethodMultiDecoratorFactory.createDecorator('test', spec); } else { @@ -533,22 +533,30 @@ describe('MethodMultiDecoratorFactory', () => { } } + function methodMultiDecorator(spec: object): MethodDecorator { + return MethodMultiDecoratorFactory.createDecorator('test', spec); + } + class BaseController { - @methodMultiDecorator({x: 1}) + @methodMultiArrayDecorator({x: 1}) public myMethod() {} - @methodMultiDecorator({foo: 1}) - @methodMultiDecorator({foo: 2}) - @methodMultiDecorator([{foo: 3}, {foo: 4}]) + @methodMultiArrayDecorator({foo: 1}) + @methodMultiArrayDecorator({foo: 2}) + @methodMultiArrayDecorator([{foo: 3}, {foo: 4}]) public multiMethod() {} + + @methodMultiDecorator({a: 'a'}) + @methodMultiDecorator({b: 'b'}) + public checkDecorator() {} } class SubController extends BaseController { - @methodMultiDecorator({y: 2}) + @methodMultiArrayDecorator({y: 2}) public myMethod() {} - @methodMultiDecorator({bar: 1}) - @methodMultiDecorator([{bar: 2}, {bar: 3}]) + @methodMultiArrayDecorator({bar: 1}) + @methodMultiArrayDecorator([{bar: 2}, {bar: 3}]) public multiMethod() {} } @@ -570,6 +578,11 @@ describe('MethodMultiDecoratorFactory', () => { }); describe('multi-decorator methods', () => { + it('applies to non-array decorator creation', () => { + const meta = Reflector.getOwnMetadata('test', BaseController.prototype); + expect(meta.checkDecorator).to.containDeep([{a: 'a'}, {b: 'b'}]); + }); + it('applies metadata to a method', () => { const meta = Reflector.getOwnMetadata('test', BaseController.prototype); expect(meta.multiMethod).to.containDeep([ @@ -605,7 +618,7 @@ describe('MethodMultiDecoratorFactory', () => { }); }); describe('MethodMultiDecoratorFactory for static methods', () => { - function methodMultiDecorator(spec: object | object[]): MethodDecorator { + function methodMultiArrayDecorator(spec: object | object[]): MethodDecorator { if (Array.isArray(spec)) { return MethodMultiDecoratorFactory.createDecorator('test', spec); } else { @@ -613,22 +626,30 @@ describe('MethodMultiDecoratorFactory for static methods', () => { } } + function methodMultiDecorator(spec: object): MethodDecorator { + return MethodMultiDecoratorFactory.createDecorator('test', spec); + } + class BaseController { - @methodMultiDecorator({x: 1}) + @methodMultiArrayDecorator({x: 1}) static myMethod() {} - @methodMultiDecorator({foo: 1}) - @methodMultiDecorator({foo: 2}) - @methodMultiDecorator([{foo: 3}, {foo: 4}]) + @methodMultiArrayDecorator({foo: 1}) + @methodMultiArrayDecorator({foo: 2}) + @methodMultiArrayDecorator([{foo: 3}, {foo: 4}]) static multiMethod() {} + + @methodMultiDecorator({a: 'a'}) + @methodMultiDecorator({b: 'b'}) + static checkDecorator() {} } class SubController extends BaseController { - @methodMultiDecorator({y: 2}) + @methodMultiArrayDecorator({y: 2}) static myMethod() {} - @methodMultiDecorator({bar: 1}) - @methodMultiDecorator([{bar: 2}, {bar: 3}]) + @methodMultiArrayDecorator({bar: 1}) + @methodMultiArrayDecorator([{bar: 2}, {bar: 3}]) static multiMethod() {} } @@ -660,6 +681,11 @@ describe('MethodMultiDecoratorFactory for static methods', () => { ]); }); + it('applies to non-array decorator creation', () => { + const meta = Reflector.getOwnMetadata('test', BaseController); + expect(meta.checkDecorator).to.containDeep([{a: 'a'}, {b: 'b'}]); + }); + it('merges with base method metadata', () => { const meta = Reflector.getOwnMetadata('test', SubController); expect(meta.multiMethod).to.containDeep([ diff --git a/packages/metadata/src/decorator-factory.ts b/packages/metadata/src/decorator-factory.ts index 47bec82986a2..10888c621812 100644 --- a/packages/metadata/src/decorator-factory.ts +++ b/packages/metadata/src/decorator-factory.ts @@ -817,21 +817,6 @@ export class MethodParameterDecoratorFactory extends DecoratorFactory< export class MethodMultiDecoratorFactory extends MethodDecoratorFactory< T[] > { - private getOrInitMetadata( - meta: MetadataMap, - target: Object, - methodName?: string, - ) { - const method = methodName ? methodName : ''; - let methodMeta = meta[method]; - if (methodMeta == null) { - // Initialize the method metadata - methodMeta = []; - meta[method] = methodMeta; - } - return methodMeta; - } - protected mergeWithInherited( inheritedMetadata: MetadataMap, target: Object, diff --git a/packages/openapi-v3/src/__tests__/unit/decorators/deprecated.decorator.unit.ts b/packages/openapi-v3/src/__tests__/unit/decorators/deprecated.decorator.unit.ts index 65984a6fbe2c..23dbcfae0415 100644 --- a/packages/openapi-v3/src/__tests__/unit/decorators/deprecated.decorator.unit.ts +++ b/packages/openapi-v3/src/__tests__/unit/decorators/deprecated.decorator.unit.ts @@ -92,4 +92,20 @@ describe('deprecation decorator', () => { expect(actualSpec.paths['/greet'].get.deprecated).to.be.undefined(); expect(actualSpec.paths['/echo'].get.deprecated).to.be.undefined(); }); + + it('Does not allow a member variable to be decorated', () => { + const shouldThrow = () => { + class MyController { + @deprecated() + public foo: string; + + @get('/greet') + greet() {} + } + + return getControllerSpec(MyController); + }; + + expect(shouldThrow).to.throw(/^\@deprecated cannot be used on a property:/); + }); }); diff --git a/packages/openapi-v3/src/__tests__/unit/decorators/tags.decorator.unit.ts b/packages/openapi-v3/src/__tests__/unit/decorators/tags.decorator.unit.ts index 85dc79710b97..8f1708c90681 100644 --- a/packages/openapi-v3/src/__tests__/unit/decorators/tags.decorator.unit.ts +++ b/packages/openapi-v3/src/__tests__/unit/decorators/tags.decorator.unit.ts @@ -83,4 +83,20 @@ describe('@tags decorator', () => { expect(actualSpec.paths['/greet'].get.tags).to.eql(['Foo']); expect(actualSpec.paths['/echo'].get.tags).to.eql(['Foo', 'Bar']); }); + + it('Does not allow a member variable to be decorated', () => { + const shouldThrow = () => { + class MyController { + @tags(['foo', 'bar']) + public foo: string; + + @get('/greet') + greet() {} + } + + return getControllerSpec(MyController); + }; + + expect(shouldThrow).to.throw(/^\@tags cannot be used on a property:/); + }); });