diff --git a/contentlake-shared.code-workspace b/contentlake-shared.code-workspace index f810230..1da94de 100644 --- a/contentlake-shared.code-workspace +++ b/contentlake-shared.code-workspace @@ -18,6 +18,9 @@ { "path": "packages/request-handler" }, + { + "path": "packages/rest-error" + }, { "path": "packages/search-index" }, diff --git a/package-lock.json b/package-lock.json index 145578a..dc0060f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -96,6 +96,14 @@ "resolved": "packages/request-handler", "link": true }, + "node_modules/@adobe/contentlake-shared-rest-error": { + "resolved": "packages/rest-error", + "link": true + }, + "node_modules/@adobe/contentlake-shared-router": { + "resolved": "packages/router", + "link": true + }, "node_modules/@adobe/contentlake-shared-search-index": { "resolved": "packages/search-index", "link": true @@ -163,9 +171,9 @@ } }, "node_modules/@adobe/helix-universal": { - "version": "4.2.0", - "license": "Apache-2.0", - "peer": true, + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@adobe/helix-universal/-/helix-universal-4.3.0.tgz", + "integrity": "sha512-f13i/cL82R/hGZ7Ibs7w2RCcITBJt55OQF5/lg0mQDhleALXYsrDlDeLCbnLb2naeYj9dyd1Xn3aoetG0ZjfZQ==", "dependencies": { "@adobe/fetch": "4.0.13", "aws4": "1.12.0" @@ -4281,6 +4289,7 @@ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -4304,6 +4313,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4319,13 +4329,15 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@eslint/js": { "version": "8.47.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.47.0.tgz", "integrity": "sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==", "dev": true, + "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -7974,6 +7986,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -7986,6 +7999,7 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -8322,8 +8336,7 @@ }, "node_modules/aws4": { "version": "1.12.0", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/balanced-match": { "version": "1.0.2", @@ -9348,6 +9361,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.47.0.tgz", "integrity": "sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -9560,6 +9574,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -9592,6 +9607,7 @@ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -10165,6 +10181,7 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -12136,7 +12153,6 @@ }, "node_modules/node-fetch": { "version": "2.6.12", - "dev": true, "license": "MIT", "dependencies": { "whatwg-url": "^5.0.0" @@ -13142,7 +13158,8 @@ }, "node_modules/routington": { "version": "1.0.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/routington/-/routington-1.0.3.tgz", + "integrity": "sha512-7hgqbBCkD8eTW4nckNS/8pxK3THKKN8pID9AqQYUbB5IkFihg4benHmhmYBrdKbK394wmVGphkUcFlnhlP9COA==", "dependencies": { "flatten": "~0.0.1", "http-assert": "1" @@ -14147,7 +14164,6 @@ }, "node_modules/tr46": { "version": "0.0.3", - "dev": true, "license": "MIT" }, "node_modules/traverse": { @@ -14214,6 +14230,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -14363,12 +14380,10 @@ }, "node_modules/webidl-conversions": { "version": "3.0.1", - "dev": true, "license": "BSD-2-Clause" }, "node_modules/whatwg-url": { "version": "5.0.0", - "dev": true, "license": "MIT", "dependencies": { "tr46": "~0.0.3", @@ -14661,19 +14676,6 @@ "dotenv": "^16.3.1" } }, - "packages/custom-secrets": { - "name": "@adobe/contentlake-shared-custom-secrets", - "version": "1.0.0", - "extraneous": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/client-secrets-manager": "^3.290.0" - }, - "devDependencies": { - "aws-sdk-client-mock": "^3.0.0", - "dotenv": "^16.3.1" - } - }, "packages/extractor-helpers": { "name": "@adobe/contentlake-shared-extractor-helpers", "version": "1.4.0", @@ -14759,6 +14761,25 @@ "url": "https://opencollective.com/node-fetch" } }, + "packages/rest-error": { + "name": "@adobe/contentlake-shared-rest-error", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "@adobe/helix-universal": "^4.3.0", + "node-fetch": "^2.6.11" + } + }, + "packages/router": { + "name": "@adobe/contentlake-shared-router", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "@adobe/contentlake-shared-rest-error": "^1.0.0", + "@adobe/helix-universal": "^4.3.0", + "routington": "^1.0.3" + } + }, "packages/search-index": { "name": "@adobe/contentlake-shared-search-index", "version": "1.0.0", @@ -14775,6 +14796,7 @@ } }, "packages/secrets-manager": { + "name": "@adobe/contentlake-shared-secrets-manager", "version": "1.0.0", "license": "Apache-2.0", "dependencies": { diff --git a/packages/rest-error/.prettierrc b/packages/rest-error/.prettierrc new file mode 100644 index 0000000..56d4876 --- /dev/null +++ b/packages/rest-error/.prettierrc @@ -0,0 +1,6 @@ +{ + "tabWidth": 2, + "useTabs": false, + "singleQuote": true, + "trailingComma": "all" +} \ No newline at end of file diff --git a/packages/rest-error/LICENSE.txt b/packages/rest-error/LICENSE.txt new file mode 100644 index 0000000..883ab09 --- /dev/null +++ b/packages/rest-error/LICENSE.txt @@ -0,0 +1,264 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +APACHE JACKRABBIT SUBCOMPONENTS + +Apache Jackrabbit includes parts with separate copyright notices and license +terms. Your use of these subcomponents is subject to the terms and conditions +of the following licenses: + + XPath 2.0/XQuery 1.0 Parser: + http://www.w3.org/2002/11/xquery-xpath-applets/xgrammar.zip + + Copyright (C) 2002 World Wide Web Consortium, (Massachusetts Institute of + Technology, European Research Consortium for Informatics and Mathematics, + Keio University). All Rights Reserved. + + This work is distributed under the W3C(R) Software License in the hope + that it will be useful, but WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + W3C(R) SOFTWARE NOTICE AND LICENSE + http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231 + + This work (and included software, documentation such as READMEs, or + other related items) is being provided by the copyright holders under + the following license. By obtaining, using and/or copying this work, + you (the licensee) agree that you have read, understood, and will comply + with the following terms and conditions. + + Permission to copy, modify, and distribute this software and its + documentation, with or without modification, for any purpose and + without fee or royalty is hereby granted, provided that you include + the following on ALL copies of the software and documentation or + portions thereof, including modifications: + + 1. The full text of this NOTICE in a location viewable to users + of the redistributed or derivative work. + + 2. Any pre-existing intellectual property disclaimers, notices, + or terms and conditions. If none exist, the W3C Software Short + Notice should be included (hypertext is preferred, text is + permitted) within the body of any redistributed or derivative code. + + 3. Notice of any changes or modifications to the files, including + the date changes were made. (We recommend you provide URIs to the + location from which the code is derived.) + + THIS SOFTWARE AND DOCUMENTATION IS PROVIDED "AS IS," AND COPYRIGHT + HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, + INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS + FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE OR + DOCUMENTATION WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, + TRADEMARKS OR OTHER RIGHTS. + + COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL + OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR + DOCUMENTATION. + + The name and trademarks of copyright holders may NOT be used in + advertising or publicity pertaining to the software without specific, + written prior permission. Title to copyright in this software and + any associated documentation will at all times remain with + copyright holders. diff --git a/packages/rest-error/README.md b/packages/rest-error/README.md new file mode 100644 index 0000000..76ce751 --- /dev/null +++ b/packages/rest-error/README.md @@ -0,0 +1,47 @@ +# Content Lake Shared - REST Error + +> Error type for returning [rfc9457](https://datatracker.ietf.org/doc/html/rfc9457) problem responses + +This is one of the [Content Lake Shared](https://github.com/adobe/contentlake-shared) libraries. + +## Status + +[![GitHub license](https://img.shields.io/github/license/adobe/contentlake-shared.svg)](https://github.com/adobe/contentlake-shared/blob/main/LICENSE.txt) + +## Usage + +Install using: + +``` +npm install @adobe/contentlake-shared-rest-error +``` + +Use with: + +``` +import { RestError } from '@adobe/contentlake-shared-rest-error'; + +throw new RestError(400, 'Missing some field'); +``` + +### Additional Properties + +To add additional requests to the error body provide a third parameter as an Object: + +``` +throw new RestError(400, 'Missing some field', { fields: ['field1', 'field2']}); +``` + +### Converting to a Problem Response + +The static method `toProblemResponse` can convert any object (including RestError objects) into a [rfc9457](https://datatracker.ietf.org/doc/html/rfc9457) problem responses: + +``` +RestError.toProblemResponse(err); +``` + +In addition, you can provide a second context parameter from which the method will extract an instance from the invocation: + +``` +RestError.toProblemResponse(err, context); +``` diff --git a/packages/rest-error/package.json b/packages/rest-error/package.json new file mode 100644 index 0000000..6f91751 --- /dev/null +++ b/packages/rest-error/package.json @@ -0,0 +1,32 @@ +{ + "name": "@adobe/contentlake-shared-rest-error", + "version": "1.0.0", + "description": "Error type for returning rfc9457 problem responses", + "main": "src/index.js", + "type": "module", + "scripts": { + "test": "c8 mocha", + "lint": "eslint ." + }, + "mocha": { + "reporter": "mocha-multi-reporters", + "reporter-options": "configFile=../../.mocha-multi.json" + }, + "repository": { + "type": "git", + "url": "https://github.com/adobe/contentlake-shared" + }, + "author": "", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/adobe/contentlake-shared/issues" + }, + "homepage": "https://github.com/adobe/contentlake-shared/tree/main/packages/rest-error#readme", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@adobe/helix-universal": "^4.3.0", + "node-fetch": "^2.6.11" + } +} diff --git a/packages/rest-error/src/index.js b/packages/rest-error/src/index.js new file mode 100644 index 0000000..f0a7d0e --- /dev/null +++ b/packages/rest-error/src/index.js @@ -0,0 +1,131 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { Response } from 'node-fetch'; + +const STATUS_TITLES = { + 400: 'Bad Request', + 401: 'Unauthorized', + 402: 'Payment Required', + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 406: 'Not Acceptable', + 407: 'Proxy Authentication Required', + 408: 'Request Timeout', + 409: 'Conflict', + 410: 'Gone', + 411: 'Length Required', + 412: 'Precondition Failed', + 413: 'Request Too Large', + 414: 'Request-URI Too Long', + 415: 'Unsupported Media Type', + 416: 'Range Not Satisfiable', + 417: 'Expectation Failed', + 500: 'Internal Server Error', + 501: 'Not Implemented', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + 504: 'Gateway Timeout', + 505: 'HTTP Version Not Supported', + 511: 'Network Authentication Required', +}; + +/** + * An error class for rfc9457-compliant errors + */ +export class RestError extends Error { + /** + * @type {number} The HTTP status code + */ + status; + + /** + * @type {Object | undefined} Additional properties to include in the problem + * response + */ + additional; + + /** + * @type {string | undefined} A human-readable explanation + */ + detail; + + /** + * @type {string | undefined} URI reference that identifies the specific + occurrence of the problem. + */ + instance; + + /** + * @type {string | undefined} A short, human-readable summary of the problem + type. It SHOULD NOT change from occurrence to occurrence of the + problem + */ + title; + + /** + * Construct a new RestError instance + * @param {number} status the HTTP status code + * @param {string} [detail] A human-readable explanation + * @param {Object} [additional] Additional properties to include in the problem response + */ + constructor(status, detail, additional) { + super(detail); + this.status = status; + this.detail = detail; + this.additional = additional; + } + + /** + * + * @param {string|undefined} initial + * @param {import('@adobe/helix-universal').UniversalContext} context + */ + static #getInstanceUri(initial, context) { + if (initial) { + return initial; + } + const instance = context?.invocation?.requestId || context?.invocation?.id; + if (instance) { + return `/invocation/${instance}`; + } + return undefined; + } + + /** + * Converts the specified error object to a problem response + * @param {*} err the error to which to convert to a application/problem+json response + * @param {import('@adobe/helix-universal').UniversalContext} [context] the context + * @returns a response for the problem + */ + static toProblemResponse(err, context) { + let { status } = err; + if (!status) { + status = 500; + } + return new Response( + JSON.stringify({ + title: err.title || STATUS_TITLES[status] || 'Unknown Problem', + status, + detail: err.detail || err.message, + instance: RestError.#getInstanceUri(err.instance, context), + ...(err.additional || {}), + }), + { + headers: { + 'Content-Type': 'application/problem+json', + }, + status, + }, + ); + } +} diff --git a/packages/rest-error/test/index.test.js b/packages/rest-error/test/index.test.js new file mode 100644 index 0000000..df5e4e4 --- /dev/null +++ b/packages/rest-error/test/index.test.js @@ -0,0 +1,119 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +/* eslint-env mocha */ +import assert from 'assert'; +import { RestError } from '../src/index.js'; + +describe('RestError Unit Tests', () => { + it('can construct error', async () => { + const err = new RestError(418, 'A problem!'); + assert.strictEqual(err.status, 418); + assert.strictEqual(err.detail, 'A problem!'); + assert.strictEqual(err.message, 'A problem!'); + }); + + it('can get error as problem response', async () => { + const err = new RestError(400, 'A problem!'); + err.instance = 'test_instance'; + err.unmappedfield = 'ruh ruh'; + + const resp = RestError.toProblemResponse(err); + assert.strictEqual(resp.status, 400); + const body = await resp.json(); + assert.deepStrictEqual(body, { + status: 400, + title: 'Bad Request', + instance: 'test_instance', + detail: 'A problem!', + }); + }); + + it('will get title from status', async () => { + const err = new RestError(502, 'A problem!'); + const resp = RestError.toProblemResponse(err); + const body = await resp.json(); + assert.deepStrictEqual(body, { + title: 'Bad Gateway', + status: 502, + detail: 'A problem!', + }); + }); + + it('handles unknown status', async () => { + const err = new RestError(418, 'A problem!'); + const resp = RestError.toProblemResponse(err); + assert.strictEqual(resp.status, 418); + const body = await resp.json(); + assert.deepStrictEqual(body, { + title: 'Unknown Problem', + status: 418, + detail: 'A problem!', + }); + }); + + it('handles no status', async () => { + const resp = RestError.toProblemResponse({ message: 'bad things' }); + assert.strictEqual(resp.status, 500); + const body = await resp.json(); + assert.deepStrictEqual(body, { + title: 'Internal Server Error', + status: 500, + detail: 'bad things', + }); + }); + + it('can get instance from context invocation id', async () => { + const err = new RestError(400, 'A problem!'); + const resp = RestError.toProblemResponse(err, { + invocation: { id: 'test-id' }, + }); + assert.strictEqual(resp.status, 400); + const body = await resp.json(); + assert.deepStrictEqual(body, { + status: 400, + title: 'Bad Request', + instance: '/invocation/test-id', + detail: 'A problem!', + }); + }); + + it('can get instance from context invocation request id', async () => { + const err = new RestError(400, 'A problem!'); + const resp = RestError.toProblemResponse(err, { + invocation: { id: 'test-id', requestId: 'test-request-id' }, + }); + assert.strictEqual(resp.status, 400); + const body = await resp.json(); + assert.deepStrictEqual(body, { + status: 400, + title: 'Bad Request', + instance: '/invocation/test-request-id', + detail: 'A problem!', + }); + }); + + it('can specify additional properties', async () => { + const err = new RestError(400, 'A problem!', { todo: 'fix' }); + const resp = RestError.toProblemResponse(err, { + invocation: { id: 'test-id', requestId: 'test-request-id' }, + }); + assert.strictEqual(resp.status, 400); + const body = await resp.json(); + assert.deepStrictEqual(body, { + status: 400, + title: 'Bad Request', + instance: '/invocation/test-request-id', + detail: 'A problem!', + todo: 'fix', + }); + }); +}); diff --git a/packages/router/.prettierrc b/packages/router/.prettierrc new file mode 100644 index 0000000..56d4876 --- /dev/null +++ b/packages/router/.prettierrc @@ -0,0 +1,6 @@ +{ + "tabWidth": 2, + "useTabs": false, + "singleQuote": true, + "trailingComma": "all" +} \ No newline at end of file diff --git a/packages/router/LICENSE.txt b/packages/router/LICENSE.txt new file mode 100644 index 0000000..883ab09 --- /dev/null +++ b/packages/router/LICENSE.txt @@ -0,0 +1,264 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +APACHE JACKRABBIT SUBCOMPONENTS + +Apache Jackrabbit includes parts with separate copyright notices and license +terms. Your use of these subcomponents is subject to the terms and conditions +of the following licenses: + + XPath 2.0/XQuery 1.0 Parser: + http://www.w3.org/2002/11/xquery-xpath-applets/xgrammar.zip + + Copyright (C) 2002 World Wide Web Consortium, (Massachusetts Institute of + Technology, European Research Consortium for Informatics and Mathematics, + Keio University). All Rights Reserved. + + This work is distributed under the W3C(R) Software License in the hope + that it will be useful, but WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + W3C(R) SOFTWARE NOTICE AND LICENSE + http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231 + + This work (and included software, documentation such as READMEs, or + other related items) is being provided by the copyright holders under + the following license. By obtaining, using and/or copying this work, + you (the licensee) agree that you have read, understood, and will comply + with the following terms and conditions. + + Permission to copy, modify, and distribute this software and its + documentation, with or without modification, for any purpose and + without fee or royalty is hereby granted, provided that you include + the following on ALL copies of the software and documentation or + portions thereof, including modifications: + + 1. The full text of this NOTICE in a location viewable to users + of the redistributed or derivative work. + + 2. Any pre-existing intellectual property disclaimers, notices, + or terms and conditions. If none exist, the W3C Software Short + Notice should be included (hypertext is preferred, text is + permitted) within the body of any redistributed or derivative code. + + 3. Notice of any changes or modifications to the files, including + the date changes were made. (We recommend you provide URIs to the + location from which the code is derived.) + + THIS SOFTWARE AND DOCUMENTATION IS PROVIDED "AS IS," AND COPYRIGHT + HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, + INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS + FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE OR + DOCUMENTATION WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, + TRADEMARKS OR OTHER RIGHTS. + + COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL + OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR + DOCUMENTATION. + + The name and trademarks of copyright holders may NOT be used in + advertising or publicity pertaining to the software without specific, + written prior permission. Title to copyright in this software and + any associated documentation will at all times remain with + copyright holders. diff --git a/packages/router/README.md b/packages/router/README.md new file mode 100644 index 0000000..ce6062a --- /dev/null +++ b/packages/router/README.md @@ -0,0 +1,32 @@ +# Content Lake Shared - Router + +> A lite Router for Serverless functions presenting multiple HTTP routes + +This is one of the [Content Lake Shared](https://github.com/adobe/contentlake-shared) libraries. + +## Status + +[![GitHub license](https://img.shields.io/github/license/adobe/contentlake-shared.svg)](https://github.com/adobe/contentlake-shared/blob/main/LICENSE.txt) + +## Usage + +Install using: + +``` +npm install @adobe/contentlake-shared-router +``` + +Use with: + +``` +import { Router } from '@adobe/contentlake-shared-router'; + +async function run(request, context) { + const router = new Router(); + router.get( + '/:id', + (_req, params) => new Response(JSON.stringify({ id: params.id })), + ); + return router.handle(request, context); +} +``` \ No newline at end of file diff --git a/packages/router/package.json b/packages/router/package.json new file mode 100644 index 0000000..e4ecddb --- /dev/null +++ b/packages/router/package.json @@ -0,0 +1,33 @@ +{ + "name": "@adobe/contentlake-shared-router", + "version": "1.0.0", + "description": "A lite Router for Serverless functions presenting multiple HTTP routes", + "main": "src/index.js", + "type": "module", + "scripts": { + "test": "c8 mocha", + "lint": "eslint ." + }, + "mocha": { + "reporter": "mocha-multi-reporters", + "reporter-options": "configFile=../../.mocha-multi.json" + }, + "repository": { + "type": "git", + "url": "https://github.com/adobe/contentlake-shared" + }, + "author": "", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/adobe/contentlake-shared/issues" + }, + "homepage": "https://github.com/adobe/contentlake-shared/tree/main/packages/router#readme", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@adobe/contentlake-shared-rest-error": "^1.0.0", + "@adobe/helix-universal": "^4.3.0", + "routington": "^1.0.3" + } +} diff --git a/packages/router/src/index.js b/packages/router/src/index.js new file mode 100644 index 0000000..015af15 --- /dev/null +++ b/packages/router/src/index.js @@ -0,0 +1,135 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import routington from 'routington'; +import { RestError } from '@adobe/contentlake-shared-rest-error'; + +/** + * Function for handling a routes inside Franklin / Content Lake services + * @callback Handler + * @param {Request} req the request + * @param {Record} [params] the parameters parsed from the request + * @param {import('@adobe/helix-universal').UniversalContext} [context] the context of the request + * @returns {Promise} the response from the request + */ + +export class Router { + methods = {}; + + /** + * + * @param {string} method + * @param {string} path + * @param {Handler} handler + */ + addRoute(method, path, handler) { + if (!this.methods[method]) { + this.methods[method] = routington(); + } + this.methods[method].define(path)[0].handler = handler; + } + + /** + * + * @param {string} path + * @param {Handler} handler + */ + delete(path, handler) { + this.addRoute('DELETE', path, handler); + return this; + } + + /** + * + * @param {string} path + * @param {Handler} handler + */ + get(path, handler) { + this.addRoute('GET', path, handler); + return this; + } + + /** + * + * @param {string} path + * @param {Handler} handler + */ + post(path, handler) { + this.addRoute('POST', path, handler); + return this; + } + + /** + * + * @param {string} path + * @param {Handler} handler + */ + put(path, handler) { + this.addRoute('PUT', path, handler); + return this; + } + + /** + * Handles the specified request + * @param {Request} request + * @param {import('@adobe/helix-universal').UniversalContext} context + * @returns {Promise} + */ + async handle(request, context) { + const log = context.log || console; + const { method } = request; + let suffix = context.pathInfo?.suffix; + if (!suffix || suffix === '') { + suffix = '/'; + } + log.debug(`-> ${method} ${suffix}`); + const start = Date.now(); + /** + * @type {Response} + */ + let response; + if (this.methods[request.method]) { + const match = this.methods[method].match(suffix); + if (match?.node?.handler) { + try { + response = await match.node.handler(request, match.param, context); + } catch (err) { + log.warn('Caught exception from handler', { + method, + suffix, + err, + }); + response = RestError.toProblemResponse(err, context); + } + } + } + if (!response) { + log.debug('No handler found for route', { method, suffix }); + response = RestError.toProblemResponse( + { + status: 405, + }, + context, + ); + } + if (!response.status || !response.headers) { + response = new Response(JSON.stringify(response), { + headers: { 'Content-Type': 'application/json' }, + }); + } + log.debug( + `<- ${response.status} ${response.headers?.get('Content-Type')} ${ + Date.now() - start + }ms`, + ); + return response; + } +} diff --git a/packages/router/test/index.test.js b/packages/router/test/index.test.js new file mode 100644 index 0000000..ad03b10 --- /dev/null +++ b/packages/router/test/index.test.js @@ -0,0 +1,204 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +/* eslint-env mocha */ +import assert from 'assert'; +import { randomUUID } from 'crypto'; +import { Router } from '../src/index.js'; + +function mockContext(suffix) { + return { + pathInfo: { + suffix, + }, + env: {}, + invocation: { + id: randomUUID(), + }, + }; +} + +describe('Router Tests', () => { + it('can route request', async () => { + const router = new Router(); + router.get('/', () => new Response('Hello World')); + + const response = await router.handle( + new Request('https://localhost/'), + mockContext('/'), + ); + assert.ok(response); + assert.strictEqual(response.status, 200); + const body = await response.text(); + assert.strictEqual(body, 'Hello World'); + }); + + it('can route different request', async () => { + let root = false; + let test = false; + const router = new Router(); + router + .get('/', () => { + root = true; + }) + .get('/test', () => { + test = true; + }); + + await router.handle(new Request('https://localhost/'), mockContext('/')); + assert.ok(root); + assert.ok(!test); + + await router.handle( + new Request('https://localhost/'), + mockContext('/test'), + ); + assert.ok(test); + }); + + it('returns 405 on unrouted', async () => { + const router = new Router(); + router.get('/', () => new Response()); + + const response = await router.handle( + new Request('https://localhost/', { method: 'POST' }), + mockContext('/'), + ); + assert.ok(response); + assert.strictEqual(405, response.status); + assert.strictEqual( + 'application/problem+json', + response.headers.get('Content-Type'), + ); + }); + + describe('can register common methods', () => { + const router = new Router(); + + router + .get('/', () => new Response('GET')) + .delete('/', () => new Response('DELETE')) + .post('/', () => new Response('POST')) + .put('/', () => new Response('PUT')); + + ['GET', 'POST', 'DELETE', 'PUT'].forEach((method) => { + it(`Handles ${method}`, async () => { + const response = await router.handle( + new Request('https://localhost/', { method }), + mockContext('/'), + ); + assert.ok(response); + assert.strictEqual(200, response.status); + const body = await response.text(); + assert.strictEqual(body, method); + }); + }); + }); + + it('handles throwing handler of handling things', async () => { + const router = new Router(); + + router.get('/', () => { + throw new Error('generic'); + }); + + const response = await router.handle( + new Request('https://localhost/'), + mockContext('/'), + ); + assert.ok(response); + assert.strictEqual(500, response.status); + const body = await response.json(); + + assert.strictEqual( + 'application/problem+json', + response.headers.get('Content-Type'), + ); + assert.strictEqual('Internal Server Error', body.title); + }); + + [undefined, ''].map(async (suffix) => { + it(`uses / for suffix if suffix = '${suffix}'`, async () => { + const router = new Router().get('/', () => new Response('GET')); + const response = await router.handle( + new Request('https://localhost/', { method: 'GET' }), + mockContext(suffix), + ); + assert.equal(response.status, 200); + }); + }); + + it('does not fail if pathInfo not defined', async () => { + const router = new Router().get('/', () => new Response('GET')); + const response = await router.handle( + new Request('https://localhost/', { method: 'GET' }), + { + env: {}, + invocation: { + id: randomUUID(), + }, + }, + ); + assert.equal(response.status, 200); + }); + + it('parses params', async () => { + const router = new Router(); + + router.get( + '/:id', + (_req, params) => new Response(JSON.stringify({ id: params.id })), + ); + + const response = await router.handle( + new Request('https://localhost/'), + mockContext('/test-id'), + ); + assert.ok(response); + assert.strictEqual(200, response.status); + const body = await response.json(); + assert.deepStrictEqual(body, { id: 'test-id' }); + }); + + it('passes request params and context', async () => { + const router = new Router(); + + const request = new Request('https://localhost/'); + const context = mockContext('/test-id2'); + router.get('/:id', (req, params, ctx) => { + assert.strictEqual(req, request); + assert.strictEqual(ctx, context); + return new Response(JSON.stringify({ id: params.id })); + }); + + const response = await router.handle(request, context); + assert.ok(response); + assert.strictEqual(200, response.status); + const body = await response.json(); + assert.deepStrictEqual(body, { id: 'test-id2' }); + }); + + it('handles object responses', async () => { + const router = new Router(); + router.get('/', () => ({ + message: 'Hello World', + })); + + const response = await router.handle( + new Request('https://localhost/'), + mockContext('/'), + ); + assert.ok(response); + assert.strictEqual(response.status, 200); + const body = await response.json(); + assert.deepStrictEqual(body, { message: 'Hello World' }); + }); +});